From c330bf823801e656ad4a496443b881932baef90d Mon Sep 17 00:00:00 2001 From: CCG Date: Sun, 8 Dec 2019 17:26:33 +0800 Subject: [PATCH 001/358] feat(Tile): long click to open in app --- app/src/main/AndroidManifest.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fa43fa6e5b..1fdfeb2733 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -24,6 +24,9 @@ + + + Date: Sun, 8 Dec 2019 20:07:05 +0800 Subject: [PATCH 002/358] fix(launch): when open the app from launcher, stay current activity --- app/src/main/AndroidManifest.xml | 2 +- .../java/com/github/kr328/clash/service/ClashNotification.kt | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fa43fa6e5b..eba37766b3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,7 +19,7 @@ android:name=".MainActivity" android:label="@string/launch_name" android:screenOrientation="portrait" - android:launchMode="singleTask"> + android:launchMode="singleTop"> diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 6138317328..51a5e5527b 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -29,12 +29,14 @@ class ClashNotification(private val context: Service) { .setContentIntent( PendingIntent.getActivity( context, - CLASH_STATUS_NOTIFICATION_ID, + (Math.random() * 100).toInt(), Intent().setComponent( ComponentName.createRelative( context, MAIN_ACTIVITY_NAME ) + ).setFlags( + Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP ), PendingIntent.FLAG_UPDATE_CURRENT ) From 24e1732091c1a5089858bfefc776fe229367b997 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 8 Dec 2019 23:40:24 +0800 Subject: [PATCH 003/358] add back button on action bar & add dns hijacking option --- .../java/com/github/kr328/clash/BaseActivity.kt | 17 +++++++++++++++++ .../java/com/github/kr328/clash/MainActivity.kt | 4 ++++ .../github/kr328/clash/SettingProxyActivity.kt | 5 +++++ app/src/main/res/values/strings.xml | 2 ++ app/src/main/res/xml/setting_proxy.xml | 4 ++++ core/src/main/golang/server/tun.go | 8 ++++++++ core/src/main/golang/tun/tun.go | 14 ++++++++++++-- .../java/com/github/kr328/clash/core/Clash.kt | 3 ++- .../kr328/clash/service/IClashService.aidl | 2 +- .../clash/service/IClashSettingService.aidl | 2 ++ .../github/kr328/clash/service/ClashService.kt | 4 ++-- .../kr328/clash/service/ClashSettingService.kt | 11 +++++++++++ .../github/kr328/clash/service/TunService.kt | 2 +- 13 files changed, 71 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 7614a3b78c..db17bf275d 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -7,6 +7,7 @@ import android.content.ServiceConnection import android.os.Bundle import android.os.IBinder import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar import com.github.kr328.clash.core.event.* import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.ClashService @@ -99,6 +100,22 @@ abstract class BaseActivity : AppCompatActivity(), IClashEventObserver { super.onDestroy() } + override fun setSupportActionBar(toolbar: Toolbar?) { + super.setSupportActionBar(toolbar) + + supportActionBar?.setDisplayHomeAsUpEnabled(shouldDisplayHomeAsUpEnabled()) + } + + open fun shouldDisplayHomeAsUpEnabled(): Boolean { + return true + } + + override fun onSupportNavigateUp(): Boolean { + this.onBackPressed() + + return true + } + private val observerBinder = object : IClashEventObserver.Stub() { override fun onLogEvent(event: LogEvent?) = this@BaseActivity.onLogEvent(event) diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index b14d7ee5da..8800712778 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -77,6 +77,10 @@ class MainActivity : BaseActivity() { } } + override fun shouldDisplayHomeAsUpEnabled(): Boolean { + return false + } + override fun onProcessEvent(event: ProcessEvent?) { runOnUiThread { if (event == lastEvent) diff --git a/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt index 4a5cd1a99b..65d2d789d9 100644 --- a/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt @@ -13,6 +13,7 @@ class SettingProxyActivity : BaseActivity() { companion object { private const val KEY_BYPASS_PRIVATE_NETWORK = "key_vpn_setting_bypass_private_network" private const val KEY_IPV6_SUPPORT = "key_vpn_setting_ipv6_support" + private const val KEY_DNS_HIJACKING = "key_dns_hijacking" } @Keep @@ -29,11 +30,13 @@ class SettingProxyActivity : BaseActivity() { val ipv6 = settings.isIPv6Enabled val privateNetwork = settings.isBypassPrivateNetwork + val dnsHijacking = settings.isDnsHijackingEnabled requireActivity().runOnUiThread { findPreference(KEY_IPV6_SUPPORT)?.isChecked = ipv6 findPreference(KEY_BYPASS_PRIVATE_NETWORK)?.isChecked = privateNetwork + findPreference(KEY_DNS_HIJACKING)?.isChecked = dnsHijacking } } } @@ -49,6 +52,8 @@ class SettingProxyActivity : BaseActivity() { settings.isBypassPrivateNetwork = findPreference(KEY_BYPASS_PRIVATE_NETWORK)?.isChecked ?: true + settings.isDnsHijackingEnabled = + findPreference(KEY_DNS_HIJACKING)?.isChecked ?: true } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3deede717d..fcd240ba27 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,6 +69,8 @@ Bypass private network subnet IPv6 Support Routing IPv6 traffic + DNS Hijacking + Redirect ALL dns traffic to clash Global Allow all apps access vpn diff --git a/app/src/main/res/xml/setting_proxy.xml b/app/src/main/res/xml/setting_proxy.xml index cef26c4362..12efeb1aa6 100644 --- a/app/src/main/res/xml/setting_proxy.xml +++ b/app/src/main/res/xml/setting_proxy.xml @@ -9,6 +9,10 @@ android:key="key_vpn_setting_ipv6_support" android:title="@string/proxy_setting_vpn_setting_ipv6" android:summary="@string/proxy_setting_vpn_setting_ipv6_summary" /> + socket.setFileDescriptorsForSend(arrayOf(fd)) socket.outputStream.write(0) socket.outputStream.flush() output.writeInt(mtu) + output.writeInt(if ( dnsHijacking ) 1 else 0) output.writeInt(0x243) } } 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 83712a0316..be56221f04 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 @@ -19,7 +19,7 @@ interface IClashService { // Control void setSelectProxy(String proxy, String selected); - void startTunDevice(in ParcelFileDescriptor fd, int mtu); + void startTunDevice(in ParcelFileDescriptor fd, int mtu, boolean dnsHijacking); void stopTunDevice(); void start(); void stop(); 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 index 31fc50581f..2670625301 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashSettingService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashSettingService.aidl @@ -4,11 +4,13 @@ interface IClashSettingService { // Set void setIPv6Enabled(boolean enabled); void setBypassPrivateNetwork(boolean enabled); + void setDnsHijackingEnabled(boolean enabled); void setAccessControl(int mode, in String[] applications); // Get boolean isIPv6Enabled(); boolean isBypassPrivateNetwork(); + boolean isDnsHijackingEnabled(); 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 d371f9e50a..3a3cb287cf 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 @@ -101,11 +101,11 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, clash.process.stop() } - override fun startTunDevice(fd: ParcelFileDescriptor, mtu: Int) { + override fun startTunDevice(fd: ParcelFileDescriptor, mtu: Int, dnsHijacking: Boolean) { try { notification.setVpn(true) - clash.startTunDevice(fd.fileDescriptor, mtu) + clash.startTunDevice(fd.fileDescriptor, mtu, dnsHijacking) } catch (e: Exception) { Log.e("Start tun failure", e) 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 index dea5d84d46..22b656883c 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashSettingService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashSettingService.kt @@ -8,6 +8,7 @@ class ClashSettingService(context: Context): IClashSettingService.Stub() { 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_DNS_HIJACKING_ENABLED = "key_dns_hijacking_enabled" const val KEY_BYPASS_PRIVATE_NETWORK = "key_bypass_private_network" const val ACCESS_CONTROL_MODE_ALLOW_ALL = 0 @@ -34,6 +35,12 @@ class ClashSettingService(context: Context): IClashSettingService.Stub() { } } + override fun setDnsHijackingEnabled(enabled: Boolean) { + preference.edit { + putBoolean(KEY_DNS_HIJACKING_ENABLED, enabled) + } + } + override fun setBypassPrivateNetwork(enabled: Boolean) { preference.edit { putBoolean(KEY_BYPASS_PRIVATE_NETWORK, enabled) @@ -48,6 +55,10 @@ class ClashSettingService(context: Context): IClashSettingService.Stub() { return preference.getBoolean(KEY_IPV6_ENABLED, true) } + override fun isDnsHijackingEnabled(): Boolean { + return preference.getBoolean(KEY_DNS_HIJACKING_ENABLED, true) + } + override fun getAccessControlApps(): Array { return preference.getStringSet(KEY_ACCESS_CONTROL_APPS, emptySet())?.toTypedArray()!! } 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 0540d9fea3..2c28946b2b 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 @@ -112,7 +112,7 @@ class TunService : VpnService(), IClashEventObserver { } ProcessEvent.STARTED -> { start = false - clash.startTunDevice(fileDescriptor, VPN_MTU) + clash.startTunDevice(fileDescriptor, VPN_MTU, settings.isDnsHijackingEnabled) Log.i("STARTED") } From f13abcbe49285f0b4cd75647d8f97d4bf8de8eb0 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 8 Dec 2019 23:47:35 +0800 Subject: [PATCH 004/358] fix url test not working after refreshing --- .../java/com/github/kr328/clash/ProxyActivity.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt index da19279e8a..a2672ee660 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt @@ -142,7 +142,7 @@ class ProxyActivity : BaseActivity() { .filterIsInstance() .map { it.now } - val changed = if (listDataOldChanged.size != listDataChanged.size) + val changed = (if (listDataOldChanged.size != listDataChanged.size) (0..listData.size).toList() else { listDataChanged.mapIndexed { index, i -> @@ -150,8 +150,12 @@ class ProxyActivity : BaseActivity() { emptyList() else listOf(listDataOldChanged[index], i) - }.flatten() - } + }.flatten() + listData.withIndex().filter { + it.value is ListProxy.ListProxyHeader + }.map { + it.index + } + }).toSet() runOnUiThread { activity_proxies_swipe.isRefreshing = false @@ -159,7 +163,7 @@ class ProxyActivity : BaseActivity() { (activity_proxies_list.adapter!! as ProxyAdapter).apply { elements = listData - changed.toSet().forEach { + changed.forEach { notifyItemChanged(it) } } From 9a893fc0a059d00352c90d1dbfcc5e2d12903d64 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 8 Dec 2019 23:49:36 +0800 Subject: [PATCH 005/358] fix form adapter dialog title --- .../main/java/com/github/kr328/clash/adapter/FormAdapter.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt index 73f72888cc..f8d9e34121 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt @@ -79,6 +79,7 @@ class FormAdapter( castedHolder.clickable.setOnClickListener { showTextEditDialog( castedHolder.text.text.toString(), + castedHolder.title.text.toString(), castedHolder.text.hint.toString() ) { current.content = it @@ -126,9 +127,9 @@ class FormAdapter( return elements[position].javaClass.hashCode() } - private fun showTextEditDialog(initial: String, hint: String, callback: (String) -> Unit) { + private fun showTextEditDialog(initial: String, title: String, hint: String, callback: (String) -> Unit) { MaterialAlertDialogBuilder(activity) - .setTitle(R.string.clash_profile_name) + .setTitle(title) .setView(R.layout.dialog_text_edit) .setPositiveButton(R.string.ok) { _, _ -> } .setNegativeButton(R.string.cancel) { _, _ -> } From a1d623c42b254793c1e56a4eb96f3b486281a152 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 8 Dec 2019 23:50:45 +0800 Subject: [PATCH 006/358] use marquee for proxy name overflow --- app/src/main/res/layout/adapter_proxy_item.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/res/layout/adapter_proxy_item.xml b/app/src/main/res/layout/adapter_proxy_item.xml index a6f5bcd605..3d40c92bcd 100644 --- a/app/src/main/res/layout/adapter_proxy_item.xml +++ b/app/src/main/res/layout/adapter_proxy_item.xml @@ -20,8 +20,9 @@ Date: Sun, 8 Dec 2019 23:55:40 +0800 Subject: [PATCH 007/358] fix restore selected proxy after update profile --- app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt | 3 +-- .../com/github/kr328/clash/service/IClashProfileService.aidl | 1 + .../com/github/kr328/clash/service/ClashProfileService.kt | 4 ++++ .../com/github/kr328/clash/service/data/ClashProfileDao.kt | 3 +++ 4 files changed, 9 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 41508f7f84..1803960de3 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -180,8 +180,7 @@ class ProfilesActivity : BaseActivity() { } runClash { clash -> - clash.profileService.addProfile(profile - .copy(lastUpdate = System.currentTimeMillis())) + clash.profileService.touchProfile(profile.id) } } catch (e: Exception) { diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileService.aidl index 511b9386bb..986c0831d5 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileService.aidl @@ -7,5 +7,6 @@ interface IClashProfileService { void setActiveProfile(int id); void addProfile(in ClashProfileEntity profile); void removeProfile(int id); + void touchProfile(int id); ClashProfileEntity queryActiveProfile(); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt index a384deee19..8d64133d36 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt @@ -42,6 +42,10 @@ class ClashProfileService(context: Context, private val master: Master) : master.preformProfileChanged() } + override fun touchProfile(id: Int) { + profileDao.touchProfile(id) + } + override fun queryProfiles(): Array { return profileDao.queryProfiles() } diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt index ffd7225f26..2a4f7d8a89 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt @@ -21,4 +21,7 @@ interface ClashProfileDao { @Query("DELETE FROM profiles WHERE id = :id") fun removeProfile(id: Int) + + @Query("UPDATE profiles SET last_update = :lastUpdate WHERE id = :id") + fun touchProfile(id: Int, lastUpdate: Long = System.currentTimeMillis()) } \ No newline at end of file From 19dcc4a1fa1b908e08095374003c10d1c3300fbc Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 8 Dec 2019 23:57:29 +0800 Subject: [PATCH 008/358] use profile log level --- core/src/main/golang/server/event.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/core/src/main/golang/server/event.go b/core/src/main/golang/server/event.go index f12a253099..48a95c95e3 100644 --- a/core/src/main/golang/server/event.go +++ b/core/src/main/golang/server/event.go @@ -134,9 +134,14 @@ func handlePullLogEvent(client *net.UnixConn) { for { select { case elm := <-subseribe: - buf.Reset() msg := elm.(*log.Event) + if msg.LogLevel < log.Level() { + break + } + + buf.Reset() + var payload struct { Level int `json:"level"` Messgae string `json:"message"` From 3b7a0b0101db8cc1f81f8a0008df9ec1298e3470 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 00:06:04 +0800 Subject: [PATCH 009/358] fix start on boot display --- .../kr328/clash/SettingApplicationActivity.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt index 143cf5349d..c127a10222 100644 --- a/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt @@ -19,8 +19,15 @@ class SettingApplicationActivity : BaseActivity() { @Keep class Fragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener { + private var startOnBootStatus = false + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.setting_application, rootKey) + + findPreference(KEY_START_ON_BOOT)?.also { + it.isChecked = startOnBootStatus + it.onPreferenceChangeListener = this + } } override fun onAttach(context: Context) { @@ -34,11 +41,12 @@ class SettingApplicationActivity : BaseActivity() { ) ) + startOnBootStatus = status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED + findPreference(KEY_START_ON_BOOT)?.also { - it.isChecked = status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED + it.isChecked = startOnBootStatus it.onPreferenceChangeListener = this } - } override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { @@ -94,6 +102,4 @@ class SettingApplicationActivity : BaseActivity() { setSupportActionBar(activity_setting_application_toolbar) } - - } \ No newline at end of file From 6947118d2e8bd13ce601475eca8f4a53a44ac94d Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 00:13:50 +0800 Subject: [PATCH 010/358] update version code --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e87fea29fc..051f8d7353 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10007 - versionName "1.0.7-alpha" + versionCode 10008 + versionName "1.0.8-alpha" } buildTypes { release { From 41a2e7effcd7f0eb8efad96a950dd65670b566a6 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 00:41:13 +0800 Subject: [PATCH 011/358] fix proxy name display & profile not fully display --- app/build.gradle | 6 +-- .../github/kr328/clash/ProfilesActivity.kt | 8 ++-- .../kr328/clash/adapter/ProfileAdapter.kt | 43 ++++++++++++++++--- app/src/main/res/layout/activity_profiles.xml | 24 ++--------- .../main/res/layout/adapter_proxy_item.xml | 2 +- 5 files changed, 48 insertions(+), 35 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 051f8d7353..421f53614a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10008 - versionName "1.0.8-alpha" + versionCode 10009 + versionName "1.0.9-alpha" } buildTypes { release { @@ -41,7 +41,7 @@ dependencies { implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.core:core-ktx:1.2.0-rc01' - implementation 'androidx.fragment:fragment-ktx:1.2.0-rc02' + implementation 'androidx.fragment:fragment-ktx:1.2.0-rc03' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta3' implementation "androidx.room:room-runtime:$room_version" diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 1803960de3..550dbdb2f6 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -33,10 +33,6 @@ class ProfilesActivity : BaseActivity() { setSupportActionBar(activity_profiles_toolbar) - activity_profiles_new_profile.setOnClickListener { - startActivity(Intent(this, CreateProfileActivity::class.java)) - } - activity_profiles_main_list.layoutManager = object : LinearLayoutManager(this) { override fun canScrollHorizontally(): Boolean = false override fun canScrollVertically(): Boolean = false @@ -44,7 +40,9 @@ class ProfilesActivity : BaseActivity() { activity_profiles_main_list.adapter = ProfileAdapter(this, this::onProfileClick, this::onOperateClick, - this::onProfileLongClick) + this::onProfileLongClick) { + startActivity(Intent(this, CreateProfileActivity::class.java)) + } } override fun onStart() { diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt index 962620dacd..62b74267cc 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt @@ -6,6 +6,7 @@ import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.R import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.view.FatItem import com.github.kr328.clash.view.RadioFatItem import java.text.SimpleDateFormat import java.util.* @@ -13,13 +14,24 @@ import java.util.* class ProfileAdapter(private val context: Context, private val onClick: (ClashProfileEntity) -> Unit, private val onOperateClick: (ClashProfileEntity) -> Unit, - private val onLongClicked: (View,ClashProfileEntity) -> Unit) : - RecyclerView.Adapter() { + private val onLongClicked: (View,ClashProfileEntity) -> Unit, + private val onNewProfile: () -> Unit) : + RecyclerView.Adapter() { var profiles: Array = emptyArray() class ProfileViewHolder(val view: RadioFatItem) : RecyclerView.ViewHolder(view) + class NewProfileHolder(val view: FatItem) : RecyclerView.ViewHolder(view) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + if ( viewType == 0 ) { + return NewProfileHolder(FatItem(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + }) + } - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ProfileViewHolder { return ProfileViewHolder( RadioFatItem(context).apply { layoutParams = ViewGroup.LayoutParams( @@ -31,11 +43,25 @@ class ProfileAdapter(private val context: Context, } override fun getItemCount(): Int { - return profiles.size + return profiles.size + 1 } - override fun onBindViewHolder(holder: ProfileViewHolder, position: Int) { + override fun onBindViewHolder(raw: RecyclerView.ViewHolder, position: Int) { + if ( position == profiles.size ) { + val holder = raw as NewProfileHolder + + holder.view.icon = context.getDrawable(R.drawable.ic_new_profile) + holder.view.title = context.getString(R.string.clash_new_profile) + + holder.view.setOnClickListener { + onNewProfile() + } + + return + } + val current = profiles[position] + val holder = raw as ProfileViewHolder holder.view.title = current.name holder.view.isChecked = current.active @@ -74,4 +100,11 @@ class ProfileAdapter(private val context: Context, } } } + + override fun getItemViewType(position: Int): Int { + return if ( position == profiles.size ) + 0 + else + 1 + } } \ No newline at end of file diff --git a/app/src/main/res/layout/activity_profiles.xml b/app/src/main/res/layout/activity_profiles.xml index 91ff361e15..77c47d43bb 100644 --- a/app/src/main/res/layout/activity_profiles.xml +++ b/app/src/main/res/layout/activity_profiles.xml @@ -17,26 +17,8 @@ android:layout_height="60dp" /> - - - - - - - - - + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_proxy_item.xml b/app/src/main/res/layout/adapter_proxy_item.xml index 3d40c92bcd..6d390ac63c 100644 --- a/app/src/main/res/layout/adapter_proxy_item.xml +++ b/app/src/main/res/layout/adapter_proxy_item.xml @@ -20,7 +20,7 @@ Date: Mon, 9 Dec 2019 01:03:05 +0800 Subject: [PATCH 012/358] fix profile scroll not working & pin selected access control apps --- .../java/com/github/kr328/clash/ProfilesActivity.kt | 5 +---- .../com/github/kr328/clash/SettingAccessActivity.kt | 12 +++++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 550dbdb2f6..af3055b859 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -33,10 +33,7 @@ class ProfilesActivity : BaseActivity() { setSupportActionBar(activity_profiles_toolbar) - activity_profiles_main_list.layoutManager = object : LinearLayoutManager(this) { - override fun canScrollHorizontally(): Boolean = false - override fun canScrollVertically(): Boolean = false - } + activity_profiles_main_list.layoutManager = LinearLayoutManager(this) activity_profiles_main_list.adapter = ProfileAdapter(this, this::onProfileClick, this::onOperateClick, 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 77a9f78f70..56cf7de716 100644 --- a/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt @@ -102,6 +102,7 @@ class SettingAccessActivity : BaseActivity() { } val exclude = resources.getStringArray(R.array.default_disallow_application).toSet() + val selected = settings.accessControlApps.toMutableSet() val applications = packageManager.getInstalledApplications(0) .filterNot { @@ -114,10 +115,11 @@ class SettingAccessActivity : BaseActivity() { app.loadIcon(packageManager) ) } - .sortedBy { app -> - app.name - } - val selected = settings.accessControlApps.toMutableSet() + .sortedWith( + compareBy( + { app -> selected.contains(app.packageName) }, + { app -> app.name }) + ) runOnUiThread { activity_setting_access_app_list.apply { @@ -230,7 +232,7 @@ class SettingAccessActivity : BaseActivity() { } private fun updateSelectedMode(mode: Int) { - when ( mode ) { + when (mode) { 0 -> { activity_setting_access_allow_all.isChecked = true activity_setting_access_allow.isChecked = false From 903c201f2d27fa9eda4b1f7331132c0eed9e5fc6 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 01:04:32 +0800 Subject: [PATCH 013/358] update version code --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 421f53614a..a9f6224973 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10009 - versionName "1.0.9-alpha" + versionCode 10010 + versionName "1.0.10-alpha" } buildTypes { release { From 9cca3c958ef3cee5398dfb99d27ea8abad9983e8 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 01:08:35 +0800 Subject: [PATCH 014/358] fix access control order --- .../main/java/com/github/kr328/clash/SettingAccessActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 56cf7de716..90a585f115 100644 --- a/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt @@ -117,7 +117,7 @@ class SettingAccessActivity : BaseActivity() { } .sortedWith( compareBy( - { app -> selected.contains(app.packageName) }, + { app -> !selected.contains(app.packageName) }, { app -> app.name }) ) From 9fe2ec39aef89c8ef5065be8a1d218964d5c1c3e Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 23:42:53 +0800 Subject: [PATCH 015/358] adjust ui --- .../com/github/kr328/clash/MainActivity.kt | 4 --- app/src/main/res/layout/activity_feedback.xml | 2 +- .../main/res/layout/activity_import_file.xml | 2 +- .../main/res/layout/activity_import_url.xml | 2 +- app/src/main/res/layout/activity_logs.xml | 2 +- app/src/main/res/layout/activity_main.xml | 32 ++++++++++++++----- .../res/layout/activity_main_clash_status.xml | 2 +- .../res/layout/activity_main_profiles.xml | 2 +- .../res/layout/activity_main_proxy_manage.xml | 2 +- .../main/res/layout/activity_new_profile.xml | 2 +- app/src/main/res/layout/activity_profiles.xml | 2 +- app/src/main/res/layout/activity_proxies.xml | 2 +- .../res/layout/activity_setting_access.xml | 2 +- .../layout/activity_setting_application.xml | 2 +- .../main/res/layout/activity_setting_main.xml | 2 +- .../res/layout/activity_setting_proxy.xml | 2 +- .../main/res/layout/preference_main_item.xml | 2 +- .../main/res/layout/preference_proxy_item.xml | 2 +- app/src/main/res/layout/view_fat_item.xml | 2 +- .../main/res/layout/view_radio_fat_item.xml | 2 +- service/src/main/res/values/arrays.xml | 1 + 21 files changed, 43 insertions(+), 30 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 8800712778..a0418519d1 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -27,10 +27,6 @@ class MainActivity : BaseActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) - setSupportActionBar(activity_main_toolbar.apply { - setLogo(R.mipmap.ic_launcher_foreground) - }) - activity_main_clash_proxies.setOnClickListener { startActivity(Intent(this, ProxyActivity::class.java)) } diff --git a/app/src/main/res/layout/activity_feedback.xml b/app/src/main/res/layout/activity_feedback.xml index 438ea220f5..e5bcaf847a 100644 --- a/app/src/main/res/layout/activity_feedback.xml +++ b/app/src/main/res/layout/activity_feedback.xml @@ -11,7 +11,7 @@ android:id="@+id/activity_feedback_toolbar" android:elevation="4dp" android:layout_width="match_parent" - android:layout_height="60dp" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content"> + android:layout_height="wrap_content"> + android:layout_height="wrap_content"> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 19af7353b5..559651c46b 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -9,12 +9,28 @@ android:animateLayoutChanges="true" tools:context=".MainActivity"> - + android:gravity="center_vertical"> + + + + @@ -44,7 +60,7 @@ @@ -71,7 +87,7 @@ @@ -98,7 +114,7 @@ @@ -125,7 +141,7 @@ diff --git a/app/src/main/res/layout/activity_main_clash_status.xml b/app/src/main/res/layout/activity_main_clash_status.xml index 7b3584df3f..d7d08ea859 100644 --- a/app/src/main/res/layout/activity_main_clash_status.xml +++ b/app/src/main/res/layout/activity_main_clash_status.xml @@ -33,7 +33,7 @@ android:layout_toEndOf="@id/activity_main_clash_status_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="20dp" + android:layout_marginStart="25dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:textColor="@android:color/white" android:text="@string/clash_status_stopped" /> diff --git a/app/src/main/res/layout/activity_main_profiles.xml b/app/src/main/res/layout/activity_main_profiles.xml index a38693c620..faa5dc1ddb 100644 --- a/app/src/main/res/layout/activity_main_profiles.xml +++ b/app/src/main/res/layout/activity_main_profiles.xml @@ -34,7 +34,7 @@ android:layout_toEndOf="@id/activity_main_profiles_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="20dp" + android:layout_marginStart="25dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:text="@string/clash_profiles" /> diff --git a/app/src/main/res/layout/activity_main_proxy_manage.xml b/app/src/main/res/layout/activity_main_proxy_manage.xml index d6922301fb..f4cf800cf0 100644 --- a/app/src/main/res/layout/activity_main_proxy_manage.xml +++ b/app/src/main/res/layout/activity_main_proxy_manage.xml @@ -34,7 +34,7 @@ android:layout_toEndOf="@id/activity_main_clash_proxies_icon" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="20dp" + android:layout_marginStart="25dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium" android:text="@string/clash_proxy_manage" /> diff --git a/app/src/main/res/layout/activity_new_profile.xml b/app/src/main/res/layout/activity_new_profile.xml index 75c7e22d60..1d22807e3c 100644 --- a/app/src/main/res/layout/activity_new_profile.xml +++ b/app/src/main/res/layout/activity_new_profile.xml @@ -11,7 +11,7 @@ + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> diff --git a/app/src/main/res/layout/activity_setting_application.xml b/app/src/main/res/layout/activity_setting_application.xml index fbaec5bf5e..517df13c07 100644 --- a/app/src/main/res/layout/activity_setting_application.xml +++ b/app/src/main/res/layout/activity_setting_application.xml @@ -12,7 +12,7 @@ android:id="@+id/activity_setting_application_toolbar" android:elevation="4dp" android:layout_width="match_parent" - android:layout_height="60dp" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> + android:layout_height="wrap_content" /> diff --git a/app/src/main/res/layout/view_radio_fat_item.xml b/app/src/main/res/layout/view_radio_fat_item.xml index f7b2d9896f..405b6b5475 100644 --- a/app/src/main/res/layout/view_radio_fat_item.xml +++ b/app/src/main/res/layout/view_radio_fat_item.xml @@ -23,7 +23,7 @@ android:layout_height="wrap_content" android:layout_alignParentStart="true" android:layout_toStartOf="@id/view_radio_fat_item_operation_clickable" - android:layout_marginStart="70dp" + android:layout_marginStart="72dp" android:paddingTop="15dp" android:paddingBottom="15dp" android:orientation="vertical"> diff --git a/service/src/main/res/values/arrays.xml b/service/src/main/res/values/arrays.xml index af1e81d467..61a924ac30 100644 --- a/service/src/main/res/values/arrays.xml +++ b/service/src/main/res/values/arrays.xml @@ -18,6 +18,7 @@ 160.0.0.0/5 168.0.0.0/6 172.0.0.0/12 + 172.19.0.0/30 172.32.0.0/11 172.64.0.0/10 172.128.0.0/9 From 475dd96ac2427637e87f63700003db14409c9036 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 23:45:28 +0800 Subject: [PATCH 016/358] adjust text --- .../main/java/com/github/kr328/clash/MainActivity.kt | 4 ++-- app/src/main/res/layout/activity_main_clash_status.xml | 2 +- app/src/main/res/values/strings.xml | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index a0418519d1..f761466c6e 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -53,7 +53,7 @@ class MainActivity : BaseActivity() { activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_stopped) activity_main_clash_status_title.text = getString(R.string.clash_status_stopped) - activity_main_clash_status_summary.text = getString(R.string.clash_status_click_to_start) + activity_main_clash_status_summary.text = getString(R.string.clash_status_tap_to_start) activity_main_clash_proxies.visibility = View.GONE activity_main_clash_logs.visibility = View.GONE @@ -99,7 +99,7 @@ class MainActivity : BaseActivity() { activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_stopped) activity_main_clash_status_title.text = getString(R.string.clash_status_stopped) activity_main_clash_status_summary.text = - getString(R.string.clash_status_click_to_start) + getString(R.string.clash_status_tap_to_start) activity_main_clash_proxies.visibility = View.GONE activity_main_clash_logs.visibility = View.GONE } diff --git a/app/src/main/res/layout/activity_main_clash_status.xml b/app/src/main/res/layout/activity_main_clash_status.xml index d7d08ea859..dc195a2dfe 100644 --- a/app/src/main/res/layout/activity_main_clash_status.xml +++ b/app/src/main/res/layout/activity_main_clash_status.xml @@ -40,7 +40,7 @@ Clash for Android Stopped - Click here to start + Tap to start Running %s Forwarded @@ -14,7 +14,7 @@ Profiles %s Activated - Unselected + Not selected Logs Settings @@ -73,11 +73,11 @@ Redirect ALL dns traffic to clash Global - Allow all apps access vpn + Allow all apps Whitelist - Allow follow apps access vpn + Only allowing selected apps Blacklist - Disallow follow apps access vpn + Disallow selected apps Loading Show Mode Hide Mode From e2073b5f7caccedbf114f5727917256cc781f869 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 23:51:59 +0800 Subject: [PATCH 017/358] disable colorized --- .../java/com/github/kr328/clash/service/ClashNotification.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 51a5e5527b..8618f7d2f0 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -24,7 +24,7 @@ class ClashNotification(private val context: Service) { .setSmallIcon(R.drawable.ic_notification_icon) .setOngoing(true) .setColor(context.getColor(R.color.colorAccentService)) - .setColorized(true) + //.setColorized(true) .setShowWhen(false) .setContentIntent( PendingIntent.getActivity( From a665acaf6341e71b5f93edc2388361eab0479519 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 23:52:33 +0800 Subject: [PATCH 018/358] fix profile event --- .../java/com/github/kr328/clash/service/ClashProfileService.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt index 8d64133d36..31dd07ab49 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt @@ -44,6 +44,8 @@ class ClashProfileService(context: Context, private val master: Master) : override fun touchProfile(id: Int) { profileDao.touchProfile(id) + + master.preformProfileChanged() } override fun queryProfiles(): Array { From 32b6608f116b500d7f759ddd95a59ffd84165a1d Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 9 Dec 2019 23:54:45 +0800 Subject: [PATCH 019/358] update version code --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index a9f6224973..f0a1dd3c3f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10010 - versionName "1.0.10-alpha" + versionCode 10011 + versionName "1.0.11-alpha" } buildTypes { release { From d7df27b91dac97aee95a401e2c8dc46df5043918 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 00:24:00 +0800 Subject: [PATCH 020/358] adjust ui --- app/src/main/res/layout/activity_main_clash_status.xml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/res/layout/activity_main_clash_status.xml b/app/src/main/res/layout/activity_main_clash_status.xml index dc195a2dfe..6155c4011f 100644 --- a/app/src/main/res/layout/activity_main_clash_status.xml +++ b/app/src/main/res/layout/activity_main_clash_status.xml @@ -23,8 +23,9 @@ From 64803bef1ef6bdee20dd77e7822f81de7a8c0bef Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 00:32:32 +0800 Subject: [PATCH 021/358] compatible with legacy rules --- app/src/main/java/com/github/kr328/clash/model/ClashRule.kt | 3 +++ app/src/main/res/layout/activity_main.xml | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt b/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt index baa411f5d7..37ed8a192b 100644 --- a/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt +++ b/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt @@ -46,6 +46,9 @@ data class ClashRule(val matcher: Matcher, val pattern: String, val target: Stri "DST-PORT" -> DST_PORT "SRC-PORT" -> SRC_PORT "MATCH" -> MATCH + // Deprecated + "SOURCE-IP-CIDR" -> SRC_IP_CIDR + "FINAL" -> MATCH else -> throw YamlException("Invalid matcher $s", 0, 0) } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 559651c46b..4070bf4312 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,6 +1,5 @@ + android:layout_marginStart="25dp"/> From 968ef77896e4198c56669d21aba867bc01a3de92 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 00:33:08 +0800 Subject: [PATCH 022/358] update version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f0a1dd3c3f..01914d1942 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10011 - versionName "1.0.11-alpha" + versionCode 10012 + versionName "1.0.12-alpha" } buildTypes { release { From 88ef4d2f1075fc621a01ae5d22eb6da49db8c264 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 01:22:02 +0800 Subject: [PATCH 023/358] fix dns hijacking not working --- app/build.gradle | 4 +-- core/src/main/golang/profile/load.go | 40 +++++++++++++++------------- core/src/main/golang/tun/tun.go | 2 +- 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 01914d1942..f5ebe35592 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -11,8 +11,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10012 - versionName "1.0.12-alpha" + versionCode 10013 + versionName "1.0.13-alpha" } buildTypes { release { diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index 8fe076c2a2..9f1e7d1a78 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -6,6 +6,7 @@ import ( adapters "github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/component/auth" trie "github.com/Dreamacro/clash/component/domain-trie" + "github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" @@ -16,25 +17,6 @@ import ( "github.com/kr328/cfa/tun" ) -var defaultDNSResolver = dns.New(dns.Config{ - Main: []dns.NameServer{ - dns.NameServer{Net: "tcp", Addr: "1.1.1.1:53"}, - dns.NameServer{Net: "tcp", Addr: "8.8.8.8:53"}, - dns.NameServer{Net: "tcp", Addr: "208.67.222.222:53"}, - dns.NameServer{Net: "", Addr: "119.29.29.29:53"}, - dns.NameServer{Net: "", Addr: "223.5.5.5:53"}, - dns.NameServer{Net: "", Addr: "114.114.114.114:53"}, - }, - Fallback: make([]dns.NameServer, 0), - IPv6: true, - EnhancedMode: dns.MAPPING, - Pool: nil, - FallbackFilter: dns.FallbackFilter{ - GeoIP: false, - IPCIDR: make([]*net.IPNet, 0), - }, -}) - // LoadDefault - load default configure func LoadDefault() { defaultC := &config.Config{ @@ -114,6 +96,26 @@ func LoadFromFile(path string) error { } if dns.DefaultResolver == nil { + _, ipnet, _ := net.ParseCIDR("198.18.0.1/16") + pool, _ := fakeip.New(ipnet, 1000) + + var defaultDNSResolver = dns.New(dns.Config{ + Main: []dns.NameServer{ + dns.NameServer{Net: "tcp", Addr: "1.1.1.1:53"}, + dns.NameServer{Net: "tcp", Addr: "208.67.222.222:53"}, + dns.NameServer{Net: "", Addr: "119.29.29.29:53"}, + dns.NameServer{Net: "", Addr: "223.5.5.5:53"}, + }, + Fallback: make([]dns.NameServer, 0), + IPv6: true, + EnhancedMode: dns.FAKEIP, + Pool: pool, + FallbackFilter: dns.FallbackFilter{ + GeoIP: false, + IPCIDR: make([]*net.IPNet, 0), + }, + }) + dns.DefaultResolver = defaultDNSResolver } diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go index 5f78d92294..2a1a78012d 100644 --- a/core/src/main/golang/tun/tun.go +++ b/core/src/main/golang/tun/tun.go @@ -61,5 +61,5 @@ func ResetDnsRedirect() { } func SetDnsHijacking(enabled bool) { - dnsHijacking = false + dnsHijacking = enabled } From 7cc53bbf32d2863643f073ce6de739ab519b493a Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 11:14:13 +0800 Subject: [PATCH 024/358] add clash://install-config handler --- app/src/main/AndroidManifest.xml | 52 +++-- .../github/kr328/clash/ImportUrlActivity.kt | 7 + core/build.gradle | 190 +++++++++--------- 3 files changed, 132 insertions(+), 117 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 2a14901c87..d606f7cf7d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,81 +18,89 @@ + android:launchMode="singleTop" + android:screenOrientation="portrait"> - + + + + + + + + + - - + @@ -101,8 +109,8 @@ + android:enabled="false" + android:exported="true"> diff --git a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt index e8aac0beb6..bafb711f68 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt @@ -51,6 +51,13 @@ class ImportUrlActivity : BaseActivity() { activity_import_url_save.setOnClickListener { checkAndInsert() } + + if ( intent.action == Intent.ACTION_VIEW + && intent.data?.scheme == "clash" + && intent.data?.host == "install-config") { + (elements[1] as FormAdapter.TextType).content = + intent.data?.getQueryParameter("url") ?: "" + } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { diff --git a/core/build.gradle b/core/build.gradle index f93f497731..1740143d73 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -1,96 +1,96 @@ -apply plugin: 'com.android.library' -apply plugin: 'kotlin-android-extensions' -apply plugin: 'kotlin-android' -apply plugin: 'kotlinx-serialization' - -android { - compileSdkVersion 29 - buildToolsVersion "29.0.2" - - defaultConfig { - minSdkVersion 24 - targetSdkVersion 29 - versionCode 1 - versionName "1.0" - - consumerProguardFiles 'consumer-rules.pro' - } - - buildTypes { - release { - minifyEnabled false - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' - } - } - - sourceSets { - main { - assets.srcDirs += ["$buildDir/intermediates/dynamic_assets"] - jniLibs.srcDirs += ["$buildDir/intermediates/native_output"] - } - } - compileOptions { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 - } -} - -dependencies { - implementation "androidx.core:core-ktx:1.1.0" - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" -} - -afterEvaluate { - def compileArch = ["arm64-v8a", "x86_64"] - def options = new GolangBuildOptions(file("src/main/golang"), file("$buildDir/intermediates/native_output"), "23") - - def ts = compileArch.stream().map { abi -> - tasks.register("golangBuildFor" + abi.replace("-", "").replace("_", ""), GolangBuildTask.class) { - setOptions options, abi - } - }.toArray() - - for ( task in tasks ) { - if ( task.name.contains("JniLib") ) - task.dependsOn(ts) - } - - def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) { - onlyIf { - System.currentTimeMillis() - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > 24 * 3600 * 1000L - } - - output = "$buildDir/intermediates/cache/Country.tar.gz" - } - - def es = tasks.register("extractMMDB", Copy.class) { - onlyIf { - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > file("$buildDir/intermediates/dynamic_assets/Country.mmdb").lastModified() - } - - from(tarTree("$buildDir/intermediates/cache/Country.tar.gz")) { - include "**/*.mmdb" - } - into "$buildDir/intermediates/cache/mmdb" - - doLast { - file("$buildDir/intermediates/dynamic_assets").mkdirs() - fileTree("$buildDir/intermediates/cache/mmdb").visit { FileVisitDetails details -> - if ( details.path.endsWith("Country.mmdb") ) - details.file.renameTo(file("$buildDir/intermediates/dynamic_assets/Country.mmdb")) - } - } - - dependsOn ds - } - - for ( task in tasks ) { - if ( task.name.contains("generate") && task.name.contains("Assets") ) - task.dependsOn(es) - } -} - -repositories { - mavenCentral() +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'kotlinx-serialization' + +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + sourceSets { + main { + assets.srcDirs += ["$buildDir/intermediates/dynamic_assets"] + jniLibs.srcDirs += ["$buildDir/intermediates/native_output"] + } + } + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } +} + +dependencies { + implementation "androidx.core:core-ktx:1.1.0" + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" +} + +afterEvaluate { + def compileArch = ["arm64-v8a", "x86_64"] + def options = new GolangBuildOptions(file("src/main/golang"), file("$buildDir/intermediates/native_output"), "23") + + def ts = compileArch.stream().map { abi -> + tasks.register("golangBuildFor" + abi.replace("-", "").replace("_", ""), GolangBuildTask.class) { + setOptions options, abi + } + }.toArray() + + for ( task in tasks ) { + if ( task.name.contains("JniLib") ) + task.dependsOn(ts) + } + + def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) { + onlyIf { + System.currentTimeMillis() - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > 24 * 3600 * 1000L + } + + output = "$buildDir/intermediates/cache/Country.tar.gz" + } + + def es = tasks.register("extractMMDB", Copy.class) { + onlyIf { + file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > file("$buildDir/intermediates/dynamic_assets/Country.mmdb").lastModified() + } + + from(tarTree("$buildDir/intermediates/cache/Country.tar.gz")) { + include "**/*.mmdb" + } + into "$buildDir/intermediates/cache/mmdb" + + doLast { + file("$buildDir/intermediates/dynamic_assets").mkdirs() + fileTree("$buildDir/intermediates/cache/mmdb").visit { FileVisitDetails details -> + if ( details.path.endsWith("Country.mmdb") ) + details.file.renameTo(file("$buildDir/intermediates/dynamic_assets/Country.mmdb")) + } + } + + dependsOn ds + } + + for ( task in tasks ) { + if ( task.name.contains("generate") && task.name.contains("Assets") ) + task.dependsOn(es) + } +} + +repositories { + mavenCentral() } \ No newline at end of file From 6c58f552b860579894ca76ca2f1e5e5093e03cb8 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 11:23:47 +0800 Subject: [PATCH 025/358] tile title = profile name --- .../com/github/kr328/clash/TileService.kt | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/TileService.kt b/app/src/main/java/com/github/kr328/clash/TileService.kt index 7248976e3a..bc967ed145 100644 --- a/app/src/main/java/com/github/kr328/clash/TileService.kt +++ b/app/src/main/java/com/github/kr328/clash/TileService.kt @@ -1,24 +1,29 @@ package com.github.kr328.clash -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.service.quicksettings.Tile import android.service.quicksettings.TileService import com.github.kr328.clash.core.event.ProcessEvent import com.github.kr328.clash.service.ClashService import com.github.kr328.clash.service.Constants import com.github.kr328.clash.service.IClashService +import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.utils.ServiceUtils class TileService : TileService() { override fun onClick() { val tile = qsTile - when ( tile.state ) { + when (tile.state) { Tile.STATE_INACTIVE -> { ServiceUtils.startProxyService(this) } Tile.STATE_ACTIVE -> { - val binder = clashStatusReceiver.peekService(this, Intent(this, ClashService::class.java)) + val binder = + clashStatusReceiver.peekService(this, Intent(this, ClashService::class.java)) runCatching { val clash = IClashService.Stub.asInterface(binder) @@ -34,7 +39,9 @@ class TileService : TileService() { } private fun refreshTileStatus() { - when ( currentStatus ) { + val current = getCurrentStatus() + + when (current.first) { ProcessEvent.STARTED -> { qsTile.state = Tile.STATE_ACTIVE } @@ -43,10 +50,12 @@ class TileService : TileService() { } } + qsTile.label = current.second?.name + qsTile.updateTile() } - private val clashStatusReceiver = object: BroadcastReceiver() { + private val clashStatusReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { refreshTileStatus() } @@ -55,8 +64,10 @@ class TileService : TileService() { override fun onCreate() { super.onCreate() - registerReceiver(clashStatusReceiver, - IntentFilter(Constants.CLASH_PROCESS_BROADCAST_ACTION)) + registerReceiver( + clashStatusReceiver, + IntentFilter(Constants.CLASH_PROCESS_BROADCAST_ACTION) + ) } override fun onDestroy() { @@ -65,12 +76,13 @@ class TileService : TileService() { unregisterReceiver(clashStatusReceiver) } - private val currentStatus: ProcessEvent - get() { - val service = clashStatusReceiver.peekService(this, Intent(this, ClashService::class.java)) + private fun getCurrentStatus(): Pair { + val service = + IClashService.Stub.asInterface(clashStatusReceiver + .peekService(this, Intent(this, ClashService::class.java))) return runCatching { - IClashService.Stub.asInterface(service)?.currentProcessStatus - }.getOrNull() ?: ProcessEvent.STOPPED + (service?.currentProcessStatus ?: ProcessEvent.STOPPED) to service?.profileService?.queryActiveProfile() + }.getOrNull() ?: ProcessEvent.STOPPED to null } } \ No newline at end of file From 340fa28c6ae218ea4edd4e01d8f8a73922ab1dc8 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 11:33:35 +0800 Subject: [PATCH 026/358] add marquee text view --- .../kr328/clash/view/MarqueeTextView.kt | 21 +++++++++++++++++++ .../main/res/layout/adapter_proxy_item.xml | 3 +-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt diff --git a/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt b/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt new file mode 100644 index 0000000000..c4c8282465 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt @@ -0,0 +1,21 @@ +package com.github.kr328.clash.view + +import android.content.Context +import android.text.TextUtils +import android.util.AttributeSet +import android.widget.TextView + +class MarqueeTextView @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyleAttr: Int = 0, + defStyleRes: Int = 0 +): TextView(context, attributeSet, defStyleAttr, defStyleRes) { + init { + ellipsize = TextUtils.TruncateAt.MARQUEE + } + + override fun isFocused(): Boolean { + return true + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_proxy_item.xml b/app/src/main/res/layout/adapter_proxy_item.xml index 6d390ac63c..9877acaf33 100644 --- a/app/src/main/res/layout/adapter_proxy_item.xml +++ b/app/src/main/res/layout/adapter_proxy_item.xml @@ -18,9 +18,8 @@ android:layout_width="wrap_content" android:layout_height="wrap_content"> - Date: Tue, 10 Dec 2019 13:10:56 +0800 Subject: [PATCH 027/358] add crashlytics --- app/build.gradle | 5 ++-- .../com/github/kr328/clash/MainApplication.kt | 24 +++++++++++++++++++ 2 files changed, 27 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index f5ebe35592..096c08f1bb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,6 +3,8 @@ apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' +apply plugin: 'com.google.gms.google-services' +apply plugin: 'io.fabric' android { compileSdkVersion 29 @@ -50,6 +52,5 @@ dependencies { implementation "com.charleskorn.kaml:kaml:0.14.0" implementation "com.google.android.material:material:1.2.0-alpha02" implementation 'com.google.firebase:firebase-analytics:17.2.1' + implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' } - -apply plugin: 'com.google.gms.google-services' diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 0736332172..7f2f7b6f7a 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,6 +2,7 @@ package com.github.kr328.clash import android.app.Application import android.content.Context +import com.github.kr328.clash.core.Constants import com.github.kr328.clash.core.utils.Log import com.google.firebase.FirebaseApp @@ -29,5 +30,28 @@ class MainApplication : Application() { } catch (e: IllegalStateException) { Log.i("Already registered") } + + Log.handler = object: Log.LogHandler { + override fun info(message: String, throwable: Throwable?) { + android.util.Log.i(Constants.TAG, message, throwable) + } + + override fun warn(message: String, throwable: Throwable?) { + + } + + override fun error(message: String, throwable: Throwable?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun wtf(message: String, throwable: Throwable?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun debug(message: String, throwable: Throwable?) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + } } } \ No newline at end of file From 7cd52a84dbf9ae54804b9766c28c617d5afcdfd5 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 13:22:40 +0800 Subject: [PATCH 028/358] update version & remove log runControlNoException --- app/build.gradle | 4 ++-- .../com/github/kr328/clash/MainApplication.kt | 20 +++++++++++++++---- .../com/github/kr328/clash/core/BaseClash.kt | 9 ++------- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 096c08f1bb..449a3e3282 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10013 - versionName "1.0.13-alpha" + versionCode 10014 + versionName "1.0.14-alpha" } buildTypes { release { diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 7f2f7b6f7a..c6c24bb2f8 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,6 +2,7 @@ package com.github.kr328.clash import android.app.Application import android.content.Context +import com.crashlytics.android.Crashlytics import com.github.kr328.clash.core.Constants import com.github.kr328.clash.core.utils.Log import com.google.firebase.FirebaseApp @@ -37,21 +38,32 @@ class MainApplication : Application() { } override fun warn(message: String, throwable: Throwable?) { + throwable?.also { + Crashlytics.logException(it) + } + android.util.Log.w(Constants.TAG, message, throwable) } override fun error(message: String, throwable: Throwable?) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + throwable?.also { + Crashlytics.logException(it) + } + + android.util.Log.e(Constants.TAG, message, throwable) } override fun wtf(message: String, throwable: Throwable?) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + throwable?.also { + Crashlytics.logException(it) + } + + android.util.Log.wtf(Constants.TAG, message, throwable) } override fun debug(message: String, throwable: Throwable?) { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + android.util.Log.d(Constants.TAG, message, throwable) } - } } } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/BaseClash.kt b/core/src/main/java/com/github/kr328/clash/core/BaseClash.kt index 6801770dfb..104d3b43c1 100644 --- a/core/src/main/java/com/github/kr328/clash/core/BaseClash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/BaseClash.kt @@ -46,18 +46,13 @@ abstract class BaseClash(private val controllerPath: File) { ): R? { return try { runControl(command, block) - } catch (e: Exception) { - Log.w("Run command $command failure", e) - null - } + } catch (ignored: Exception) { null } } protected fun runControlNoException(command: Int) { try { runControl(command) - } catch (e: Exception) { - Log.w("Run command $command failure", e) - } + } catch (ignored: Exception) {} } protected fun DataOutputStream.writeString(string: String) { From 7238e919c51285d0175a2d4637fe5e0d9146784d Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 13:45:08 +0800 Subject: [PATCH 029/358] fix tile service clash start failure --- .../java/com/github/kr328/clash/BootCompleteReceiver.kt | 7 ++----- app/src/main/java/com/github/kr328/clash/TileService.kt | 2 +- .../java/com/github/kr328/clash/utils/ServiceUtils.kt | 8 ++++++++ 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt b/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt index bc334bc4c7..7c666817c2 100644 --- a/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt +++ b/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt @@ -4,16 +4,13 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Build +import com.github.kr328.clash.utils.ServiceUtils class BootCompleteReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (Intent.ACTION_BOOT_COMPLETED != intent?.action || context == null) return - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(Intent(context, ClashStartService::class.java)) - } else { - context.startService(Intent(context, ClashStartService::class.java)) - } + ServiceUtils.startStarterService(context) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/TileService.kt b/app/src/main/java/com/github/kr328/clash/TileService.kt index bc967ed145..b46dbb2a6b 100644 --- a/app/src/main/java/com/github/kr328/clash/TileService.kt +++ b/app/src/main/java/com/github/kr328/clash/TileService.kt @@ -19,7 +19,7 @@ class TileService : TileService() { when (tile.state) { Tile.STATE_INACTIVE -> { - ServiceUtils.startProxyService(this) + ServiceUtils.startStarterService(this) } Tile.STATE_ACTIVE -> { val binder = diff --git a/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt index 20a9f83ff6..9e68f1a5b1 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.Intent import android.net.VpnService import android.os.Build +import com.github.kr328.clash.ClashStartService import com.github.kr328.clash.MainApplication import com.github.kr328.clash.service.ClashService import com.github.kr328.clash.service.TunService @@ -35,4 +36,11 @@ object ServiceUtils { return null } } + + fun startStarterService(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + context.startForegroundService(Intent(context, ClashStartService::class.java)) + else + context.startService(Intent(context, ClashStartService::class.java)) + } } \ No newline at end of file From d48beadcbc280716fe189022b9b6d3dd33bb2a0b Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 14:14:26 +0800 Subject: [PATCH 030/358] fix empty tile name --- app/src/main/java/com/github/kr328/clash/TileService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/github/kr328/clash/TileService.kt b/app/src/main/java/com/github/kr328/clash/TileService.kt index b46dbb2a6b..f8e1d0057c 100644 --- a/app/src/main/java/com/github/kr328/clash/TileService.kt +++ b/app/src/main/java/com/github/kr328/clash/TileService.kt @@ -50,7 +50,7 @@ class TileService : TileService() { } } - qsTile.label = current.second?.name + qsTile.label = current.second?.name ?: getString(R.string.launch_name) qsTile.updateTile() } From ddeff030e3c6362ea0ed174a9ed1a3e73eb36f01 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 14:17:54 +0800 Subject: [PATCH 031/358] update version code --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 449a3e3282..8b502faa3f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10014 - versionName "1.0.14-alpha" + versionCode 10015 + versionName "1.0.15-alpha" } buildTypes { release { From 41b263a7eedd9415734a94d57d9499449f58244a Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 14:21:02 +0800 Subject: [PATCH 032/358] fix fabric --- app/src/main/java/com/github/kr328/clash/MainApplication.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index c6c24bb2f8..ee2987ab8c 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -6,6 +6,7 @@ import com.crashlytics.android.Crashlytics import com.github.kr328.clash.core.Constants import com.github.kr328.clash.core.utils.Log import com.google.firebase.FirebaseApp +import io.fabric.sdk.android.Fabric class MainApplication : Application() { companion object { @@ -27,6 +28,7 @@ class MainApplication : Application() { try { FirebaseApp.initializeApp(this) + Fabric.with(this) Log.i("Registered") } catch (e: IllegalStateException) { Log.i("Already registered") From 4b498d7dc7da8b8c7c2f208d43a6c80e1390bd1d Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 14:59:19 +0800 Subject: [PATCH 033/358] update tile on clash reload --- .../java/com/github/kr328/clash/TileService.kt | 17 +++++++++++------ .../github/kr328/clash/service/ClashService.kt | 5 ++++- .../com/github/kr328/clash/service/Constants.kt | 1 + 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/TileService.kt b/app/src/main/java/com/github/kr328/clash/TileService.kt index f8e1d0057c..02cec2f1e5 100644 --- a/app/src/main/java/com/github/kr328/clash/TileService.kt +++ b/app/src/main/java/com/github/kr328/clash/TileService.kt @@ -7,8 +7,10 @@ import android.content.IntentFilter import android.service.quicksettings.Tile import android.service.quicksettings.TileService import com.github.kr328.clash.core.event.ProcessEvent +import com.github.kr328.clash.core.event.ProfileReloadEvent import com.github.kr328.clash.service.ClashService import com.github.kr328.clash.service.Constants +import com.github.kr328.clash.service.IClashEventObserver import com.github.kr328.clash.service.IClashService import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.utils.ServiceUtils @@ -35,29 +37,29 @@ class TileService : TileService() { } override fun onStartListening() { - refreshTileStatus() + refreshStatus() } - private fun refreshTileStatus() { + private fun refreshStatus() { val current = getCurrentStatus() when (current.first) { ProcessEvent.STARTED -> { qsTile.state = Tile.STATE_ACTIVE + qsTile.label = current.second?.name ?: getString(R.string.launch_name) } ProcessEvent.STOPPED -> { qsTile.state = Tile.STATE_INACTIVE + qsTile.label = getString(R.string.launch_name) } } - qsTile.label = current.second?.name ?: getString(R.string.launch_name) - qsTile.updateTile() } private val clashStatusReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - refreshTileStatus() + refreshStatus() } } @@ -66,7 +68,10 @@ class TileService : TileService() { registerReceiver( clashStatusReceiver, - IntentFilter(Constants.CLASH_PROCESS_BROADCAST_ACTION) + IntentFilter().apply { + addAction(Constants.CLASH_PROCESS_BROADCAST_ACTION) + addAction(Constants.CLASH_RELOAD_BROADCAST_ACTION) + } ) } 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 3a3cb287cf..5ab938773f 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 @@ -290,6 +290,10 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } } + override fun onProfileReloaded(event: ProfileReloadEvent?) { + sendBroadcast(Intent(Constants.CLASH_RELOAD_BROADCAST_ACTION).setPackage(packageName)) + } + override fun onSpeedEvent(event: SpeedEvent?) { notification.setSpeed(event?.up ?: 0, event?.down ?: 0) } @@ -313,7 +317,6 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, override fun onBandwidthEvent(event: BandwidthEvent?) {} override fun onLogEvent(event: LogEvent?) {} override fun onErrorEvent(event: ErrorEvent?) {} - override fun onProfileReloaded(event: ProfileReloadEvent?) {} override fun asBinder(): IBinder = object : Binder() { override fun queryLocalInterface(descriptor: String): IInterface? { return this@ClashService diff --git a/service/src/main/java/com/github/kr328/clash/service/Constants.kt b/service/src/main/java/com/github/kr328/clash/service/Constants.kt index 1416826648..925e5305a4 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Constants.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Constants.kt @@ -2,4 +2,5 @@ package com.github.kr328.clash.service object Constants { const val CLASH_PROCESS_BROADCAST_ACTION = "com.github.kr328.clash.ClashService.ClashProcessEvent" + const val CLASH_RELOAD_BROADCAST_ACTION = "com.github.kr328.clash.ClashService.ProfileReloadEvent" } \ No newline at end of file From a154230bca5035cbba7b3d470c17ef7edafe0f57 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 22:28:43 +0800 Subject: [PATCH 034/358] fix some bugs --- app/build.gradle | 4 ++-- .../main/java/com/github/kr328/clash/MainApplication.kt | 7 +++---- .../main/java/com/github/kr328/clash/ProfilesActivity.kt | 3 ++- app/src/main/java/com/github/kr328/clash/ProxyActivity.kt | 2 +- app/src/main/java/com/github/kr328/clash/TileService.kt | 3 +++ .../java/com/github/kr328/clash/core/model/ProxyPacket.kt | 4 ++-- 6 files changed, 13 insertions(+), 10 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 8b502faa3f..8840242b91 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10015 - versionName "1.0.15-alpha" + versionCode 10016 + versionName "1.0.16-alpha" } buildTypes { release { diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index ee2987ab8c..2a1983bb2a 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -26,12 +26,11 @@ class MainApplication : Application() { override fun onCreate() { super.onCreate() - try { + runCatching { FirebaseApp.initializeApp(this) + } + runCatching { Fabric.with(this) - Log.i("Registered") - } catch (e: IllegalStateException) { - Log.i("Already registered") } Log.handler = object: Log.LogHandler { diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index af3055b859..636e74b668 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -189,7 +189,8 @@ class ProfilesActivity : BaseActivity() { } runOnUiThread { - dialog?.dismiss() + if ( dialog?.isShowing == true ) + dialog?.dismiss() } } } diff --git a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt index a2672ee660..ee599d9b62 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt @@ -78,7 +78,7 @@ class ProxyActivity : BaseActivity() { runClash { clash -> val packet = clash.queryAllProxies() val proxies = packet.proxies - val order = (proxies["GLOBAL".hashCode()] ?: error("GLOBAL not found")).all + val order = (proxies["GLOBAL".hashCode()]?.all ?: emptyList()) .mapIndexed { index, i -> i to index }.toMap() val listData = proxies diff --git a/app/src/main/java/com/github/kr328/clash/TileService.kt b/app/src/main/java/com/github/kr328/clash/TileService.kt index 02cec2f1e5..3ec689c4cc 100644 --- a/app/src/main/java/com/github/kr328/clash/TileService.kt +++ b/app/src/main/java/com/github/kr328/clash/TileService.kt @@ -41,6 +41,9 @@ class TileService : TileService() { } private fun refreshStatus() { + if ( qsTile == null ) + return + val current = getCurrentStatus() when (current.first) { diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt index c9fdb2c121..651297dfae 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.Serializable @Serializable data class ProxyPacket(val mode: String, val proxies: Map): Parcelable { @Serializable - data class Proxy(val name: String, val type: Type, val now: Int, val all: Set, val delay: Long) + data class Proxy(val name: String, val type: Type, val now: Int, val all: List, val delay: Long) enum class Type { SELECT, @@ -78,7 +78,7 @@ data class ProxyPacket(val mode: String, val proxies: Map): Parcelab } val now = hashed[entry.value.second.now]?.first ?: 0 - val all = entry.value.second.all.mapNotNull { hashed[it]?.first }.toSet() + val all = entry.value.second.all.mapNotNull { hashed[it]?.first } val delay = entry.value.second.history.firstOrNull()?.delay ?: 0 entry.value.first to Proxy(entry.key, type, now, all, delay) From 1b87bc0417cc8489bf8f592e4e0ca42cf5759889 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 22:35:31 +0800 Subject: [PATCH 035/358] finish activity on import successfully --- .../kr328/clash/CreateProfileActivity.kt | 19 +++++++++++++++---- .../github/kr328/clash/ImportFileActivity.kt | 5 +++++ .../github/kr328/clash/ImportUrlActivity.kt | 5 +++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt index a04ecc4f32..13300886eb 100644 --- a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash +import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle @@ -23,6 +24,7 @@ class CreateProfileActivity : BaseActivity() { R.string.clash_new_profile_url_summary ) ) + private const val IMPORT_REQUEST_CODE = 1024 } class Adapter(private val context: Context) : BaseAdapter() { @@ -51,6 +53,15 @@ class CreateProfileActivity : BaseActivity() { } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if ( requestCode == IMPORT_REQUEST_CODE && resultCode == Activity.RESULT_OK ) { + finish() + return + } + + super.onActivityResult(requestCode, resultCode, data) + } + data class AdapterData(val icon: Int, val title: Int, val summary: Int) override fun onCreate(savedInstanceState: Bundle?) { @@ -64,19 +75,19 @@ class CreateProfileActivity : BaseActivity() { setOnItemClickListener { _, _, index, _ -> when (index) { 0 -> { - startActivity( + startActivityForResult( Intent( this@CreateProfileActivity, ImportFileActivity::class.java - ) + ), IMPORT_REQUEST_CODE ) } 1 -> { - startActivity( + startActivityForResult( Intent( this@CreateProfileActivity, ImportUrlActivity::class.java - ) + ), IMPORT_REQUEST_CODE ) } } diff --git a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt index 8452a6609f..3cbc5211ac 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash +import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle @@ -50,6 +51,8 @@ class ImportFileActivity : BaseActivity() { checkAndInsert() } } + + setResult(Activity.RESULT_CANCELED) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -123,6 +126,8 @@ class ImportFileActivity : BaseActivity() { ) } + setResult(Activity.RESULT_OK) + finish() } catch (e: Exception) { Snackbar.make( diff --git a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt index bafb711f68..50072daa60 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash +import android.app.Activity import android.content.Intent import android.os.Bundle import android.view.View @@ -58,6 +59,8 @@ class ImportUrlActivity : BaseActivity() { (elements[1] as FormAdapter.TextType).content = intent.data?.getQueryParameter("url") ?: "" } + + setResult(Activity.RESULT_CANCELED) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -123,6 +126,8 @@ class ImportUrlActivity : BaseActivity() { } runOnUiThread { + setResult(Activity.RESULT_OK) + finish() } } From 5ddeaf278316d18253ef19541376f3eb22e91628 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 10 Dec 2019 22:58:13 +0800 Subject: [PATCH 036/358] add privacy policy --- PRIVACY_POLICY.md | 50 +++++++++++++++++++++++++++++++++++++++++++++++ README.md | 6 ++++++ 2 files changed, 56 insertions(+) create mode 100644 PRIVACY_POLICY.md diff --git a/PRIVACY_POLICY.md b/PRIVACY_POLICY.md new file mode 100644 index 0000000000..6acc2a2ff9 --- /dev/null +++ b/PRIVACY_POLICY.md @@ -0,0 +1,50 @@ +## Privacy Policy + +The Clash for Android is built as an Open Source software. This app is provided by personal at no cost and is intended for use as is. + +This page is used to inform visitors regarding our policies with the collection, use, and disclosure of Personal Information if anyone decided to use our app. + +If you choose to use our app, then you agree to the collection and use of information in relation to this policy. The Personal Information that we collect is used for providing and improving the app. We will not use or share your information with anyone except as described in this Privacy Policy. + +The terms used in this Privacy Policy have the same meanings as in our Terms and Conditions, which is accessible at Clash for Android unless otherwise defined in this Privacy Policy. + +**Information Collection and Use** + +For a better experience, while using our app, we may require you to provide us with certain personally identifiable information. The information that we request will be retained by us and used as described in this privacy policy. + +The app does use third party services that may collect information used to identify you. + +Link to privacy policy of third party service providers used by the app + +* [Google Play Services](https://www.google.com/policies/privacy/) +* [Firebase Analytics](https://firebase.google.com/policies/analytics) + +**Log Data** + +We want to inform you that whenever you use our app, in a case of an error in the app we collect data and information (through third party products) on your phone called Log Data. This Log Data may include information such as your device Internet Protocol (“IP”) address, device name, operating system version, the configuration of the app when utilizing our App, the time and date of your use of the app, and other statistics. + +**Cookies** + +Cookies are files with a small amount of data that are commonly used as anonymous unique identifiers. These are sent to your browser from the websites that you visit and are stored on your device's internal memory. + +This app does not use these “cookies” explicitly. However, the app may use third party code and libraries that use “cookies” to collect information and improve their services. You have the option to either accept or refuse these cookies and know when a cookie is being sent to your device. If you choose to refuse our cookies, you may not be able to use some portions of this app. + +**Security** + +We value your trust in providing us your Personal Information, thus we are striving to use commercially acceptable means of protecting it. But remember that no method of transmission over the internet, or method of electronic storage is 100% secure and reliable, and we cannot guarantee its absolute security. + +**Links to Other Sites** + +This app may contain links to other sites. If you click on a third-party link, you will be directed to that site. Note that these external sites are not operated by us. Therefore, we strongly advise you to review the Privacy Policy of these websites. We have no control over and assume no responsibility for the content, privacy policies, or practices of any third-party sites or services. + +**Children’s Privacy** + +These Services do not address anyone under the age of 13\. We do not knowingly collect personally identifiable information from children under 13\. In the case we discover that a child under 13 has provided us with personal information, we immediately delete this from our servers. If you are a parent or guardian and you are aware that your child has provided us with personal information, please contact us so that we will be able to do necessary actions. + +**Changes to This Privacy Policy** + +We may update our Privacy Policy from time to time. Thus, you are advised to review this page periodically for any changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are effective immediately after they are posted on this page. + +**Contact Us** + +If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us. diff --git a/README.md b/README.md index e3abe38997..ca0c7566a0 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,12 @@ See also [LICENSE](./LICENSE) and [NOTICE](./NOTICE) +### Privacy Policy + +See also [PRIVACY_POLICY.md](./PRIVACY_POLICY.md) + + + ### Build 1. Update submodules From c4f4c157bda6da1aae243f02419a22fab751fe39 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Wed, 11 Dec 2019 09:29:59 +0800 Subject: [PATCH 037/358] try fix app bundle not extract native libs --- app/build.gradle | 7 +++++-- app/src/main/AndroidManifest.xml | 1 + gradle.properties | 4 ++++ .../com/github/kr328/clash/service/ClashService.kt | 3 +++ .../com/github/kr328/clash/service/StubSoLoader.kt | 10 ++++++++++ 5 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 service/src/main/java/com/github/kr328/clash/service/StubSoLoader.kt diff --git a/app/build.gradle b/app/build.gradle index 8840242b91..dd7da1e90f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10016 - versionName "1.0.16-alpha" + versionCode 10017 + versionName "1.0.17-alpha" } buildTypes { release { @@ -29,6 +29,9 @@ android { kotlinOptions { jvmTarget = "1.8" } + bundle { + + } } dependencies { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index d606f7cf7d..fff4b5c57a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ Date: Wed, 11 Dec 2019 10:21:56 +0800 Subject: [PATCH 038/358] fix libclash.so not found --- app/build.gradle | 4 +- gradle.properties | 50 +++++++++---------- .../kr328/clash/service/ClashService.kt | 2 - .../kr328/clash/service/StubSoLoader.kt | 10 ---- 4 files changed, 27 insertions(+), 39 deletions(-) delete mode 100644 service/src/main/java/com/github/kr328/clash/service/StubSoLoader.kt diff --git a/app/build.gradle b/app/build.gradle index dd7da1e90f..45507803b6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10017 - versionName "1.0.17-alpha" + versionCode 10018 + versionName "1.0.18-alpha" } buildTypes { release { diff --git a/gradle.properties b/gradle.properties index a15bc6d1a0..44e0c530ea 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,25 +1,25 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official - -kapt.incremental.apt=false - -android.bundle.enableUncompressedNativeLibraries=false \ No newline at end of file +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official + +kapt.incremental.apt=false + +android.bundle.enableUncompressedNativeLibs=false \ No newline at end of file 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 6311286e99..969314e310 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 @@ -185,8 +185,6 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, override fun onCreate() { super.onCreate() - StubSoLoader.loadSo() - clash = Clash( this, filesDir.resolve("clash"), diff --git a/service/src/main/java/com/github/kr328/clash/service/StubSoLoader.kt b/service/src/main/java/com/github/kr328/clash/service/StubSoLoader.kt deleted file mode 100644 index 90402f54fe..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/StubSoLoader.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.kr328.clash.service - -import androidx.annotation.Keep - -@Keep -object StubSoLoader { - fun loadSo() { - System.loadLibrary("clash") - } -} \ No newline at end of file From dafba83b8d4359fced1de5aac055bbe4ad10ef35 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 14 Dec 2019 14:42:00 +0800 Subject: [PATCH 039/358] auto build clash bind --- buildSrc/src/main/java/GolangBindTask.kt | 138 ++++++++++++++++++ core/build.gradle | 16 +- core/src/main/golang/bridge/callback.go | 12 ++ core/src/main/golang/bridge/profiles.go | 11 ++ core/src/main/golang/go.mod | 1 + core/src/main/golang/go.sum | 21 +++ core/src/main/golang/main.go | 44 ------ core/src/main/golang/tun/tun.go | 25 ++-- .../github/kr328/clash/service/TunService.kt | 2 +- 9 files changed, 202 insertions(+), 68 deletions(-) create mode 100644 buildSrc/src/main/java/GolangBindTask.kt create mode 100644 core/src/main/golang/bridge/callback.go create mode 100644 core/src/main/golang/bridge/profiles.go diff --git a/buildSrc/src/main/java/GolangBindTask.kt b/buildSrc/src/main/java/GolangBindTask.kt new file mode 100644 index 0000000000..3b7ba2f94a --- /dev/null +++ b/buildSrc/src/main/java/GolangBindTask.kt @@ -0,0 +1,138 @@ +import org.apache.tools.ant.taskdefs.condition.Os +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.tasks.TaskAction +import java.io.File +import java.io.FileReader +import java.io.FileWriter +import java.util.* +import kotlin.concurrent.thread + +open class GolangBindTask : DefaultTask() { + companion object { + private val STUB_GO_MOD_CONTENT = """ + module github.com/kr328/cfa-bind + + require ( + github.com/kr328/cfa v0.0.0 // redirect + github.com/Dreamacro/clash v0.0.0 // redirect + ) + + replace github.com/kr328/cfa v0.0.0 => {SOURCE_PATH} + replace github.com/Dreamacro/clash v0.0.0 => {SOURCE_PATH}/clash + """.trimIndent() + private val STUB_GO_FILE_CONTENT = """ + package main + + import "github.com/kr328/cfa/bridge" + + func main() {} + """.trimIndent() + } + + val javaOutput: File + get() { + return project.buildDir.resolve("intermediates/go_output/generate_java") + } + val nativeOutput: File + get() { + return project.buildDir.resolve("intermediates/go_output/native_library") + } + private val goBuildPath: File + get() { + return project.buildDir.resolve("intermediates/go_build") + } + private val goPath: File + get() { + return goBuildPath.resolve("go_path") + } + private val goBindPath: File + get() { + return goBuildPath.resolve("go_bind_path") + } + private val sourcePath: File + get() { + return project.file("src/main/golang") + } + private val properties by lazy { + FileReader(project.rootProject.file("local.properties")).use { + Properties().apply { load(it) } + } + } + private val environment = mutableMapOf() + + init { + + } + + @TaskAction + fun process() { + environment.put("GOPATH", goPath.absolutePath) + environment.put("ANDROID_HOME", findAndroidSdkPath().absolutePath) + environment.put("ANDROID_NDK_HOME", findAndroidNdkPath().absolutePath) + + if ( Os.isFamily(Os.FAMILY_WINDOWS) ) + environment.put("Path", System.getenv("Path") + ";" + goPath.resolve("bin")) + else + environment.put("PATH", System.getenv("PATH") + ":" + goPath.resolve("bin")) + + goBindPath.deleteRecursively() + goBindPath.mkdirs() + + "go get golang.org/x/mobile/cmd/gomobile".exec() + + FileWriter(goBindPath.resolve("go.mod")).use { + it.write(STUB_GO_MOD_CONTENT.replace("{SOURCE_PATH}", sourcePath.absolutePath)) + } + FileWriter(goBindPath.resolve("main.go")).use { + it.write(STUB_GO_FILE_CONTENT) + } + + "go mod vendor".exec(goBindPath) + + goBindPath.resolve("vendor") + .copyRecursively(goPath.resolve("src"), overwrite = true) + + "gomobile init".exec(goBuildPath) + "gomobile bind -target=android github.com/kr328/cfa/bridge".exec(goBuildPath) + } + + private fun findAndroidNdkPath(): File { + return properties.getProperty("ndk.dir")?.let { File(it) }?.takeIf { it.exists() } + ?: throw GradleException("Android NDK not found.") + } + + private fun findAndroidSdkPath(): File { + return properties.getProperty("sdk.dir")?.let { File(it) }?.takeIf { it.exists() } + ?: throw GradleException("Android SDK not found.") + } + + private fun String.exec(pwd: File = File(".")) { + val process = with (ProcessBuilder()) { + if ( Os.isFamily(Os.FAMILY_WINDOWS) ) + command("cmd.exe", "/c", this@exec) + else + command("bash", "-c", this@exec) + + environment().putAll(environment) + directory(pwd) + + redirectErrorStream(true) + + start() + } + + process.inputStream.copyTo(System.out) + println() + + if ( process.waitFor() != 0 ) + throw GradleException("Run command $this failure") + } + + private fun String.exe(): String { + return if ( Os.isFamily(Os.FAMILY_WINDOWS) ) + "$this.exe" + else + this + } +} \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index 1740143d73..f3a71e37c3 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -42,20 +42,6 @@ dependencies { } afterEvaluate { - def compileArch = ["arm64-v8a", "x86_64"] - def options = new GolangBuildOptions(file("src/main/golang"), file("$buildDir/intermediates/native_output"), "23") - - def ts = compileArch.stream().map { abi -> - tasks.register("golangBuildFor" + abi.replace("-", "").replace("_", ""), GolangBuildTask.class) { - setOptions options, abi - } - }.toArray() - - for ( task in tasks ) { - if ( task.name.contains("JniLib") ) - task.dependsOn(ts) - } - def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) { onlyIf { System.currentTimeMillis() - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > 24 * 3600 * 1000L @@ -89,6 +75,8 @@ afterEvaluate { if ( task.name.contains("generate") && task.name.contains("Assets") ) task.dependsOn(es) } + + tasks.register("golangBind", GolangBindTask.class) } repositories { diff --git a/core/src/main/golang/bridge/callback.go b/core/src/main/golang/bridge/callback.go new file mode 100644 index 0000000000..ae4773b1bb --- /dev/null +++ b/core/src/main/golang/bridge/callback.go @@ -0,0 +1,12 @@ +package bridge + +type Callback interface { +} + +var callback Callback = defaultCallback{} + +func SetCallback(cb Callback) { + callback = cb +} + +type defaultCallback struct{} diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go new file mode 100644 index 0000000000..1e12cc66c6 --- /dev/null +++ b/core/src/main/golang/bridge/profiles.go @@ -0,0 +1,11 @@ +package bridge + +import "github.com/kr328/cfa/profile" + +func LoadProfileFile(path string) { + profile.LoadFromFile(path) +} + +func LoadProfileDefault() { + profile.LoadDefault() +} diff --git a/core/src/main/golang/go.mod b/core/src/main/golang/go.mod index 236db9dd0b..f0053a4aa5 100644 --- a/core/src/main/golang/go.mod +++ b/core/src/main/golang/go.mod @@ -5,6 +5,7 @@ go 1.13 require ( github.com/Dreamacro/clash v0.0.0 // local github.com/google/go-cmp v0.3.1 // indirect + golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d // indirect golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 ) diff --git a/core/src/main/golang/go.sum b/core/src/main/golang/go.sum index cb075b6be4..9676285ec3 100644 --- a/core/src/main/golang/go.sum +++ b/core/src/main/golang/go.sum @@ -1,3 +1,5 @@ +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802 h1:1BDTz0u9nC3//pOCMdNH+CiXJVYJh5UQNCOBG7jbELc= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Dreamacro/clash v0.16.0 h1:ZvV9apOrDHC0s5ff4YCHTRANd/XyOuLWvpyLWOkwS6U= github.com/Dreamacro/clash v0.16.0/go.mod h1:4ZBtABBmIGqISniBtu9fpYORrE5mKqIP8SMCTrNNVNY= github.com/Dreamacro/go-shadowsocks2 v0.1.3 h1:1ffY/q4e3o+MnztYgIq1iZiX1BWoWQ6D3AIO1kkb8bc= @@ -55,6 +57,7 @@ github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMB github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= @@ -62,11 +65,23 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734 h1:p/H982KKEjUnLJkM3tt/LemDnOc1GiZL5FCVlORJ5zo= golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq69pTHfNouLtWZG7j9rPN8= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= +golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b h1:+qEpEAPhDZ1o0x3tHzZTQDArnOixOzGD9HUJfcg0mb4= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d h1:LlA9R5JFi974qK4gm9FRK1+qSkduxnQKcrimdzcidyc= +golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d/go.mod h1:p895TfNkDgPEmEQrNiOtIl3j98d/tGU95djDj7NfyjQ= +golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -89,12 +104,18 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU= golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190909214602-067311248421 h1:NmmWqJbt02YJHmp4A4gBXvsXXIzzixjzE1y6PKUyIjk= +golang.org/x/tools v0.0.0-20190909214602-067311248421/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/eapache/channels.v1 v1.1.0 h1:5bGAyKKvyCTWjSj7mhefG6Lc68VyN4MH1v8/7OoeeB4= gopkg.in/eapache/channels.v1 v1.1.0/go.mod h1:BHIBujSvu9yMTrTYbTCjDD43gUhtmaOtTWDe7sTv1js= diff --git a/core/src/main/golang/main.go b/core/src/main/golang/main.go index 68c3f662db..06ab7d0f9a 100644 --- a/core/src/main/golang/main.go +++ b/core/src/main/golang/main.go @@ -1,45 +1 @@ package main - -import ( - "fmt" - "os" - "os/signal" - "syscall" - - "github.com/Dreamacro/clash/constant" - - "github.com/kr328/cfa/profile" - "github.com/kr328/cfa/server" -) - -func main() { - if len(os.Args) != 2 { - fmt.Println("Invalid argument") - return - } - - // Redirect stderr to stdout - os.Stderr = os.Stdout - - if cwd, err := os.Getwd(); err == nil { - constant.SetHomeDir(cwd) - } else { - return - } - - if err := server.Start(os.Args[1]); err != nil { - fmt.Println("[CONTROLLER] ERROR={" + err.Error() + "}") - return - } - - fmt.Println("[PID]", os.Getpid()) - fmt.Println("[CONTROLLER] STARTED") - - profile.LoadDefault() - - sigCh := make(chan os.Signal, 1) - signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) - <-sigCh - - return -} diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go index 2a1a78012d..3712023eda 100644 --- a/core/src/main/golang/tun/tun.go +++ b/core/src/main/golang/tun/tun.go @@ -2,9 +2,9 @@ package tun import ( "fmt" + "net" "strconv" - "github.com/Dreamacro/clash/dns" T "github.com/Dreamacro/clash/proxy/tun" ) @@ -13,6 +13,8 @@ type handler struct { } const dnsServerAddress = "172.19.0.2:53" +const gatewayAddress = "172.19.0.1/30" +const fakeInterface = "172.19.0.2" var ( dnsHijacking bool = false @@ -23,13 +25,19 @@ var ( func StartTunProxy(fd, mtu int) error { StopTunProxy() - adapter, err := T.NewTunProxy("fd://" + strconv.Itoa(fd) + "?mtu=" + strconv.Itoa(mtu)) + ip, network, _ := net.ParseCIDR(gatewayAddress) + + network.IP = ip.To4() + + fakeInterface := net.ParseIP(fakeInterface).To4() + + adapter, err := T.NewTunProxy("fd://"+strconv.Itoa(fd)+"?mtu="+strconv.Itoa(mtu), *network, fakeInterface) if err != nil { return err } instance = &handler{ - tunAdapter: &adapter, + tunAdapter: adapter, } ResetDnsRedirect() @@ -52,12 +60,11 @@ func ResetDnsRedirect() { return } - if dnsHijacking { - (*instance.tunAdapter).ReCreateDNSServer(dns.DefaultResolver, "0.0.0.0:53") - } else { - (*instance.tunAdapter).ReCreateDNSServer(dns.DefaultResolver, dnsServerAddress) - } - + // if dnsHijacking { + // (*instance.tunAdapter).ReCreateDNSServer(dns.DefaultResolver, "0.0.0.0:53") + // } else { + // (*instance.tunAdapter).ReCreateDNSServer(dns.DefaultResolver, dnsServerAddress) + // } } func SetDnsHijacking(enabled bool) { 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 2c28946b2b..4bfc754947 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 @@ -15,7 +15,7 @@ class TunService : VpnService(), IClashEventObserver { companion object { // from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/bg/VpnService.kt private const val VPN_MTU = 1500 - private const val PRIVATE_VLAN_DNS = "172.19.0.2" // sync with tun/tun.go/dnsServerAddress + private const val PRIVATE_VLAN_DNS = "114.114.114.114" // sync with tun/tun.go/dnsServerAddress private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1" private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1" } From dde7f77c297c808c0d31a4d09114078ef97eae68 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 14 Dec 2019 15:24:57 +0800 Subject: [PATCH 040/358] add golang output to source sets --- buildSrc/src/main/java/GolangBindTask.kt | 72 +++++++++++++++++++----- core/build.gradle | 10 +++- 2 files changed, 65 insertions(+), 17 deletions(-) diff --git a/buildSrc/src/main/java/GolangBindTask.kt b/buildSrc/src/main/java/GolangBindTask.kt index 3b7ba2f94a..ff8a236330 100644 --- a/buildSrc/src/main/java/GolangBindTask.kt +++ b/buildSrc/src/main/java/GolangBindTask.kt @@ -3,10 +3,11 @@ import org.gradle.api.DefaultTask import org.gradle.api.GradleException import org.gradle.api.tasks.TaskAction import java.io.File +import java.io.FileOutputStream import java.io.FileReader import java.io.FileWriter import java.util.* -import kotlin.concurrent.thread +import java.util.zip.ZipFile open class GolangBindTask : DefaultTask() { companion object { @@ -30,14 +31,14 @@ open class GolangBindTask : DefaultTask() { """.trimIndent() } - val javaOutput: File - get() { - return project.buildDir.resolve("intermediates/go_output/generate_java") - } - val nativeOutput: File - get() { - return project.buildDir.resolve("intermediates/go_output/native_library") - } + private val javaOutput: File + get() { + return project.buildDir.resolve("intermediates/go_output/generate_java") + } + private val nativeOutput: File + get() { + return project.buildDir.resolve("intermediates/go_output/native_library") + } private val goBuildPath: File get() { return project.buildDir.resolve("intermediates/go_build") @@ -62,7 +63,14 @@ open class GolangBindTask : DefaultTask() { private val environment = mutableMapOf() init { - + onlyIf { + val lastModify = sourcePath.walk() + .filter { it.extension == "go" || it.extension == "mod" } + .map { it.lastModified() } + .max() ?: 0L + + return@onlyIf goBuildPath.resolve("bridge.aar").lastModified() < lastModify + } } @TaskAction @@ -71,7 +79,7 @@ open class GolangBindTask : DefaultTask() { environment.put("ANDROID_HOME", findAndroidSdkPath().absolutePath) environment.put("ANDROID_NDK_HOME", findAndroidNdkPath().absolutePath) - if ( Os.isFamily(Os.FAMILY_WINDOWS) ) + if (Os.isFamily(Os.FAMILY_WINDOWS)) environment.put("Path", System.getenv("Path") + ";" + goPath.resolve("bin")) else environment.put("PATH", System.getenv("PATH") + ":" + goPath.resolve("bin")) @@ -95,6 +103,40 @@ open class GolangBindTask : DefaultTask() { "gomobile init".exec(goBuildPath) "gomobile bind -target=android github.com/kr328/cfa/bridge".exec(goBuildPath) + + with(ZipFile(goBuildPath.resolve("bridge.aar"))) { + stream() + .filter { !it.isDirectory } + .filter { it.name.startsWith("jni") } + .forEach { + val target = nativeOutput.resolve(it.name.removePrefix("jni/")) + + target.parentFile.mkdirs() + + FileOutputStream(target).use { output -> + getInputStream(it).use { input -> + input.copyTo(output) + } + } + } + } + + with(ZipFile(goBuildPath.resolve("bridge-sources.jar"))) { + stream() + .filter { !it.isDirectory } + .filter { it.name.endsWith(".java") } + .forEach { + val target = javaOutput.resolve(it.name) + + target.parentFile.mkdirs() + + FileOutputStream(target).use { output -> + getInputStream(it).use { input -> + input.copyTo(output) + } + } + } + } } private fun findAndroidNdkPath(): File { @@ -108,8 +150,8 @@ open class GolangBindTask : DefaultTask() { } private fun String.exec(pwd: File = File(".")) { - val process = with (ProcessBuilder()) { - if ( Os.isFamily(Os.FAMILY_WINDOWS) ) + val process = with(ProcessBuilder()) { + if (Os.isFamily(Os.FAMILY_WINDOWS)) command("cmd.exe", "/c", this@exec) else command("bash", "-c", this@exec) @@ -125,12 +167,12 @@ open class GolangBindTask : DefaultTask() { process.inputStream.copyTo(System.out) println() - if ( process.waitFor() != 0 ) + if (process.waitFor() != 0) throw GradleException("Run command $this failure") } private fun String.exe(): String { - return if ( Os.isFamily(Os.FAMILY_WINDOWS) ) + return if (Os.isFamily(Os.FAMILY_WINDOWS)) "$this.exe" else this diff --git a/core/build.gradle b/core/build.gradle index f3a71e37c3..909138d59f 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -26,7 +26,8 @@ android { sourceSets { main { assets.srcDirs += ["$buildDir/intermediates/dynamic_assets"] - jniLibs.srcDirs += ["$buildDir/intermediates/native_output"] + jniLibs.srcDirs += ["$buildDir/intermediates/go_output/native_library"] + java.srcDirs += ["$buildDir/intermediates/go_output/generate_java"] } } compileOptions { @@ -76,7 +77,12 @@ afterEvaluate { task.dependsOn(es) } - tasks.register("golangBind", GolangBindTask.class) + def gs = tasks.register("golangBind", GolangBindTask.class) + + for ( task in tasks ) { + if ( task.name.contains("compile") && task.name.contains("Sources") ) + task.dependsOn(gs) + } } repositories { From c3cd2351a55001a3545d21ec460a376f7ab3d11b Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 15 Dec 2019 14:06:36 +0800 Subject: [PATCH 041/358] redirect tun initial --- buildSrc/src/main/java/GolangBindTask.kt | 5 +- core/build.gradle | 5 +- core/src/main/golang/bridge/callback.go | 11 - core/src/main/golang/bridge/init.go | 7 + core/src/main/golang/bridge/profiles.go | 4 +- core/src/main/golang/bridge/tun.go | 17 ++ core/src/main/golang/clash | 2 +- core/src/main/golang/go.sum | 8 + core/src/main/golang/main.go | 2 + core/src/main/golang/profile/load.go | 71 ++---- core/src/main/golang/server/event.go | 177 -------------- core/src/main/golang/server/http.go | 11 - core/src/main/golang/server/ping.go | 14 -- core/src/main/golang/server/profile.go | 60 ----- core/src/main/golang/server/proxies.go | 220 ------------------ core/src/main/golang/server/server.go | 84 ------- core/src/main/golang/server/tun.go | 63 ----- core/src/main/golang/server/utils.go | 41 ---- core/src/main/golang/tun/tun.go | 85 ++++--- gradle.properties | 4 +- .../kr328/clash/service/ClashService.kt | 105 +++------ .../github/kr328/clash/service/TunService.kt | 17 +- service/src/main/res/values/arrays.xml | 1 - 23 files changed, 153 insertions(+), 861 deletions(-) create mode 100644 core/src/main/golang/bridge/init.go create mode 100644 core/src/main/golang/bridge/tun.go delete mode 100644 core/src/main/golang/server/event.go delete mode 100644 core/src/main/golang/server/http.go delete mode 100644 core/src/main/golang/server/ping.go delete mode 100644 core/src/main/golang/server/profile.go delete mode 100644 core/src/main/golang/server/proxies.go delete mode 100644 core/src/main/golang/server/server.go delete mode 100644 core/src/main/golang/server/tun.go delete mode 100644 core/src/main/golang/server/utils.go diff --git a/buildSrc/src/main/java/GolangBindTask.kt b/buildSrc/src/main/java/GolangBindTask.kt index ff8a236330..b6654b6a2c 100644 --- a/buildSrc/src/main/java/GolangBindTask.kt +++ b/buildSrc/src/main/java/GolangBindTask.kt @@ -104,6 +104,9 @@ open class GolangBindTask : DefaultTask() { "gomobile init".exec(goBuildPath) "gomobile bind -target=android github.com/kr328/cfa/bridge".exec(goBuildPath) + nativeOutput.deleteRecursively() + javaOutput.deleteRecursively() + with(ZipFile(goBuildPath.resolve("bridge.aar"))) { stream() .filter { !it.isDirectory } @@ -165,7 +168,7 @@ open class GolangBindTask : DefaultTask() { } process.inputStream.copyTo(System.out) - println() + System.out.flush() if (process.waitFor() != 0) throw GradleException("Run command $this failure") diff --git a/core/build.gradle b/core/build.gradle index 909138d59f..1c754e1427 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -79,10 +79,7 @@ afterEvaluate { def gs = tasks.register("golangBind", GolangBindTask.class) - for ( task in tasks ) { - if ( task.name.contains("compile") && task.name.contains("Sources") ) - task.dependsOn(gs) - } + preBuild.dependsOn(gs) } repositories { diff --git a/core/src/main/golang/bridge/callback.go b/core/src/main/golang/bridge/callback.go index ae4773b1bb..ac94c40bdb 100644 --- a/core/src/main/golang/bridge/callback.go +++ b/core/src/main/golang/bridge/callback.go @@ -1,12 +1 @@ package bridge - -type Callback interface { -} - -var callback Callback = defaultCallback{} - -func SetCallback(cb Callback) { - callback = cb -} - -type defaultCallback struct{} diff --git a/core/src/main/golang/bridge/init.go b/core/src/main/golang/bridge/init.go new file mode 100644 index 0000000000..f7baf7dcce --- /dev/null +++ b/core/src/main/golang/bridge/init.go @@ -0,0 +1,7 @@ +package bridge + +import "github.com/Dreamacro/clash/constant" + +func Init(home string) { + constant.SetHomeDir(home) +} diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go index 1e12cc66c6..0298b251cd 100644 --- a/core/src/main/golang/bridge/profiles.go +++ b/core/src/main/golang/bridge/profiles.go @@ -2,8 +2,8 @@ package bridge import "github.com/kr328/cfa/profile" -func LoadProfileFile(path string) { - profile.LoadFromFile(path) +func LoadProfileFile(path string) error { + return profile.LoadFromFile(path) } func LoadProfileDefault() { diff --git a/core/src/main/golang/bridge/tun.go b/core/src/main/golang/bridge/tun.go new file mode 100644 index 0000000000..4bd4865e18 --- /dev/null +++ b/core/src/main/golang/bridge/tun.go @@ -0,0 +1,17 @@ +package bridge + +import ( + "github.com/kr328/cfa/tun" +) + +type TunCallback interface { + OnNewSocket(fd int) +} + +func StartTunDevice(fd, mtu int, gateway string, dns string, callback TunCallback) error { + return tun.StartTunDevice(fd, mtu, gateway, dns, callback) +} + +func StopTunDevice() { + tun.StopTunDevice() +} diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index 7767f1554c..a18182a9ed 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit 7767f1554c6813b51e165732b85e5820b34ee9e9 +Subproject commit a18182a9ed815be22594f2729533391c9ed5ec71 diff --git a/core/src/main/golang/go.sum b/core/src/main/golang/go.sum index 9676285ec3..36689209c6 100644 --- a/core/src/main/golang/go.sum +++ b/core/src/main/golang/go.sum @@ -6,6 +6,8 @@ github.com/Dreamacro/go-shadowsocks2 v0.1.3 h1:1ffY/q4e3o+MnztYgIq1iZiX1BWoWQ6D3 github.com/Dreamacro/go-shadowsocks2 v0.1.3/go.mod h1:0x17IhQ+mlY6q/ffKRpzaE7u4aHMxxnitTRSrV5G6TU= github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68 h1:UBDLpj1IGVkUcUBuZWE6DmZApPTZcnmcV6AfyDN/yhg= github.com/Dreamacro/go-shadowsocks2 v0.1.5-0.20191012162057-46254afc8b68/go.mod h1:Y8obOtHDOqxMGHjPglfCiXZBKExOA9VL6I6sJagOwYM= +github.com/Dreamacro/go-shadowsocks2 v0.1.5 h1:BizWSjmwzAyQoslz6YhJYMiAGT99j9cnm9zlxVr+kyI= +github.com/Dreamacro/go-shadowsocks2 v0.1.5/go.mod h1:LSXCjyHesPY3pLjhwff1mQX72ItcBT/N2xNC685cYeU= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -42,6 +44,8 @@ github.com/miekg/dns v1.1.9 h1:OIdC9wT96RzuZMf2PfKRhFgsStHUUBZLM/lo1LqiM9E= github.com/miekg/dns v1.1.9/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc= github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.24 h1:6G8Eop/HM8hpagajbn0rFQvAKZWiiCa8P6N2I07+wwI= +github.com/miekg/dns v1.1.24/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU= github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE= github.com/oschwald/geoip2-golang v1.3.0 h1:D+Hsdos1NARPbzZ2aInUHZL+dApIzo8E0ErJVsWcku8= @@ -71,6 +75,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550 h1:ObdrDkeb4kJdCP557AjRjq golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0nWkCNaMEMUwAv13Y92hd8= golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -90,6 +96,8 @@ golang.org/x/net v0.0.0-20191011234655-491137f69257 h1:ry8e2D+cwaV6hk7lb3aRTjjZo golang.org/x/net v0.0.0-20191011234655-491137f69257/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5PW3vNmnN64U2ZW759Lk= golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663 h1:Dd5RoEW+yQi+9DMybroBctIdyiwuNT7sJFMC27/6KxI= +golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= diff --git a/core/src/main/golang/main.go b/core/src/main/golang/main.go index 06ab7d0f9a..38dd16da61 100644 --- a/core/src/main/golang/main.go +++ b/core/src/main/golang/main.go @@ -1 +1,3 @@ package main + +func main() {} diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index 9f1e7d1a78..948f95d815 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -3,65 +3,36 @@ package profile import ( "net" - adapters "github.com/Dreamacro/clash/adapters/outbound" - "github.com/Dreamacro/clash/component/auth" - trie "github.com/Dreamacro/clash/component/domain-trie" "github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/config" - "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/hub/executor" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" - "github.com/kr328/cfa/tun" ) +const defaultConfig = ` +mode: Direct +Proxy: +- name: "ss1" + type: ss + server: server + port: 443 + cipher: chacha20-ietf-poly1305 + password: "password" + # udp: true + +Proxy Group: +- name: "select" + type: select + proxies: [DIRECT] + +Rule: +- 'MATCH,DIRECT' +` + // LoadDefault - load default configure func LoadDefault() { - defaultC := &config.Config{ - General: &config.General{ - Port: 0, - SocksPort: 0, - RedirPort: 0, - Authentication: []string{}, - AllowLan: false, - BindAddress: "*", - Mode: tunnel.Direct, - LogLevel: log.SILENT, - ExternalController: "", - ExternalUI: "", - Secret: "", - }, - DNS: &config.DNS{ - Enable: false, - IPv6: false, - NameServer: []dns.NameServer{}, - Fallback: []dns.NameServer{}, - FallbackFilter: config.FallbackFilter{ - GeoIP: false, - IPCIDR: []*net.IPNet{}, - }, - Listen: "", - EnhancedMode: dns.NORMAL, - FakeIPRange: nil, - }, - Experimental: &config.Experimental{ - IgnoreResolveFail: false, - }, - Hosts: trie.New(), - Rules: []constant.Rule{}, - Users: []auth.AuthUser{}, - Proxies: map[string]constant.Proxy{}, - } - - reject := adapters.NewProxy(adapters.NewReject()) - direct := adapters.NewProxy(adapters.NewDirect()) - global, _ := adapters.NewSelector("GLOBAL", []constant.Proxy{direct}) - - defaultC.Proxies["DIRECT"] = direct - defaultC.Proxies["REJECT"] = reject - defaultC.Proxies["GLOBAL"] = adapters.NewProxy(global) + defaultC, _ := config.Parse([]byte(defaultConfig)) tun.ResetDnsRedirect() diff --git a/core/src/main/golang/server/event.go b/core/src/main/golang/server/event.go deleted file mode 100644 index 48a95c95e3..0000000000 --- a/core/src/main/golang/server/event.go +++ /dev/null @@ -1,177 +0,0 @@ -package server - -import ( - "bytes" - "encoding/json" - "net" - "time" - - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" -) - -func handlePullTrafficEvent(client *net.UnixConn) { - trafficExit := make(chan int) - ticker := time.NewTicker(time.Second) - - traffic := tunnel.DefaultManager - buf := &bytes.Buffer{} - - defer ticker.Stop() - defer client.Close() - - go func() { - for { - var buf [4]byte - - client.Read(buf[:]) - - close(trafficExit) - return - } - }() - - for { - select { - case <-ticker.C: - buf.Reset() - - var Packet struct { - Up int64 `json:"up"` - Down int64 `json:"down"` - } - - up, down := traffic.Now() - - Packet.Up = up - Packet.Down = down - - if json.NewEncoder(buf).Encode(&Packet) != nil { - return - } - - if writeCommandPacket(client, buf.Bytes()) != nil { - return - } - case <-trafficExit: - return - } - } -} - -func handlePullBandwidthEvent(client *net.UnixConn) { - bandWidthExit := make(chan int) - ticker := time.NewTicker(time.Second) - - mgr := tunnel.DefaultManager - buf := &bytes.Buffer{} - - defer ticker.Stop() - defer client.Close() - - go func() { - for { - var buf [4]byte - - client.Read(buf[:]) - - close(bandWidthExit) - return - } - }() - - tick := func() error { - buf.Reset() - - var Packet struct { - Total int64 `json:"total"` - } - - sp := mgr.Snapshot() - - Packet.Total = sp.DownloadTotal + sp.UploadTotal - - json.NewEncoder(buf).Encode(&Packet) - - return writeCommandPacket(client, buf.Bytes()) - } - - tick() - - for { - select { - case <-ticker.C: - if tick() != nil { - return - } - case <-bandWidthExit: - return - } - } -} - -func handlePullLogEvent(client *net.UnixConn) { - logExit := make(chan int) - - subseribe := log.Subscribe() - buf := &bytes.Buffer{} - - defer log.UnSubscribe(subseribe) - defer client.Close() - - go func() { - var buf [4]byte - - for { - client.Read(buf[:]) - - logExit <- 0 - close(logExit) - return - } - }() - - for { - select { - case elm := <-subseribe: - msg := elm.(*log.Event) - - if msg.LogLevel < log.Level() { - break - } - - buf.Reset() - - var payload struct { - Level int `json:"level"` - Messgae string `json:"message"` - } - - switch msg.LogLevel { - case log.DEBUG: - payload.Level = 1 - break - case log.INFO: - payload.Level = 2 - break - case log.WARNING: - payload.Level = 3 - break - case log.ERROR: - payload.Level = 4 - } - - payload.Messgae = msg.Payload - - if err := json.NewEncoder(buf).Encode(&payload); err != nil { - return - } - - if writeCommandPacket(client, buf.Bytes()) != nil { - return - } - case <-logExit: - return - } - } -} diff --git a/core/src/main/golang/server/http.go b/core/src/main/golang/server/http.go deleted file mode 100644 index fbe7f9c060..0000000000 --- a/core/src/main/golang/server/http.go +++ /dev/null @@ -1,11 +0,0 @@ -package server - -import "github.com/Dreamacro/clash/proxy/http" - -var httpListener *http.HttpListener - -func startRandomHttpPort() { - listener, _ := http.NewHttpProxy("127.0.0.1:0") - - httpListener = listener -} diff --git a/core/src/main/golang/server/ping.go b/core/src/main/golang/server/ping.go deleted file mode 100644 index 37e044ab4b..0000000000 --- a/core/src/main/golang/server/ping.go +++ /dev/null @@ -1,14 +0,0 @@ -package server - -import ( - "net" - "encoding/binary" -) - -const ( - pingReply = 233 -) - -func handlePing(client *net.UnixConn) { - binary.Write(client, binary.BigEndian, uint32(pingReply)) -} \ No newline at end of file diff --git a/core/src/main/golang/server/profile.go b/core/src/main/golang/server/profile.go deleted file mode 100644 index 7ec6cf6ad4..0000000000 --- a/core/src/main/golang/server/profile.go +++ /dev/null @@ -1,60 +0,0 @@ -package server - -import ( - "encoding/json" - "net" - - "github.com/Dreamacro/clash/log" - "github.com/kr328/cfa/profile" -) - -func handleProfileDefault(client *net.UnixConn) { - profile.LoadDefault() - - log.Infoln("Profile default loaded") -} - -func handleProfileReload(client *net.UnixConn) { - packet, err := readCommandPacket(client) - if err != nil { - log.Errorln("Read profile payload failure, %s", err.Error()) - return - } - - var payload struct { - Path string `json:"path"` - Selected map[string]string `json:"selected"` - } - - var response struct { - Error string `json:"error"` - InvalidSelected []string `json:"invalidSelected"` - } - - response.InvalidSelected = make([]string, 0) - - defer func() { - buf, _ := json.Marshal(&response) - - writeCommandPacket(client, buf) - }() - - err = json.Unmarshal(packet, &payload) - if err != nil { - log.Errorln("Parse profile payload failure, %s", err.Error()) - return - } - - err = profile.LoadFromFile(payload.Path) - - if err != nil { - response.Error = err.Error() - return - } - - for k, v := range payload.Selected { - setProxySelect(k, v) - } - - log.Infoln("Profile " + payload.Path + " loaded") -} diff --git a/core/src/main/golang/server/proxies.go b/core/src/main/golang/server/proxies.go deleted file mode 100644 index 1497290c6f..0000000000 --- a/core/src/main/golang/server/proxies.go +++ /dev/null @@ -1,220 +0,0 @@ -package server - -import ( - "context" - "encoding/json" - "errors" - "net" - "strconv" - "time" - - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/proxy" - "github.com/Dreamacro/clash/tunnel" - - A "github.com/Dreamacro/clash/adapters/outbound" -) - -const ( - modeDirect = 1 - modeGlobal = 2 - modeRule = 3 -) - -func handleQueryGeneral(client *net.UnixConn) { - var payload struct { - Ports struct { - Http int `json:"http"` - Socks int `json:"socks"` - Redirect int `json:"redirect"` - RandomHttpPort int `json:"randomHttp"` - } `json:"ports"` - Mode int `json:"mode"` - } - - mode := tunnel.Instance().Mode() - ports := proxy.GetPorts() - - payload.Ports.Http = ports.Port - payload.Ports.Socks = ports.SocksPort - payload.Ports.Redirect = ports.RedirPort - - if httpListener != nil { - addr := httpListener.Addr().String() - - _, port, _ := net.SplitHostPort(addr) - p, _ := strconv.Atoi(port) - - payload.Ports.RandomHttpPort = p - } - - switch mode { - case tunnel.Direct: - payload.Mode = modeDirect - break - case tunnel.Global: - payload.Mode = modeGlobal - break - case tunnel.Rule: - payload.Mode = modeRule - break - } - - buf, _ := json.Marshal(&payload) - - writeCommandPacket(client, buf) -} - -func handleQueryProxies(client *net.UnixConn) { - proxies := tunnel.Instance().Proxies() - - var root struct { - Mode string `json:"mode"` - Proxies map[string]interface{} `json:"proxies"` - } - - root.Mode = tunnel.Instance().Mode().String() - root.Proxies = make(map[string]interface{}) - - for k, p := range proxies { - inner, err := p.MarshalJSON() - - if err != nil { - log.Errorln("MarshalJSON failure %s", err.Error()) - continue - } - - mapping := map[string]interface{}{} - json.Unmarshal(inner, &mapping) - root.Proxies[k] = mapping - } - - data, err := json.Marshal(&root) - if err != nil { - log.Errorln("MarshJSON failure %s", err.Error()) - } - - writeCommandPacket(client, data) -} - -func handleSetProxy(client *net.UnixConn) { - var Request struct { - Key string `json:"key"` - Value string `json:"value"` - } - - var Response struct { - Error string `json:"error"` - } - - defer func() { - buf, _ := json.Marshal(&Response) - - writeCommandPacket(client, buf) - }() - - buf, err := readCommandPacket(client) - if err != nil { - return - } - - json.Unmarshal(buf, &Request) - - if err := setProxySelect(Request.Key, Request.Value); err != nil { - Response.Error = err.Error() - } -} - -func handleUrlTest(client *net.UnixConn) { - var request struct { - URL string `json:"url"` - Timeout int `json:"timeout"` - Proxies []string `json:"proxies"` - } - - request.Proxies = make([]string, 0) - - buf, err := readCommandPacket(client) - if err != nil { - return - } - - if json.Unmarshal(buf, &request) != nil { - return - } - - type Response struct { - Name string `json:"name"` - Delay int `json:"delay"` - } - - proxies := tunnel.Instance().Proxies() - channel := make(chan *Response, len(request.Proxies)) - - for _, p := range request.Proxies { - go func(p string) { - proxy := proxies[p] - if proxy == nil { - channel <- nil - return - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Duration(request.Timeout)*time.Millisecond) - - defer cancel() - - delay, err := proxies[p].URLTest(ctx, request.URL) - if err != nil { - channel <- nil - return - } - - channel <- &Response{ - Name: p, - Delay: int(delay), - } - }(p) - } - - for range request.Proxies { - response := <-channel - - if response != nil { - buf, _ := json.Marshal(&response) - - writeCommandPacket(client, buf) - } - } - - writeCommandPacket(client, nil) - - log.Infoln("URLTest exited") -} - -func setProxySelect(name, selected string) error { - proxies := tunnel.Instance().Proxies() - - p := proxies[name] - - if p == nil { - return errors.New("Unknown proxy " + name) - } - - proxy, ok := p.(*A.Proxy) - if !ok { - return errors.New("Invalid proxy " + name) - } - - selector, ok := proxy.ProxyAdapter.(*A.Selector) - if !ok { - return errors.New("Not selector") - } - - log.Infoln("Set selector " + name + " -> " + selected) - - if err := selector.Set(selected); err != nil { - return err - } - - return nil -} diff --git a/core/src/main/golang/server/server.go b/core/src/main/golang/server/server.go deleted file mode 100644 index 4214e3ae61..0000000000 --- a/core/src/main/golang/server/server.go +++ /dev/null @@ -1,84 +0,0 @@ -package server - -import ( - "encoding/binary" - "net" - - "github.com/Dreamacro/clash/log" -) - -const ( - commandPing uint32 = 0 - commandTunStart uint32 = 1 - commandTunStop uint32 = 2 - commandProfileDefault uint32 = 3 - commandProfileReload uint32 = 4 - commandQueryProxies uint32 = 5 - commandPullTraffic uint32 = 6 - commandPullLog uint32 = 7 - commandPullBandwidth uint32 = 8 - commandSetProxy uint32 = 9 - commandQueryGeneral uint32 = 10 - commandUrlTest uint32 = 11 -) - -var handlers map[uint32]func(*net.UnixConn) = make(map[uint32]func(*net.UnixConn)) - -func init() { - handlers[commandPing] = handlePing // ping.go - handlers[commandTunStart] = handleTunStart // tun.go - handlers[commandTunStop] = handleTunStop // tun.go - handlers[commandProfileDefault] = handleProfileDefault // profile.go - handlers[commandProfileReload] = handleProfileReload // profile.go - handlers[commandQueryProxies] = handleQueryProxies // proxies.go - handlers[commandPullTraffic] = handlePullTrafficEvent // event.go - handlers[commandPullLog] = handlePullLogEvent // event.go - handlers[commandPullBandwidth] = handlePullBandwidthEvent // event.go - handlers[commandSetProxy] = handleSetProxy // proxies.go - handlers[commandQueryGeneral] = handleQueryGeneral // proxies.go - handlers[commandUrlTest] = handleUrlTest // proxies.go -} - -// Start local control server -func Start(path string) error { - startRandomHttpPort() - - address, _ := net.ResolveUnixAddr("unix", path) - - listener, err := net.ListenUnix("unix", address) - if err != nil { - return err - } - - go func() { - for { - client, err := listener.AcceptUnix() - if err != nil { - log.Fatalln("Unix socket accept failure: %s", err.Error()) - listener.Close() - client.Close() - return - } - - go handleConnection(client) - } - }() - - return nil -} - -func handleConnection(client *net.UnixConn) { - var command uint32 - - if err := binary.Read(client, binary.BigEndian, &command); err != nil { - log.Errorln("Unix read command failure %s", err.Error()) - } - - handler := handlers[command] - if handler == nil { - log.Errorln("Invalid command failure %d", command) - client.Close() - } else { - handler(client) - } -} diff --git a/core/src/main/golang/server/tun.go b/core/src/main/golang/server/tun.go deleted file mode 100644 index 10bd38da0f..0000000000 --- a/core/src/main/golang/server/tun.go +++ /dev/null @@ -1,63 +0,0 @@ -package server - -import ( - "encoding/binary" - "net" - - "github.com/Dreamacro/clash/log" - "github.com/kr328/cfa/tun" - "golang.org/x/sys/unix" -) - -const ( - tunCommandEnd = 0x243 -) - -func handleTunStart(client *net.UnixConn) { - buffer := make([]byte, unix.CmsgLen(4*1)) - - _, noob, _, _, err := client.ReadMsgUnix(nil, buffer) - if err != nil { - log.Errorln("Read tun socket failure, %s", err.Error()) - return - } - - msg, err := unix.ParseSocketControlMessage(buffer[:noob]) - if err != nil || len(msg) != 1 { - log.Errorln("Parse tun socket failure, %s", err.Error()) - return - } - - fds, err := unix.ParseUnixRights(&msg[0]) - if err != nil { - log.Errorln("Parse tun socket failure, %s", err.Error()) - return - } - - var mtu uint32 - var dns uint32 - var end uint32 - - binary.Read(client, binary.BigEndian, &mtu) - binary.Read(client, binary.BigEndian, &dns) - binary.Read(client, binary.BigEndian, &end) - - if end != tunCommandEnd { - log.Errorln("Invalid tun command end") - return - } - - if dns != 0 { - tun.SetDnsHijacking(true) - } else { - tun.SetDnsHijacking(false) - } - - if err := tun.StartTunProxy(fds[0], int(mtu)); err != nil { - log.Errorln("Open tun device failure" + err.Error()) - } -} - -func handleTunStop(client *net.UnixConn) { - tun.StopTunProxy() -} diff --git a/core/src/main/golang/server/utils.go b/core/src/main/golang/server/utils.go deleted file mode 100644 index 8c8146296a..0000000000 --- a/core/src/main/golang/server/utils.go +++ /dev/null @@ -1,41 +0,0 @@ -package server - -import ( - "encoding/binary" - "errors" - "io" - "net" -) - -func readCommandPacket(client *net.UnixConn) ([]byte, error) { - var packageSize uint32 - - if err := binary.Read(client, binary.BigEndian, &packageSize); err != nil { - return nil, err - } - - buffer := make([]byte, packageSize) - - if readSize, err := io.ReadFull(client, buffer); err != nil || uint32(readSize) != packageSize { - return nil, err - } - - return buffer, nil -} - -func writeCommandPacket(client *net.UnixConn, buffer []byte) error { - if err := binary.Write(client, binary.BigEndian, uint32(len(buffer))); err != nil { - return err - } - - n, err := client.Write(buffer) - if err != nil { - return err - } - - if n != len(buffer) { - return errors.New("Write invalid data size") - } - - return nil -} diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go index 3712023eda..414fb1f54e 100644 --- a/core/src/main/golang/tun/tun.go +++ b/core/src/main/golang/tun/tun.go @@ -1,72 +1,83 @@ package tun import ( - "fmt" "net" "strconv" + "syscall" - T "github.com/Dreamacro/clash/proxy/tun" + "github.com/Dreamacro/clash/dns" + "github.com/Dreamacro/clash/global" + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/proxy/tun" ) -type handler struct { - tunAdapter *T.TunAdapter +type Callback interface { + OnNewSocket(fd int) } -const dnsServerAddress = "172.19.0.2:53" -const gatewayAddress = "172.19.0.1/30" -const fakeInterface = "172.19.0.2" +var tunInstance *tun.TUN +var dnsGateway net.IP -var ( - dnsHijacking bool = false - instance *handler -) +func StartTunDevice(fd, mtu int, gateway string, dns string, callback Callback) error { + if tunInstance != nil { + return nil + } -// StartTunProxy - start -func StartTunProxy(fd, mtu int) error { - StopTunProxy() + ip, n, err := net.ParseCIDR(gateway) + if err != nil { + return err + } - ip, network, _ := net.ParseCIDR(gatewayAddress) + n.IP = ip.To4() - network.IP = ip.To4() + global.DefaultDialer.Control = func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + callback.OnNewSocket(int(fd)) + }) + } + global.DefaultListenConfig.Control = func(network, address string, c syscall.RawConn) error { + return c.Control(func(fd uintptr) { + callback.OnNewSocket(int(fd)) + }) + } - fakeInterface := net.ParseIP(fakeInterface).To4() + t, err := tun.NewTunProxy("fd://"+strconv.Itoa(fd)+"?mtu="+strconv.Itoa(mtu), *n) - adapter, err := T.NewTunProxy("fd://"+strconv.Itoa(fd)+"?mtu="+strconv.Itoa(mtu), *network, fakeInterface) if err != nil { + global.DefaultDialer.Control = nil + global.DefaultListenConfig.Control = nil + return err } + tunInstance = t - instance = &handler{ - tunAdapter: adapter, - } + dnsGateway = net.ParseIP(dns) ResetDnsRedirect() - fmt.Println("Android tun started") + log.Infoln("Android tun started") return nil } -// StopTunProxy - stop -func StopTunProxy() { - if instance != nil { - (*instance.tunAdapter).Close() - instance = nil +func StopTunDevice() { + t := tunInstance + if t == nil { + return } + + t.Close() + + global.DefaultDialer.Control = nil + global.DefaultListenConfig.Control = nil + + tunInstance = nil } func ResetDnsRedirect() { - if instance == nil { + if tunInstance == nil { return } - // if dnsHijacking { - // (*instance.tunAdapter).ReCreateDNSServer(dns.DefaultResolver, "0.0.0.0:53") - // } else { - // (*instance.tunAdapter).ReCreateDNSServer(dns.DefaultResolver, dnsServerAddress) - // } -} - -func SetDnsHijacking(enabled bool) { - dnsHijacking = enabled + tunInstance.ReCreateDNSServer(dns.DefaultResolver, dnsGateway) } diff --git a/gradle.properties b/gradle.properties index 44e0c530ea..caa72347b8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -20,6 +20,4 @@ android.enableJetifier=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official -kapt.incremental.apt=false - -android.bundle.enableUncompressedNativeLibs=false \ No newline at end of file +kapt.incremental.apt=false \ No newline at end of file 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 969314e310..cc60f5a02a 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 @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.* +import bridge.Bridge import com.github.kr328.clash.callback.IUrlTestCallback import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.event.* @@ -26,23 +27,20 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, private val profileService = ClashProfileService(this, this) private val settingService = ClashSettingService(this) - private lateinit var clash: Clash - private lateinit var puller: ClashEventPuller + private var processStatus = ProcessEvent.STOPPED + + //private lateinit var puller: ClashEventPuller private lateinit var notification: ClashNotification private val clashService = object : IClashService.Stub() { override fun stopTunDevice() { notification.setVpn(false) - - clash.stopTunDevice() } override fun setSelectProxy(proxy: String?, selected: String?) { require(proxy != null && selected != null) try { - clash.setSelectProxy(proxy, selected) - this@ClashService.profileService.setCurrentProfileProxy(proxy, selected) } catch (e: IOException) { Log.w("Set proxy failure", e) @@ -54,59 +52,35 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } override fun queryGeneral(): GeneralPacket { - return clash.queryGeneral() + return GeneralPacket(GeneralPacket.Ports(0, 0, 0, 0), GeneralPacket.Mode.DIRECT) } override fun queryAllProxies(): ProxyPacket { - return try { - ProxyPacket.fromRawProxy(clash.queryProxies()) - } catch (e: Exception) { - this@ClashService.eventService.performErrorEvent( - ErrorEvent(ErrorEvent.Type.QUERY_PROXY_FAILURE, e.toString()) - ) - ProxyPacket("Unknown", emptyMap()) - } + return ProxyPacket("Direct", emptyMap()) } override fun startUrlTest(proxies: Array?, callback: IUrlTestCallback?) { require(proxies != null && callback != null) - - thread { - try { - clash.startUrlTest(proxies.toList()) { name, delay -> - callback.onResult(name, delay) - } - } - catch (e: Exception) { - Log.w("Url test failure", e) - } - - callback.onResult(null, -1) - // Ignore exceptions - } } override fun start() { - try { - clash.process.start() - } catch (e: Exception) { - Log.e("Start failure", e) + if ( processStatus == ProcessEvent.STARTED ) + return - this@ClashService.eventService.performErrorEvent( - ErrorEvent(ErrorEvent.Type.START_FAILURE, e.toString()) - ) - } + processStatus = ProcessEvent.STARTED + + this@ClashService.eventService.performProcessEvent(processStatus) } override fun stop() { - clash.process.stop() + processStatus = ProcessEvent.STOPPED + + this@ClashService.eventService.performProcessEvent(processStatus) } override fun startTunDevice(fd: ParcelFileDescriptor, mtu: Int, dnsHijacking: Boolean) { try { notification.setVpn(true) - - clash.startTunDevice(fd.fileDescriptor, mtu, dnsHijacking) } catch (e: Exception) { Log.e("Start tun failure", e) @@ -131,7 +105,7 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } override fun getCurrentProcessStatus(): ProcessEvent { - return clash.process.getProcessStatus() + return processStatus } } @@ -155,44 +129,22 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } override fun acquireEvent(event: Int) { - if (clash.process.getProcessStatus() == ProcessEvent.STOPPED) + if (processStatus == ProcessEvent.STOPPED) return - when (event) { - Event.EVENT_SPEED -> - puller.startSpeedPull() - Event.EVENT_LOG -> - puller.startLogPuller() - Event.EVENT_BANDWIDTH -> - puller.startBandwidthPull() - } } override fun releaseEvent(event: Int) { - if (clash.process.getProcessStatus() == ProcessEvent.STOPPED) + if (processStatus == ProcessEvent.STOPPED) return - - when (event) { - Event.EVENT_SPEED -> - puller.stopSpeedPull() - Event.EVENT_LOG -> - puller.stopLogPull() - Event.EVENT_BANDWIDTH -> - puller.stopBandwidthPull() - } } override fun onCreate() { super.onCreate() - clash = Clash( - this, - filesDir.resolve("clash"), - cacheDir.resolve("clash_controller"), - eventService::performProcessEvent - ) + Bridge.init(filesDir.resolve("clash").absolutePath) - puller = ClashEventPuller(clash, this) + //puller = ClashEventPuller(clash, this) notification = ClashNotification(this) @@ -211,7 +163,7 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) - clash.process.start() + clashService.start() return START_NOT_STICKY } @@ -221,7 +173,7 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } override fun onDestroy() { - clash.process.stop() + clashService.stop() executor.shutdown() eventService.shutdown() @@ -251,6 +203,8 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, notification.cancel() stopSelf() + + Bridge.loadProfileDefault() } } @@ -259,32 +213,27 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, private fun reloadProfile() { executor.submit { - if ( clash.process.getProcessStatus() != ProcessEvent.STARTED) + if ( processStatus != ProcessEvent.STARTED) return@submit val active = profileService.queryActiveProfile() if (active == null) { eventService.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, "No profile activated")) - clash.process.stop() + clashService.stop() return@submit } Log.i("Loading profile ${active.file}") try { - val remove = clash.loadProfile( - File(active.file), - profileService.queryProfileSelected(active.id) - ) - - profileService.removeCurrentProfileProxy(remove) + Bridge.loadProfileFile(active.file) notification.setProfile(active.name) eventService.performProfileReloadEvent(ProfileReloadEvent()) } catch (e: Exception) { - clash.process.stop() + clashService.stop() eventService.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, e.message ?: "Unknown")) Log.w("Load profile failure", e) } 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 4bfc754947..e6d4c5c81b 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 @@ -7,6 +7,8 @@ import android.content.Intent import android.content.ServiceConnection import android.net.VpnService import android.os.* +import bridge.Bridge +import bridge.TunCallback import com.github.kr328.clash.core.event.* import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.net.DefaultNetworkObserver @@ -15,7 +17,7 @@ class TunService : VpnService(), IClashEventObserver { companion object { // from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/bg/VpnService.kt private const val VPN_MTU = 1500 - private const val PRIVATE_VLAN_DNS = "114.114.114.114" // sync with tun/tun.go/dnsServerAddress + private const val PRIVATE_VLAN_DNS = "172.19.0.2" // sync with tun/tun.go/dnsServerAddress private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1" private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1" } @@ -108,11 +110,18 @@ class TunService : VpnService(), IClashEventObserver { else stopSelf() + Bridge.stopTunDevice() + Log.i("STOPPED") } ProcessEvent.STARTED -> { start = false - clash.startTunDevice(fileDescriptor, VPN_MTU, settings.isDnsHijackingEnabled) + + Bridge.startTunDevice(fileDescriptor.fd.toLong(), VPN_MTU.toLong(), + "$PRIVATE_VLAN4_CLIENT/30", PRIVATE_VLAN_DNS + ) { + protect(it.toInt()) + } Log.i("STARTED") } @@ -156,6 +165,7 @@ class TunService : VpnService(), IClashEventObserver { } } ClashSettingService.ACCESS_CONTROL_MODE_ALLOW -> { + addAllowedApplication(packageName) for ( app in (settings.accessControlApps.toSet() - resources.getStringArray(R.array.default_disallow_application)) ) { runCatching { @@ -167,7 +177,8 @@ class TunService : VpnService(), IClashEventObserver { } ClashSettingService.ACCESS_CONTROL_MODE_DISALLOW -> { for ( app in (settings.accessControlApps.toSet() + - resources.getStringArray(R.array.default_disallow_application)) ) { + resources.getStringArray(R.array.default_disallow_application) - + listOf(packageName)) ) { runCatching { addDisallowedApplication(app) }.onFailure { diff --git a/service/src/main/res/values/arrays.xml b/service/src/main/res/values/arrays.xml index 61a924ac30..7777245026 100644 --- a/service/src/main/res/values/arrays.xml +++ b/service/src/main/res/values/arrays.xml @@ -42,7 +42,6 @@ - com.github.kr328.clash com.android.networkstack From 51acbc75f5e62b555c7747c9190e9c0830468e44 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 15 Dec 2019 16:01:48 +0800 Subject: [PATCH 042/358] fix crash on close --- .../kr328/clash/service/IClashService.aidl | 2 -- .../kr328/clash/service/ClashService.kt | 21 ++---------- .../github/kr328/clash/service/TunService.kt | 33 +++++++++---------- 3 files changed, 18 insertions(+), 38 deletions(-) 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 be56221f04..8b2c4baca7 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 @@ -19,8 +19,6 @@ interface IClashService { // Control void setSelectProxy(String proxy, String selected); - void startTunDevice(in ParcelFileDescriptor fd, int mtu, boolean dnsHijacking); - void stopTunDevice(); void start(); void stop(); 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 cc60f5a02a..7ff9d13d0d 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 @@ -33,10 +33,6 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, private lateinit var notification: ClashNotification private val clashService = object : IClashService.Stub() { - override fun stopTunDevice() { - notification.setVpn(false) - } - override fun setSelectProxy(proxy: String?, selected: String?) { require(proxy != null && selected != null) @@ -73,25 +69,14 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } override fun stop() { + if ( processStatus == ProcessEvent.STOPPED ) + return + processStatus = ProcessEvent.STOPPED this@ClashService.eventService.performProcessEvent(processStatus) } - override fun startTunDevice(fd: ParcelFileDescriptor, mtu: Int, dnsHijacking: Boolean) { - try { - notification.setVpn(true) - } catch (e: Exception) { - Log.e("Start tun failure", e) - - this@ClashService.eventService.performErrorEvent( - ErrorEvent(ErrorEvent.Type.START_FAILURE, e.toString()) - ) - } finally { - fd.close() - } - } - override fun getEventService(): IClashEventService { return this@ClashService.eventService } 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 e6d4c5c81b..d3c6063da4 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,7 +8,6 @@ import android.content.ServiceConnection import android.net.VpnService import android.os.* import bridge.Bridge -import bridge.TunCallback import com.github.kr328.clash.core.event.* import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.net.DefaultNetworkObserver @@ -19,7 +18,6 @@ class TunService : VpnService(), IClashEventObserver { private const val VPN_MTU = 1500 private const val PRIVATE_VLAN_DNS = "172.19.0.2" // sync with tun/tun.go/dnsServerAddress private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1" - private const val PRIVATE_VLAN6_CLIENT = "fdfe:dcba:9876::1" } private var start = true @@ -87,9 +85,6 @@ class TunService : VpnService(), IClashEventObserver { override fun onDestroy() { super.onDestroy() - fileDescriptor.close() - - clash.stopTunDevice() clash.stop() clash.eventService.unregisterEventObserver(TunService::class.java.simpleName) @@ -117,11 +112,22 @@ class TunService : VpnService(), IClashEventObserver { ProcessEvent.STARTED -> { start = false - Bridge.startTunDevice(fileDescriptor.fd.toLong(), VPN_MTU.toLong(), - "$PRIVATE_VLAN4_CLIENT/30", PRIVATE_VLAN_DNS - ) { - protect(it.toInt()) + if ( settings.isDnsHijackingEnabled ) { + Bridge.startTunDevice(fileDescriptor.fd.toLong(), VPN_MTU.toLong(), + "$PRIVATE_VLAN4_CLIENT/30", "0.0.0.0" + ) { + protect(it.toInt()) + } } + else { + Bridge.startTunDevice(fileDescriptor.fd.toLong(), VPN_MTU.toLong(), + "$PRIVATE_VLAN4_CLIENT/30", PRIVATE_VLAN_DNS + ) { + protect(it.toInt()) + } + } + + fileDescriptor.close() Log.i("STARTED") } @@ -146,12 +152,6 @@ class TunService : VpnService(), IClashEventObserver { addRoute("0.0.0.0", 0) } - // IPv6 - if ( settings.isIPv6Enabled ) { - - addRoute("::", 0) - } - return this } @@ -194,9 +194,6 @@ class TunService : VpnService(), IClashEventObserver { private fun Builder.addAddress(): Builder { addAddress(PRIVATE_VLAN4_CLIENT, 30) - if ( settings.isIPv6Enabled ) - addAddress(PRIVATE_VLAN6_CLIENT, 126) - return this } From 0bd1fcf51bbc28a768b3b194ad6bf711a0bcd362 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 15 Dec 2019 16:03:48 +0800 Subject: [PATCH 043/358] fix crash on close --- core/src/main/golang/clash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index a18182a9ed..78b0f8c718 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit a18182a9ed815be22594f2729533391c9ed5ec71 +Subproject commit 78b0f8c71846dd8cc800648a82eff8d40a6b3868 From eccc9bd8863db8df63ce554dba733f67b169109b Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 16 Dec 2019 18:57:23 +0800 Subject: [PATCH 044/358] refactor event service dispatch --- .../com/github/kr328/clash/MainActivity.kt | 8 +- buildSrc/src/main/java/GolangBindTask.kt | 7 - buildSrc/src/main/java/GolangBuildOptions.kt | 3 - buildSrc/src/main/java/GolangBuildTask.kt | 92 -------- core/src/main/golang/bridge/general.go | 27 +++ core/src/main/golang/bridge/proxies.go | 63 +++++ .../java/com/github/kr328/clash/core/Clash.kt | 216 ++++++------------ .../kr328/clash/core/model/Compression.kt | 65 ++++++ .../model/{GeneralPacket.kt => General.kt} | 49 ++-- .../clash/core/model/LoadProfilePacket.kt | 10 - .../github/kr328/clash/core/model/Proxy.kt | 80 +++++++ .../kr328/clash/core/model/ProxyPacket.kt | 109 --------- .../kr328/clash/core/model/RawProxyPacket.kt | 15 -- .../kr328/clash/core/model/SetProxyPacket.kt | 10 - .../kr328/clash/core/model/UrlTestPacket.kt | 11 - .../github/kr328/clash/core/model/Packet.aidl | 4 +- .../kr328/clash/service/IClashService.aidl | 4 +- .../kr328/clash/service/ClashEventBridge.kt | 37 +++ .../kr328/clash/service/ClashService.kt | 162 ++++--------- .../kr328/clash/service/ClashServiceImpl.kt | 88 +++++++ .../github/kr328/clash/service/TunService.kt | 2 + 21 files changed, 516 insertions(+), 546 deletions(-) delete mode 100644 buildSrc/src/main/java/GolangBuildOptions.kt delete mode 100644 buildSrc/src/main/java/GolangBuildTask.kt create mode 100644 core/src/main/golang/bridge/general.go create mode 100644 core/src/main/golang/bridge/proxies.go create mode 100644 core/src/main/java/com/github/kr328/clash/core/model/Compression.kt rename core/src/main/java/com/github/kr328/clash/core/model/{GeneralPacket.kt => General.kt} (54%) delete mode 100644 core/src/main/java/com/github/kr328/clash/core/model/LoadProfilePacket.kt create mode 100644 core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/model/RawProxyPacket.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/model/SetProxyPacket.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/model/UrlTestPacket.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index f761466c6e..5e480fd0fe 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -6,7 +6,7 @@ import android.content.Intent import android.os.Bundle import android.view.View import com.github.kr328.clash.core.event.* -import com.github.kr328.clash.core.model.GeneralPacket +import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.utils.ByteFormatter import com.github.kr328.clash.service.TunService import com.github.kr328.clash.utils.ServiceUtils @@ -113,13 +113,13 @@ class MainActivity : BaseActivity() { runOnUiThread { when ( general.mode ) { - GeneralPacket.Mode.DIRECT -> + General.Mode.DIRECT -> activity_main_clash_proxies_summary.text = getText(R.string.clash_proxy_manage_summary_direct) - GeneralPacket.Mode.GLOBAL -> + General.Mode.GLOBAL -> activity_main_clash_proxies_summary.text = getText(R.string.clash_proxy_manage_summary_global) - GeneralPacket.Mode.RULE -> + General.Mode.RULE -> activity_main_clash_proxies_summary.text = getText(R.string.clash_proxy_manage_summary_rule) } diff --git a/buildSrc/src/main/java/GolangBindTask.kt b/buildSrc/src/main/java/GolangBindTask.kt index b6654b6a2c..6aaf2bd856 100644 --- a/buildSrc/src/main/java/GolangBindTask.kt +++ b/buildSrc/src/main/java/GolangBindTask.kt @@ -173,11 +173,4 @@ open class GolangBindTask : DefaultTask() { if (process.waitFor() != 0) throw GradleException("Run command $this failure") } - - private fun String.exe(): String { - return if (Os.isFamily(Os.FAMILY_WINDOWS)) - "$this.exe" - else - this - } } \ No newline at end of file diff --git a/buildSrc/src/main/java/GolangBuildOptions.kt b/buildSrc/src/main/java/GolangBuildOptions.kt deleted file mode 100644 index a3df27338d..0000000000 --- a/buildSrc/src/main/java/GolangBuildOptions.kt +++ /dev/null @@ -1,3 +0,0 @@ -import java.io.File - -data class GolangBuildOptions(val sourceDir: File, val outputDir: File, val platform: String) diff --git a/buildSrc/src/main/java/GolangBuildTask.kt b/buildSrc/src/main/java/GolangBuildTask.kt deleted file mode 100644 index 68a2d3d8ae..0000000000 --- a/buildSrc/src/main/java/GolangBuildTask.kt +++ /dev/null @@ -1,92 +0,0 @@ -import org.apache.tools.ant.taskdefs.condition.Os -import org.gradle.api.GradleException -import org.gradle.api.tasks.Exec -import java.io.File -import java.io.FileReader - -import java.util.Properties - -open class GolangBuildTask : Exec() { - private lateinit var options: GolangBuildOptions - private lateinit var abi: String - - override fun exec() { - val toolchainRoot = findAndroidNdkPath().resolve("toolchains/llvm/prebuilt/${detectOsType()}/bin") - val linkerPrefix = toolChainPrefix() - val compilerPrefix = linkerPrefix + options.platform - - if ( !toolchainRoot.exists() ) - throw GradleException("Compiler not found") - - workingDir = options.sourceDir - - environment.put("GOARCH", golangArch()) - environment.put("GOOS", "android") - environment.put("CGO_ENABLED", "1") - environment.put("GOPATH", project.buildDir.resolve("intermediates/gopath").absolutePath) - environment.put("CXX", toolchainRoot.resolve(compilerPrefix + "-clang++".cmd()).absolutePath) - environment.put("CC", toolchainRoot.resolve(compilerPrefix + "-clang".cmd()).absolutePath) - environment.put("LD", toolchainRoot.resolve(linkerPrefix + "ld".exe()).absolutePath) - - commandLine = listOf("go".exe(), "build", "-o", options.outputDir.resolve("$abi/libclash.so").absolutePath) - - super.exec() - } - - fun setOptions(options: GolangBuildOptions, abi: String) { - this.options = options - this.abi = abi - } - - private fun findAndroidNdkPath(): File { - val properties = FileReader(project.rootProject.file("local.properties")).use { - Properties().apply { load(it) } - } - - return properties.getProperty("ndk.dir")?.let { File(it) }?.takeIf { it.exists() } - ?: throw GradleException("Android NDK not found.") - } - - private fun detectOsType(): String { - return when { - Os.isFamily(Os.FAMILY_WINDOWS) -> "windows-x86_64" - Os.isFamily(Os.FAMILY_MAC) -> "darwin-x86_64" - Os.isFamily(Os.FAMILY_UNIX) -> "linux-x86_64" - else -> throw GradleException("Unsupported Build OS ${System.getenv("os.name")}") - } - } - - private fun golangArch(): String { - return when (abi) { - "armeabi-v7a" -> "arm" - "arm64-v8a" -> "arm64" - "x86" -> "i686" - "x86_64" -> "amd64" - else -> throw GradleException("Unsupported arch $abi") - } - } - - private fun toolChainPrefix(): String { - return when (abi) { - "armeabi-v7a" -> "arm-linux-androideabi" - "arm64-v8a" -> "aarch64-linux-android" - "x86" -> "i686-linux-android" - "x86_64" -> "x86_64-linux-android" - else -> throw GradleException("Unsupported arch $abi") - } - } - - private fun String.exe(): String { - return if ( Os.isFamily(Os.FAMILY_WINDOWS) ) - "$this.exe" - else - this - } - - private fun String.cmd(): String { - return if ( Os.isFamily(Os.FAMILY_WINDOWS) ) - "$this.cmd" - else - this - } -} diff --git a/core/src/main/golang/bridge/general.go b/core/src/main/golang/bridge/general.go new file mode 100644 index 0000000000..ac422e3606 --- /dev/null +++ b/core/src/main/golang/bridge/general.go @@ -0,0 +1,27 @@ +package bridge + +import ( + "github.com/Dreamacro/clash/hub/executor" + "github.com/Dreamacro/clash/tunnel" +) + +type TunnelGeneral struct { + Mode string + HTTPPort int + SocksPort int + RedirectPort int +} + +func QueryGeneral() *TunnelGeneral { + result := &TunnelGeneral{} + + g := executor.GetGeneral() + t := tunnel.Instance() + + result.Mode = t.Mode().String() + result.HTTPPort = g.Port + result.SocksPort = g.SocksPort + result.RedirectPort = g.RedirPort + + return result +} diff --git a/core/src/main/golang/bridge/proxies.go b/core/src/main/golang/bridge/proxies.go new file mode 100644 index 0000000000..7693d4b807 --- /dev/null +++ b/core/src/main/golang/bridge/proxies.go @@ -0,0 +1,63 @@ +package bridge + +import ( + "encoding/json" + + "github.com/Dreamacro/clash/tunnel" +) + +type Proxy struct { + Name string + Type string `json:"type"` + All []string `json:"all"` + Now string `json:"now"` + Delay int +} + +type ProxyList struct { + proxies []*Proxy +} + +func (p Proxy) GetAllLength() int { + return len(p.All) +} + +func (p Proxy) GetAllElement(index int) string { + return p.All[index] +} + +func (pl *ProxyList) GetProxiesLength() int { + return len(pl.proxies) +} + +func (pl *ProxyList) GetProxiesElement(index int) *Proxy { + return pl.proxies[index] +} + +func QueryAllProxies() (*ProxyList, error) { + ps := tunnel.Instance().Proxies() + result := make([]*Proxy, len(ps)) + currentIndex := 0 + + for k, v := range ps { + current := &Proxy{ + Name: k, + Type: v.Type().String(), + All: []string{}, + Delay: int(v.LastDelay()), + } + + data, err := v.MarshalJSON() + if err != nil { + return nil, err + } + + json.Unmarshal(data, current) + + result[currentIndex] = current + + currentIndex++ + } + + return &ProxyList{proxies: result}, nil +} diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 2e68cc1d2b..43b11c7b38 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -1,194 +1,118 @@ package com.github.kr328.clash.core import android.content.Context -import android.net.LocalSocket -import com.github.kr328.clash.core.event.BandwidthEvent -import com.github.kr328.clash.core.event.LogEvent +import bridge.Bridge import com.github.kr328.clash.core.event.ProcessEvent -import com.github.kr328.clash.core.event.SpeedEvent -import com.github.kr328.clash.core.model.* -import com.github.kr328.clash.core.utils.Log -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonConfiguration -import kotlinx.serialization.stringify -import java.io.File -import java.io.FileDescriptor -import java.io.IOException +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.model.Proxy +import java.lang.IllegalStateException class Clash( context: Context, - clashDir: File, - controllerPath: File, - listener: (ProcessEvent) -> Unit -) : BaseClash(controllerPath) { + private val listener: (ProcessEvent) -> Unit +) { companion object { - const val COMMAND_PING = 0 - const val COMMAND_TUN_START = 1 - const val COMMAND_TUN_STOP = 2 - const val COMMAND_PROFILE_RELOAD = 4 - const val COMMAND_QUERY_PROXIES = 5 - const val COMMAND_PULL_SPEED = 6 - const val COMMAND_PULL_LOG = 7 - const val COMMAND_PULL_BANDWIDTH = 8 - const val COMMAND_SET_PROXY = 9 - const val COMMAND_QUERY_GENERAL = 10 - const val COMMAND_URL_TEST = 11 - - const val PING_REPLY = 233 + const val CLASH_DIR = "clash" const val DEFAULT_URL_TEST_TIMEOUT = 5000 const val DEFAULT_URL_TEST_URL = "https://www.gstatic.com/generate_204" } - val process = ClashProcess(context, clashDir, controllerPath, listener) + private var currentProcess = ProcessEvent.STOPPED - fun ping(): Boolean { - return runControlNoException(COMMAND_PING) { _, input, _ -> - input.readInt() == PING_REPLY - } ?: false - } - - fun loadProfile(file: File, selected: Map): List { - return runControl(COMMAND_PROFILE_RELOAD) { _, input, output -> - output.writeString( - Json(JsonConfiguration.Stable) - .stringify( - LoadProfilePacket.Request.serializer(), - LoadProfilePacket.Request(file.absolutePath, selected) - ) - ) + init { + val home = context.filesDir.resolve(CLASH_DIR) - val result = Json(JsonConfiguration.Stable) - .parse(LoadProfilePacket.Response.serializer(), input.readString()) - - if (result.error.isNotEmpty()) { - throw IOException(result.error) - } + home.mkdirs() - return@runControl result.invalidSelected - } + Bridge.init(home.absolutePath) } - fun queryGeneral(): GeneralPacket { - return runCatching { - runControl(COMMAND_QUERY_GENERAL) { _, input, _ -> - Json(JsonConfiguration.Stable).parse(GeneralPacket.serializer(), input.readString()) - } - }.getOrDefault( - GeneralPacket( - GeneralPacket.Ports(0, 0, 0, 0), - GeneralPacket.Mode.DIRECT - ) - ) + fun getCurrentProcessStatus(): ProcessEvent { + return currentProcess } - fun queryProxies(): RawProxyPacket { - return runControl(COMMAND_QUERY_PROXIES) { _, input, _ -> - val data = input.readString() + fun start() { + if ( currentProcess == ProcessEvent.STARTED ) + return - Json(JsonConfiguration.Stable.copy(strictMode = false)) - .parse(RawProxyPacket.serializer(), data) - } + currentProcess = ProcessEvent.STARTED + + listener(currentProcess) + + loadDefault() } - fun setSelectProxy(key: String, value: String) { - runControl(COMMAND_SET_PROXY) { _, input, output -> - output.writeString( - Json(JsonConfiguration.Stable) - .stringify( - SetProxyPacket.Request.serializer(), - SetProxyPacket.Request(key, value) - ) - ) + fun stop() { + if ( currentProcess == ProcessEvent.STOPPED ) + return - val response = Json(JsonConfiguration.Stable).parse( - SetProxyPacket.Response.serializer(), - input.readString() - ) + currentProcess = ProcessEvent.STOPPED - if (response.error.isNotEmpty()) - throw IOException(response.error) - } + listener(currentProcess) + + loadDefault() } - fun pullSpeedEvent(initial: (LocalSocket) -> Unit, callback: (SpeedEvent) -> Unit) { - runControlNoException(COMMAND_PULL_SPEED) { socket, input, _ -> - initial(socket) + fun startTunDevice(fd: Int, mtu: Int, gateway: String, dns: String, onSocket: (Int) -> Unit) { + enforceStarted() - while (!Thread.currentThread().isInterrupted) { - callback( - Json(JsonConfiguration.Stable).parse( - SpeedEvent.serializer(), - input.readString() - ) - ) - } + Bridge.startTunDevice(fd.toLong(), mtu.toLong(), gateway, dns) { + onSocket(it.toInt()) } } - fun pullLogsEvent(initial: (LocalSocket) -> Unit, callback: (LogEvent) -> Unit) { - runControlNoException(COMMAND_PULL_LOG) { socket, input, _ -> - initial(socket) - - while (!Thread.currentThread().isInterrupted) { - callback( - Json(JsonConfiguration.Stable).parse( - LogEvent.serializer(), - input.readString() - ) - ) - } - } + fun stopTunDevice() { + Bridge.stopTunDevice() } - fun pullBandwidthEvent(initial: (LocalSocket) -> Unit, callback: (BandwidthEvent) -> Unit) { - runControlNoException(COMMAND_PULL_BANDWIDTH) { socket, input, _ -> - initial(socket) + fun loadProfile(path: String) { + enforceStarted() - while (!Thread.currentThread().isInterrupted) { - callback( - Json(JsonConfiguration.Stable).parse( - BandwidthEvent.serializer(), - input.readString() - ) - ) - } - } + Bridge.loadProfileFile(path) } - fun startUrlTest(proxies: List, callback: (String, Long) -> Unit) { - runControl(COMMAND_URL_TEST) { _, input, output -> - output.writeString(Json(JsonConfiguration.Stable) - .stringify(UrlTestPacket.Request.serializer(), - UrlTestPacket.Request(proxies, DEFAULT_URL_TEST_TIMEOUT, DEFAULT_URL_TEST_URL))) + fun queryProxies(): List { + enforceStarted() - while ( true ) { - val data = input.readString() - if ( data.isEmpty() ) - return@runControl + val list = Bridge.queryAllProxies() + val result = mutableListOf() - val response = Json(JsonConfiguration.Stable) - .parse(UrlTestPacket.Response.serializer(), data) + for (i in 0..list.proxiesLength) { + val p = list.getProxiesElement(i) + val all = mutableListOf() - callback(response.name, response.delay) + for (index in 0..p.allLength) { + all.add(p.getAllElement(index)) } + + result.add( + Proxy( + p.name, + Proxy.Type.fromString(p.type), + p.now, + all, + p.delay + ) + ) } - Log.i("Url test exited") + return result } - fun startTunDevice(fd: FileDescriptor, mtu: Int, dnsHijacking: Boolean) { - runControl(COMMAND_TUN_START) { socket, _, output -> - socket.setFileDescriptorsForSend(arrayOf(fd)) - socket.outputStream.write(0) - socket.outputStream.flush() - output.writeInt(mtu) - output.writeInt(if ( dnsHijacking ) 1 else 0) - output.writeInt(0x243) - } + fun queryGeneral(): General { + val t = Bridge.queryGeneral() + + return General(General.Mode.fromString(t.mode), + t.httpPort.toInt(), t.socksPort.toInt(), t.redirectPort.toInt()) } - fun stopTunDevice() { - runControlNoException(COMMAND_TUN_STOP) + private fun loadDefault() { + Bridge.loadProfileDefault() + } + + private fun enforceStarted() { + if ( currentProcess == ProcessEvent.STOPPED ) + throw IllegalStateException("Clash Stopped") } } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt b/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt new file mode 100644 index 0000000000..b411b7bc88 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt @@ -0,0 +1,65 @@ +package com.github.kr328.clash.core.model + +import android.os.Parcel +import android.os.Parcelable +import com.github.kr328.clash.core.serialization.Parcels +import kotlinx.serialization.Serializable +import java.lang.IllegalStateException + +fun List.compress(): CompressedProxyList { + val nameMap = this.toList().mapIndexed { index, proxy -> + proxy.name to index + }.toMap() + + val elements = this.map { + CompressedProxyList.Element( + name = nameMap[it.name] ?: throw IllegalStateException("Unknown proxy $it"), + type = it.type, + now = nameMap[it.now] ?: throw IllegalStateException("Unknown proxy $it"), + all = it.all.mapNotNull { name -> nameMap[name] }, + delay = it.delay + ) + } + + return CompressedProxyList(nameMap.entries.associate { (k, v) -> v to k }, elements) +} + +@Serializable +data class CompressedProxyList(val proxyName: Map, val elements: List): Parcelable { + @Serializable + data class Element(val name: Int, + val type: Proxy.Type, + val now: Int, + val all: List, + val delay: Long) + + fun uncompress(): List { + return elements.map { + Proxy( + name = proxyName[it.name] ?: throw IllegalStateException("Unknown proxy $it"), + type = it.type, + now = proxyName[it.now] ?: throw IllegalStateException("Unknown proxy $it"), + all = it.all.mapNotNull { index -> proxyName[index] }, + delay = it.delay + ) + } + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + Parcels.dump(serializer(), this, parcel) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): CompressedProxyList { + return Parcels.load(serializer(), parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/GeneralPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/General.kt similarity index 54% rename from core/src/main/java/com/github/kr328/clash/core/model/GeneralPacket.kt rename to core/src/main/java/com/github/kr328/clash/core/model/General.kt index cad1778814..f310e93eec 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/GeneralPacket.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/General.kt @@ -5,25 +5,35 @@ import android.os.Parcelable import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.* import kotlinx.serialization.internal.StringDescriptor -import java.lang.IllegalArgumentException +import kotlin.IllegalArgumentException @Serializable -data class GeneralPacket(val ports: Ports, val mode: Mode) : Parcelable { +data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: Int) : Parcelable { @Serializable(with = ModeSerializer::class) enum class Mode { - DIRECT, GLOBAL, RULE - } + DIRECT, GLOBAL, RULE; - @Serializable - data class Ports(val http: Int, val socks: Int, val redirect: Int, val randomHttp: Int) + override fun toString(): String { + return when ( this ) { + DIRECT -> "Direct" + GLOBAL -> "Global" + RULE -> "Rule" + } + } - class ModeSerializer : KSerializer { companion object { - const val MODE_DIRECT = 1 - const val MODE_GLOBAL = 2 - const val MODE_RULE = 3 + fun fromString(mode: String): Mode { + return when ( mode ) { + "Direct" -> DIRECT + "Global" -> GLOBAL + "Rule" -> RULE + else -> throw IllegalArgumentException("Invalid mode $mode") + } + } } + } + class ModeSerializer : KSerializer { override val descriptor: SerialDescriptor get() = StringDescriptor @@ -53,13 +63,20 @@ data class GeneralPacket(val ports: Ports, val mode: Mode) : Parcelable { return 0 } - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): GeneralPacket { - return Parcels.load(serializer(), parcel) - } + companion object { + const val MODE_DIRECT = 1 + const val MODE_GLOBAL = 2 + const val MODE_RULE = 3 - override fun newArray(size: Int): Array { - return arrayOfNulls(size) + @JvmField + val CREATOR = object: Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): General { + return Parcels.load(serializer(), parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } } } } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/LoadProfilePacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/LoadProfilePacket.kt deleted file mode 100644 index 77adf3e715..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/LoadProfilePacket.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.kr328.clash.core.model - -import kotlinx.serialization.Serializable - -class LoadProfilePacket { - @Serializable - data class Request(val path: String, val selected: Map) - @Serializable - data class Response(val error: String, val invalidSelected: List) -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt b/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt new file mode 100644 index 0000000000..19cf097730 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt @@ -0,0 +1,80 @@ +package com.github.kr328.clash.core.model + +import kotlinx.serialization.Serializable + +@Serializable +data class Proxy( + val name: String, + val type: Type, + val now: String, + val all: List, + val delay: Long +) { + enum class Type { + SELECT, + URL_TEST, + FALLBACK, + DIRECT, + REJECT, + SHADOWSOCKS, + SNELL, + SOCKS5, + HTTP, + VMESS, + LOAD_BALANCE, + UNKNOWN; + + override fun toString(): String { + return when (this) { + SELECT -> TYPE_SELECT + URL_TEST -> TYPE_URL_TEST + FALLBACK -> TYPE_FALLBACK + DIRECT -> TYPE_DIRECT + REJECT -> TYPE_REJECT + SHADOWSOCKS -> TYPE_SHADOWSOCKS + SNELL -> TYPE_SNELL + SOCKS5 -> TYPE_SOCKS5 + HTTP -> TYPE_HTTP + VMESS -> TYPE_VMESS + LOAD_BALANCE -> TYPE_LOAD_BALANCE + UNKNOWN -> TYPE_UNKNOWN + } + } + + companion object { + fun fromString(type: String): Type { + return when (type) { + TYPE_SELECT -> SELECT + TYPE_URL_TEST -> URL_TEST + TYPE_FALLBACK -> FALLBACK + TYPE_DIRECT -> DIRECT + TYPE_REJECT -> REJECT + TYPE_SHADOWSOCKS -> SHADOWSOCKS + TYPE_SNELL -> SNELL + TYPE_SOCKS5 -> SOCKS5 + TYPE_HTTP -> HTTP + TYPE_VMESS -> VMESS + TYPE_LOAD_BALANCE -> LOAD_BALANCE + TYPE_UNKNOWN -> UNKNOWN + else -> UNKNOWN + } + } + } + } + + companion object { + private const val TYPE_SELECT = "Selector" + private const val TYPE_URL_TEST = "URLTest" + private const val TYPE_FALLBACK = "Fallback" + private const val TYPE_DIRECT = "Direct" + private const val TYPE_REJECT = "Reject" + private const val TYPE_SHADOWSOCKS = "Shadowsocks" + private const val TYPE_SNELL = "Snell" + private const val TYPE_SOCKS5 = "Socks5" + private const val TYPE_HTTP = "Http" + private const val TYPE_VMESS = "Vmess" + private const val TYPE_LOAD_BALANCE = "LoadBalance" + private const val TYPE_UNKNOWN = "Unknown" + + } +} diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt deleted file mode 100644 index 651297dfae..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyPacket.kt +++ /dev/null @@ -1,109 +0,0 @@ -package com.github.kr328.clash.core.model - -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.Parcels -import kotlinx.serialization.Serializable - -@Serializable -data class ProxyPacket(val mode: String, val proxies: Map): Parcelable { - @Serializable - data class Proxy(val name: String, val type: Type, val now: Int, val all: List, val delay: Long) - - enum class Type { - SELECT, - URL_TEST, - FALLBACK, - DIRECT, - REJECT, - SHADOWSOCKS, - SNELL, - SOCKS5, - HTTP, - VMESS, - LOAD_BALANCE, - UNKNOWN; - - override fun toString(): String { - return when ( this ) { - SELECT -> "Select" - URL_TEST -> "UrlTest" - FALLBACK -> "Fallback" - DIRECT -> "Direct" - REJECT -> "Reject" - SHADOWSOCKS -> "Shadowsocks" - SNELL -> "Snell" - SOCKS5 -> "Socks5" - HTTP -> "HTTP" - VMESS -> "Vmess" - LOAD_BALANCE -> "LoadBalance" - UNKNOWN -> "Unknown" - } - } - } - - companion object { - private const val TYPE_SELECT = "Selector" - private const val TYPE_URL_TEST = "URLTest" - private const val TYPE_FALLBACK = "Fallback" - private const val TYPE_DIRECT = "Direct" - private const val TYPE_REJECT = "Reject" - private const val TYPE_SHADOWSOCKS = "Shadowsocks" - private const val TYPE_SNELL = "Snell" - private const val TYPE_SOCKS5 = "Socks5" - private const val TYPE_HTTP = "Http" - private const val TYPE_VMESS = "Vmess" - private const val TYPE_LOAD_BALANCE = "LoadBalance" - private const val TYPE_UNKNOWN = "Unknown" - - fun fromRawProxy(rawProxy: RawProxyPacket): ProxyPacket { - val hashed = rawProxy.proxies - .map { it.key to (it.key.hashCode() to it.value) }.toMap() - - val proxies = hashed.map { entry -> - val type = when ( entry.value.second.type ) { - TYPE_SELECT -> Type.SELECT - TYPE_URL_TEST -> Type.URL_TEST - TYPE_FALLBACK -> Type.FALLBACK - TYPE_DIRECT -> Type.DIRECT - TYPE_REJECT -> Type.REJECT - TYPE_SHADOWSOCKS -> Type.SHADOWSOCKS - TYPE_SNELL -> Type.SNELL - TYPE_SOCKS5 -> Type.SOCKS5 - TYPE_HTTP -> Type.HTTP - TYPE_VMESS -> Type.VMESS - TYPE_LOAD_BALANCE -> Type.LOAD_BALANCE - TYPE_UNKNOWN -> Type.UNKNOWN - else -> Type.UNKNOWN - } - - val now = hashed[entry.value.second.now]?.first ?: 0 - val all = entry.value.second.all.mapNotNull { hashed[it]?.first } - val delay = entry.value.second.history.firstOrNull()?.delay ?: 0 - - entry.value.first to Proxy(entry.key, type, now, all, delay) - } - - return ProxyPacket(rawProxy.mode, proxies.toMap()) - } - - @JvmField - val CREATOR = object: Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ProxyPacket { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/RawProxyPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/RawProxyPacket.kt deleted file mode 100644 index ac581cab27..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/RawProxyPacket.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.kr328.clash.core.model - -import kotlinx.serialization.Serializable - -@Serializable -data class RawProxyPacket(val mode: String, val proxies: Map) { - @Serializable - data class RawProxy(val type: String, - val all: List = emptyList(), - val now: String = "", - val history: List = emptyList()) { - @Serializable - data class History(val delay: Long) - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/SetProxyPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/SetProxyPacket.kt deleted file mode 100644 index ac8157e074..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/SetProxyPacket.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.kr328.clash.core.model - -import kotlinx.serialization.Serializable - -class SetProxyPacket { - @Serializable - data class Request(val key: String, val value: String) - @Serializable - data class Response(val error: String) -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/UrlTestPacket.kt b/core/src/main/java/com/github/kr328/clash/core/model/UrlTestPacket.kt deleted file mode 100644 index 91ea1220c9..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/UrlTestPacket.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.kr328.clash.core.model - -import kotlinx.serialization.Serializable - -class UrlTestPacket { - @Serializable - data class Request(val proxies: List, val timeout: Int, val url: String) - @Serializable - data class Response(val name: String, val delay: Long) -} - diff --git a/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl b/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl index 515a8d4521..ba5c22586d 100644 --- a/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl @@ -1,4 +1,4 @@ package com.github.kr328.clash.core.model; -parcelable ProxyPacket; -parcelable GeneralPacket; \ No newline at end of file +parcelable CompressedProxyList; +parcelable General; \ No newline at end of file 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 8b2c4baca7..98166b3ce7 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 @@ -23,7 +23,7 @@ interface IClashService { void stop(); // Query - ProxyPacket queryAllProxies(); - GeneralPacket queryGeneral(); + CompressedProxyList queryAllProxies(); + General queryGeneral(); void startUrlTest(in String[] proxies, IUrlTestCallback callback); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt new file mode 100644 index 0000000000..b9fd4ba5d6 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt @@ -0,0 +1,37 @@ +package com.github.kr328.clash.service + +import android.content.Context +import com.github.kr328.clash.core.event.* + +class ClashEventBridge(val service: ClashService): ClashProfileService.Master, + ClashEventService.Master, ClashEventPuller.Master { + val eventService: ClashEventService = ClashEventService(this) + + override fun preformProfileChanged() { + eventService.performProfileChangedEvent(ProfileChangedEvent()) + } + + override fun acquireEvent(event: Int) { + service.acquireEvent(event) + } + + override fun releaseEvent(event: Int) { + service.releaseEvent(event) + } + + fun onProcessChanged(event: ProcessEvent) { + eventService.performProcessEvent(event) + } + + override fun onLogPulled(event: LogEvent) { + eventService.performLogEvent(event) + } + + override fun onSpeedPulled(event: SpeedEvent) { + eventService.performSpeedEvent(event) + } + + override fun onBandwidthPulled(event: BandwidthEvent) { + eventService.performBandwidthEvent(event) + } +} \ No newline at end of file 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 7ff9d13d0d..3b45749320 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 @@ -5,106 +5,40 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.os.* -import bridge.Bridge -import com.github.kr328.clash.callback.IUrlTestCallback +import android.os.Binder +import android.os.IBinder +import android.os.IInterface import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.event.* -import com.github.kr328.clash.core.model.GeneralPacket -import com.github.kr328.clash.core.model.ProxyPacket import com.github.kr328.clash.core.utils.Log -import java.io.File -import java.io.IOException import java.util.concurrent.Executors -import javax.xml.transform.SourceLocator -import kotlin.concurrent.thread -class ClashService : Service(), IClashEventObserver, ClashEventService.Master, - ClashProfileService.Master, ClashEventPuller.Master { +class ClashService : Service(), IClashEventObserver { private val executor = Executors.newSingleThreadExecutor() - private val eventService = ClashEventService(this) - private val profileService = ClashProfileService(this, this) - private val settingService = ClashSettingService(this) + private val instance: ClashServiceImpl by lazy { + ClashServiceImpl(this) + } - private var processStatus = ProcessEvent.STOPPED + val events: ClashEventService + get() = instance.eventService + val clash: Clash + get() = instance.clash //private lateinit var puller: ClashEventPuller private lateinit var notification: ClashNotification - private val clashService = object : IClashService.Stub() { - override fun setSelectProxy(proxy: String?, selected: String?) { - require(proxy != null && selected != null) - - try { - this@ClashService.profileService.setCurrentProfileProxy(proxy, selected) - } catch (e: IOException) { - Log.w("Set proxy failure", e) - - this@ClashService.eventService.performErrorEvent( - ErrorEvent(ErrorEvent.Type.SET_PROXY_SELECTED, e.toString()) - ) - } - } - - override fun queryGeneral(): GeneralPacket { - return GeneralPacket(GeneralPacket.Ports(0, 0, 0, 0), GeneralPacket.Mode.DIRECT) - } - - override fun queryAllProxies(): ProxyPacket { - return ProxyPacket("Direct", emptyMap()) - } - - override fun startUrlTest(proxies: Array?, callback: IUrlTestCallback?) { - require(proxies != null && callback != null) - } - - override fun start() { - if ( processStatus == ProcessEvent.STARTED ) - return - - processStatus = ProcessEvent.STARTED - - this@ClashService.eventService.performProcessEvent(processStatus) - } - - override fun stop() { - if ( processStatus == ProcessEvent.STOPPED ) - return - - processStatus = ProcessEvent.STOPPED - - this@ClashService.eventService.performProcessEvent(processStatus) - } - - override fun getEventService(): IClashEventService { - return this@ClashService.eventService - } - - override fun getProfileService(): IClashProfileService { - return this@ClashService.profileService - } - - override fun getSettingService(): IClashSettingService { - return this@ClashService.settingService - } - - override fun getCurrentProcessStatus(): ProcessEvent { - return processStatus - } - } - private val screenReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { Intent.ACTION_SCREEN_ON -> - eventService.registerEventObserver( + instance.eventService.registerEventObserver( ClashService::class.java.name, this@ClashService, intArrayOf(Event.EVENT_SPEED) ) Intent.ACTION_SCREEN_OFF -> - eventService.registerEventObserver( + instance.eventService.registerEventObserver( ClashService::class.java.name, this@ClashService, intArrayOf() @@ -113,27 +47,32 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, } } - override fun acquireEvent(event: Int) { - if (processStatus == ProcessEvent.STOPPED) + private fun onClashProcessChanged(event: ProcessEvent) { + instance.eventService.performProcessEvent(event) + } + + fun acquireEvent(event: Int) { + if ( instance.clash.getCurrentProcessStatus() == ProcessEvent.STOPPED ) return + } - override fun releaseEvent(event: Int) { - if (processStatus == ProcessEvent.STOPPED) + fun releaseEvent(event: Int) { + if ( instance.clash.getCurrentProcessStatus() == ProcessEvent.STOPPED ) return + + } override fun onCreate() { super.onCreate() - Bridge.init(filesDir.resolve("clash").absolutePath) - //puller = ClashEventPuller(clash, this) notification = ClashNotification(this) - eventService.registerEventObserver( + instance.eventService.registerEventObserver( ClashService::class.java.name, this@ClashService, intArrayOf(Event.EVENT_SPEED) @@ -148,20 +87,19 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) - clashService.start() + instance.clash.start() return START_NOT_STICKY } override fun onBind(intent: Intent?): IBinder? { - return clashService + return instance } override fun onDestroy() { - clashService.stop() + instance.clash.stop() executor.shutdown() - eventService.shutdown() unregisterReceiver(screenReceiver) @@ -179,17 +117,15 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, notification.show() - eventService.recastEventRequirement() + instance.eventService.recastEventRequirement() } ProcessEvent.STOPPED -> { - eventService.performSpeedEvent(SpeedEvent(0, 0)) - eventService.performBandwidthEvent(BandwidthEvent(0)) + instance.eventService.performSpeedEvent(SpeedEvent(0, 0)) + instance.eventService.performBandwidthEvent(BandwidthEvent(0)) notification.cancel() stopSelf() - - Bridge.loadProfileDefault() } } @@ -198,28 +134,28 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, private fun reloadProfile() { executor.submit { - if ( processStatus != ProcessEvent.STARTED) + if ( clash.getCurrentProcessStatus() != ProcessEvent.STARTED) return@submit - val active = profileService.queryActiveProfile() + val active = instance.profileService.queryActiveProfile() if (active == null) { - eventService.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, "No profile activated")) - clashService.stop() + sendError(ErrorEvent.Type.PROFILE_LOAD, "No profile activated") + clash.stop() return@submit } Log.i("Loading profile ${active.file}") try { - Bridge.loadProfileFile(active.file) + clash.loadProfile(active.file) notification.setProfile(active.name) - eventService.performProfileReloadEvent(ProfileReloadEvent()) + events.performProfileReloadEvent(ProfileReloadEvent()) } catch (e: Exception) { - clashService.stop() - eventService.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, e.message ?: "Unknown")) + clash.stop() + sendError(ErrorEvent.Type.PROFILE_LOAD, e.message) Log.w("Load profile failure", e) } } @@ -233,22 +169,6 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, notification.setSpeed(event?.up ?: 0, event?.down ?: 0) } - override fun preformProfileChanged() { - eventService.performProfileChangedEvent(ProfileChangedEvent()) - } - - override fun onLogPulled(event: LogEvent) { - eventService.performLogEvent(event) - } - - override fun onSpeedPulled(event: SpeedEvent) { - eventService.performSpeedEvent(event) - } - - override fun onBandwidthPulled(event: BandwidthEvent) { - eventService.performBandwidthEvent(event) - } - override fun onBandwidthEvent(event: BandwidthEvent?) {} override fun onLogEvent(event: LogEvent?) {} override fun onErrorEvent(event: ErrorEvent?) {} @@ -257,4 +177,8 @@ class ClashService : Service(), IClashEventObserver, ClashEventService.Master, return this@ClashService } } + + private fun sendError(type: ErrorEvent.Type, message: String?) { + events.performErrorEvent(ErrorEvent(type, message ?: "Unknown")) + } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt new file mode 100644 index 0000000000..33e7569544 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt @@ -0,0 +1,88 @@ +package com.github.kr328.clash.service + +import com.github.kr328.clash.callback.IUrlTestCallback +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.event.ErrorEvent +import com.github.kr328.clash.core.event.ProcessEvent +import com.github.kr328.clash.core.model.CompressedProxyList +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.model.compress +import com.github.kr328.clash.core.utils.Log +import java.io.IOException + +class ClashServiceImpl(val clashService: ClashService) : IClashService.Stub() { + private val bridge: ClashEventBridge = ClashEventBridge(clashService) + + val clash: Clash = Clash(clashService, bridge::onProcessChanged) + val profileService = ClashProfileService(clashService, bridge) + val settingService = ClashSettingService(clashService) + val eventService: ClashEventService + get() = bridge.eventService + + override fun setSelectProxy(proxy: String?, selected: String?) { + require(proxy != null && selected != null) + + try { + profileService.setCurrentProfileProxy(proxy, selected) + } catch (e: IOException) { + Log.w("Set proxy failure", e) + + eventService.performErrorEvent( + ErrorEvent(ErrorEvent.Type.SET_PROXY_SELECTED, e.toString()) + ) + } + } + + override fun queryGeneral(): General { + return clash.queryGeneral() + } + + override fun queryAllProxies(): CompressedProxyList { + return try { + clash.queryProxies().compress() + } + catch (e: Exception) { + Log.w("Query proxies", e) + + eventService.performErrorEvent(ErrorEvent(ErrorEvent.Type.QUERY_PROXY_FAILURE, e.message ?: "Unknown")) + + CompressedProxyList(emptyMap(), emptyList()) + } + } + + override fun startUrlTest(proxies: Array?, callback: IUrlTestCallback?) { + require(proxies != null && callback != null) + } + + override fun start() { + clash.start() + } + + override fun stop() { + clash.stop() + } + + override fun getEventService(): IClashEventService { + return eventService + } + + override fun getProfileService(): IClashProfileService { + return profileService + } + + override fun getSettingService(): IClashSettingService { + return settingService + } + + override fun getCurrentProcessStatus(): ProcessEvent { + return clash.getCurrentProcessStatus() + } + + fun getClashInstance(): Clash { + return clash + } + + fun shutdown() { + eventService.shutdown() + } +} \ 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 d3c6063da4..f8fbc509f8 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 @@ -143,6 +143,8 @@ class TunService : VpnService(), IClashEventObserver { private fun Builder.addBypassPrivateRoute(): Builder { // IPv4 if ( settings.isBypassPrivateNetwork ) { + Log.i("Bypass Private Network") + resources.getStringArray(R.array.bypass_private_route).forEach { val address = it.split("/") addRoute(address[0], address[1].toInt()) From b3ff9abc0de9a6bba8677af2fec618e0243df557 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 16 Dec 2019 23:06:13 +0800 Subject: [PATCH 045/358] refactor service --- .../com/github/kr328/clash/BaseActivity.kt | 6 +- .../github/kr328/clash/ImportFileActivity.kt | 124 +++++++++------- .../github/kr328/clash/ImportUrlActivity.kt | 25 ++-- .../com/github/kr328/clash/MainApplication.kt | 2 +- .../github/kr328/clash/ProfilesActivity.kt | 29 ++-- .../com/github/kr328/clash/ProxyActivity.kt | 57 ++++---- .../kr328/clash/adapter/ProxyAdapter.kt | 14 +- .../com/github/kr328/clash/model/ListProxy.kt | 4 +- core/src/main/golang/bridge/profiles.go | 4 + core/src/main/golang/bridge/proxies.go | 56 ++++++++ core/src/main/golang/bridge/statistics.go | 112 +++++++++++++++ core/src/main/golang/go.mod | 1 + core/src/main/golang/profile/load.go | 5 + .../java/com/github/kr328/clash/core/Clash.kt | 69 ++++++++- .../github/kr328/clash/core/event/Event.kt | 2 +- .../github/kr328/clash/core/event/LogEvent.kt | 50 +++---- .../event/{SpeedEvent.kt => TrafficEvent.kt} | 8 +- .../kr328/clash/core/model/Compression.kt | 7 +- service/build.gradle | 1 - .../github/kr328/clash/core/event/Event.aidl | 2 +- .../clash/service/IClashEventObserver.aidl | 2 +- .../kr328/clash/service/IClashService.aidl | 3 + .../kr328/clash/service/ClashEventBridge.kt | 9 +- .../kr328/clash/service/ClashEventPoll.kt | 78 ++++++++++ .../kr328/clash/service/ClashEventPuller.kt | 134 ------------------ .../kr328/clash/service/ClashEventService.kt | 8 +- .../kr328/clash/service/ClashService.kt | 52 +++++-- .../kr328/clash/service/ClashServiceImpl.kt | 42 +++++- .../github/kr328/clash/service/TunService.kt | 13 +- .../clash/service/data/ClashProfileEntity.kt | 5 +- 30 files changed, 593 insertions(+), 331 deletions(-) create mode 100644 core/src/main/golang/bridge/statistics.go rename core/src/main/java/com/github/kr328/clash/core/event/{SpeedEvent.kt => TrafficEvent.kt} (65%) create mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashEventPuller.kt diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index db17bf275d..928eacb287 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -126,8 +126,8 @@ abstract class BaseActivity : AppCompatActivity(), IClashEventObserver { override fun onErrorEvent(event: ErrorEvent?) = this@BaseActivity.onErrorEvent(event) - override fun onSpeedEvent(event: SpeedEvent?) = - this@BaseActivity.onSpeedEvent(event) + override fun onTrafficEvent(event: TrafficEvent?) = + this@BaseActivity.onTrafficEvent(event) override fun onBandwidthEvent(event: BandwidthEvent?) { this@BaseActivity.onBandwidthEvent(event) @@ -145,7 +145,7 @@ abstract class BaseActivity : AppCompatActivity(), IClashEventObserver { override fun onErrorEvent(event: ErrorEvent?) {} override fun onProfileChanged(event: ProfileChangedEvent?) {} override fun onProcessEvent(event: ProcessEvent?) {} - override fun onSpeedEvent(event: SpeedEvent?) {} + override fun onTrafficEvent(event: TrafficEvent?) {} override fun onBandwidthEvent(event: BandwidthEvent?) {} override fun onProfileReloaded(event: ProfileReloadEvent?) {} override fun asBinder(): IBinder { diff --git a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt index 3cbc5211ac..88e12ec83b 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt @@ -4,13 +4,11 @@ import android.app.Activity import android.content.Intent import android.net.Uri import android.os.Bundle +import android.os.ParcelFileDescriptor import android.view.View import androidx.recyclerview.widget.LinearLayoutManager -import com.charleskorn.kaml.Yaml -import com.charleskorn.kaml.YamlConfiguration import com.charleskorn.kaml.YamlException import com.github.kr328.clash.adapter.FormAdapter -import com.github.kr328.clash.model.ClashProfile import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.utils.FileUtils import com.google.android.material.snackbar.Snackbar @@ -47,9 +45,7 @@ class ImportFileActivity : BaseActivity() { activity_import_file_save.visibility = View.GONE activity_import_file_saving.visibility = View.VISIBLE - thread { - checkAndInsert() - } + checkAndInsert() } setResult(Activity.RESULT_CANCELED) @@ -68,53 +64,63 @@ class ImportFileActivity : BaseActivity() { } private fun checkAndInsert() { - try { - val name = elements[0] as FormAdapter.TextType - val file = elements[1] as FormAdapter.FilePickerType - if (name.content.isEmpty()) { - runOnUiThread { - activity_import_file_save.visibility = View.VISIBLE - activity_import_file_saving.visibility = View.GONE - Snackbar.make( - activity_import_file_root, - R.string.clash_import_file_empty_name, - Snackbar.LENGTH_LONG - ).show() - } - return - } + val name = elements[0] as FormAdapter.TextType + val file = elements[1] as FormAdapter.FilePickerType - if (file.content == null || file.content == Uri.EMPTY) { - runOnUiThread { - activity_import_file_save.visibility = View.VISIBLE - activity_import_file_saving.visibility = View.GONE - Snackbar.make( - activity_import_file_root, - R.string.clash_import_file_empty_path, - Snackbar.LENGTH_LONG - ).show() + if (name.content.isEmpty()) { + activity_import_file_save.visibility = View.VISIBLE + activity_import_file_saving.visibility = View.GONE + Snackbar.make( + activity_import_file_root, + R.string.clash_import_file_empty_name, + Snackbar.LENGTH_LONG + ).show() + return + } + + if (file.content == null || file.content == Uri.EMPTY) { + activity_import_file_save.visibility = View.VISIBLE + activity_import_file_saving.visibility = View.GONE + Snackbar.make( + activity_import_file_root, + R.string.clash_import_file_empty_path, + Snackbar.LENGTH_LONG + ).show() + return + } + + val data = + contentResolver.openInputStream(file.content!!)?.use { + it.readBytes().toString(Charsets.UTF_8) + } ?: throw NullPointerException("Unable to open config file") + + + runClash { + try { + val pipe = ParcelFileDescriptor.createPipe() + + thread { + FileOutputStream(pipe[1].fileDescriptor).use { + it.write(data.toByteArray()) + } + + pipe[0].close() + pipe[1].close() } - return - } - val data = - contentResolver.openInputStream(file.content!!)?.use { - it.readBytes().toString(Charsets.UTF_8) - } ?: throw NullPointerException("Unable to open config file") + val error = it.checkProfileValid(pipe[0]) - Yaml(configuration = YamlConfiguration(strictMode = false)).parse( - ClashProfile.serializer(), - data - ) - val cache = - FileUtils.generateRandomFile(filesDir.resolve(Constants.PROFILES_DIR), ".yaml") + if (error != null) + throw Exception(error) - FileOutputStream(cache).use { - it.write(data.toByteArray()) - } + val cache = + FileUtils.generateRandomFile(filesDir.resolve(Constants.PROFILES_DIR), ".yaml") + + FileOutputStream(cache).use { + it.write(data.toByteArray()) + } - runClash { it.profileService.addProfile( ClashProfileEntity( name = name.content, @@ -124,17 +130,25 @@ class ImportFileActivity : BaseActivity() { lastUpdate = System.currentTimeMillis() ) ) - } - setResult(Activity.RESULT_OK) + runOnUiThread { + setResult(Activity.RESULT_OK) - finish() - } catch (e: Exception) { - Snackbar.make( - activity_import_file_root, - getString(R.string.clash_profile_invalid, e.message?.replace(YamlException::class.java.name + ":", "") ?: "Unknown"), - Snackbar.LENGTH_LONG - ).show() + finish() + } + } catch (e: Exception) { + runOnUiThread { + Snackbar.make( + activity_import_file_root, + getString( + R.string.clash_profile_invalid, + e.message?.replace(YamlException::class.java.name + ":", "") + ?: "Unknown" + ), + Snackbar.LENGTH_LONG + ).show() + } + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt index 50072daa60..0f97d7dd1c 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt @@ -3,6 +3,7 @@ package com.github.kr328.clash import android.app.Activity import android.content.Intent import android.os.Bundle +import android.os.ParcelFileDescriptor import android.view.View import androidx.recyclerview.widget.LinearLayoutManager import com.charleskorn.kaml.Yaml @@ -89,15 +90,9 @@ class ImportUrlActivity : BaseActivity() { activity_import_url_saving.visibility = View.VISIBLE runClash { - val httpPort = it.queryGeneral().ports.randomHttp - thread { try { - val connection = if ( httpPort > 0 ) - URL(url.content).openConnection(Proxy(Proxy.Type.HTTP, - InetSocketAddress.createUnresolved("127.0.0.1", httpPort))) - else - URL(url.content).openConnection() + val connection = URL(url.content).openConnection() val data = with (connection) { connectTimeout = DEFAULT_TIMEOUT @@ -108,7 +103,21 @@ class ImportUrlActivity : BaseActivity() { } } - Yaml(configuration = YamlConfiguration(strictMode = false)).parse(ClashProfile.serializer(), data) + val pipe = ParcelFileDescriptor.createPipe() + + thread { + FileOutputStream(pipe[1].fileDescriptor).use { + it.write(data.toByteArray()) + } + + pipe[0].close() + pipe[1].close() + } + + val error = it.checkProfileValid(pipe[0]) + + if ( error != null ) + throw Exception(error) val cache = FileUtils.generateRandomFile(filesDir.resolve(Constants.PROFILES_DIR), ".yaml") diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 2a1983bb2a..7a21d7ba77 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -30,7 +30,7 @@ class MainApplication : Application() { FirebaseApp.initializeApp(this) } runCatching { - Fabric.with(this) + Fabric.with(this, Crashlytics()) } Log.handler = object: Log.LogHandler { diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 636e74b668..dbfe65b5e3 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -4,6 +4,7 @@ import android.app.AlertDialog import android.app.Dialog import android.content.Intent import android.os.Bundle +import android.os.ParcelFileDescriptor import android.view.View import android.widget.PopupMenu import androidx.recyclerview.widget.DiffUtil @@ -145,18 +146,9 @@ class ProfilesActivity : BaseActivity() { val url = ClashProfileEntity.getUrl(profile.token) runClash { - val httpPort = it.queryGeneral().ports.randomHttp - thread { try { - val connection = if ( httpPort > 0 ) - URL(url).openConnection( - Proxy( - Proxy.Type.HTTP, - InetSocketAddress.createUnresolved("127.0.0.1", httpPort)) - ) - else - URL(url).openConnection() + val connection = URL(url).openConnection() val data = with (connection) { connectTimeout = ImportUrlActivity.DEFAULT_TIMEOUT @@ -167,8 +159,21 @@ class ProfilesActivity : BaseActivity() { } } - Yaml(configuration = YamlConfiguration(strictMode = false)).parse( - ClashProfile.serializer(), data) + val pipe = ParcelFileDescriptor.createPipe() + + thread { + FileOutputStream(pipe[1].fileDescriptor).use { + it.write(data.toByteArray()) + } + + pipe[0].close() + pipe[1].close() + } + + val error = it.checkProfileValid(pipe[0]) + + if ( error != null ) + throw Exception(error) FileOutputStream(profile.file).use { outputStream -> outputStream.write(data.toByteArray()) diff --git a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt index ee599d9b62..8446a35400 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt @@ -4,7 +4,8 @@ import android.os.Bundle import com.github.kr328.clash.adapter.ProxyAdapter import com.github.kr328.clash.callback.IUrlTestCallback import com.github.kr328.clash.core.event.ErrorEvent -import com.github.kr328.clash.core.model.ProxyPacket +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.model.ListProxy import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_proxies.* @@ -76,58 +77,62 @@ class ProxyActivity : BaseActivity() { activity_proxies_swipe.isRefreshing = true runClash { clash -> - val packet = clash.queryAllProxies() - val proxies = packet.proxies - val order = (proxies["GLOBAL".hashCode()]?.all ?: emptyList()) - .mapIndexed { index, i -> i to index }.toMap() + val proxies = clash.queryAllProxies().uncompress().map { + it.name to it + }.toMap() + val general = clash.queryGeneral() + val order = (proxies["GLOBAL"]?.all ?: emptyList()) + .mapIndexed { index, name -> + name to index + }.toMap() val listData = proxies + .map { it.value } .asSequence() .filter { - when (it.value.type) { - ProxyPacket.Type.URL_TEST -> true - ProxyPacket.Type.FALLBACK -> true - ProxyPacket.Type.LOAD_BALANCE -> true - ProxyPacket.Type.SELECT -> true + when (it.type) { + Proxy.Type.URL_TEST -> true + Proxy.Type.FALLBACK -> true + Proxy.Type.LOAD_BALANCE -> true + Proxy.Type.SELECT -> true else -> false } } .filter { - when (packet.mode) { - "Global" -> it.value.name == "GLOBAL" - "Rule" -> it.value.name != "GLOBAL" - else -> false + when (general.mode) { + General.Mode.GLOBAL -> it.name == "GLOBAL" + General.Mode.RULE -> it.name != "GLOBAL" + else -> true } } .sortedWith(compareBy( { - it.value.type != ProxyPacket.Type.SELECT + it.type != Proxy.Type.SELECT }, { - order[it.key] ?: 0 + order[it.name] ?: Int.MAX_VALUE }) ) .flatMap { val header = - ListProxy.ListProxyHeader(it.value.name, it.value.type, it.value.now) + ListProxy.ListProxyHeader(it.name, it.type, it.now, 0) sequenceOf(header) + - it.value.all - .mapNotNull { - proxies[it] - } + it.all + .mapNotNull { name -> proxies[name] } .map { item -> ListProxy.ListProxyItem( item.name, item.type.toString(), - item.delay, header + item.delay, + header ) } .asSequence() } .mapIndexed { index, listProxy -> if (listProxy is ListProxy.ListProxyItem) { - if (listProxy.name.hashCode() == listProxy.header.now) - listProxy.header.now = index + if (listProxy.name == listProxy.header.now) + listProxy.header.nowIndex = index } listProxy } @@ -136,11 +141,11 @@ class ProxyActivity : BaseActivity() { val listDataOldChanged = (activity_proxies_list.adapter!! as ProxyAdapter) .elements .filterIsInstance(ListProxy.ListProxyHeader::class.java) - .map { it.now } + .map { it.nowIndex } val listDataChanged = listData .filterIsInstance() - .map { it.now } + .map { it.nowIndex } val changed = (if (listDataOldChanged.size != listDataChanged.size) (0..listData.size).toList() diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt index dadca9e40b..495a5e5d44 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt @@ -9,7 +9,7 @@ import android.widget.TextView import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.R -import com.github.kr328.clash.core.model.ProxyPacket +import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.model.ListProxy import com.google.android.material.card.MaterialCardView @@ -86,7 +86,7 @@ class ProxyAdapter(private val context: Context, } private fun bindItemView(holder: ItemHolder, current: ListProxy.ListProxyItem, position: Int) { - if (position == current.header.now) { + if (position == current.header.nowIndex) { holder.card.setCardBackgroundColor(context.getColor(R.color.colorAccent)) holder.name.setTextColor(Color.WHITE) holder.type.setTextColor(Color.WHITE - 0x22222222) @@ -98,14 +98,14 @@ class ProxyAdapter(private val context: Context, holder.delay.setTextColor(Color.DKGRAY) } - if (current.header.type == ProxyPacket.Type.SELECT) { + if (current.header.type == Proxy.Type.SELECT) { holder.card.isFocusable = true holder.card.isClickable = true holder.card.setOnClickListener { val element = (elements[position] as ListProxy.ListProxyItem) - val old = element.header.now - element.header.now = position + val old = element.header.nowIndex + element.header.nowIndex = position notifyItemChanged(old) notifyItemChanged(position) @@ -128,7 +128,7 @@ class ProxyAdapter(private val context: Context, current.delay.toString() } else -> { - if (current.header.type != ProxyPacket.Type.SELECT) + if (current.header.type != Proxy.Type.SELECT) "N/A" else "" @@ -139,7 +139,7 @@ class ProxyAdapter(private val context: Context, private fun bindHeaderView(holder: HeaderHolder, current: ListProxy.ListProxyHeader) { holder.name.text = current.name holder.test.visibility = - if (current.type == ProxyPacket.Type.SELECT) View.VISIBLE else View.GONE + if (current.type == Proxy.Type.SELECT) View.VISIBLE else View.GONE holder.test.setOnClickListener { if ( current.urlTest ) return@setOnClickListener diff --git a/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt b/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt index 50dd35d47c..5f751ee159 100644 --- a/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt +++ b/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt @@ -1,9 +1,9 @@ package com.github.kr328.clash.model -import com.github.kr328.clash.core.model.ProxyPacket +import com.github.kr328.clash.core.model.Proxy interface ListProxy { - data class ListProxyHeader(val name: String, val type: ProxyPacket.Type, var now: Int, var urlTest: Boolean = false) : + data class ListProxyHeader(val name: String, val type: Proxy.Type, val now: String, var nowIndex: Int, var urlTest: Boolean = false) : ListProxy data class ListProxyItem( diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go index 0298b251cd..1245beecad 100644 --- a/core/src/main/golang/bridge/profiles.go +++ b/core/src/main/golang/bridge/profiles.go @@ -9,3 +9,7 @@ func LoadProfileFile(path string) error { func LoadProfileDefault() { profile.LoadDefault() } + +func CheckProfileValid(profileData string) error { + return profile.CheckValid(profileData) +} diff --git a/core/src/main/golang/bridge/proxies.go b/core/src/main/golang/bridge/proxies.go index 7693d4b807..6d23a73054 100644 --- a/core/src/main/golang/bridge/proxies.go +++ b/core/src/main/golang/bridge/proxies.go @@ -1,8 +1,13 @@ package bridge import ( + "context" "encoding/json" + "time" + "github.com/Dreamacro/clash/adapters/outbound" + "github.com/Dreamacro/clash/adapters/outboundgroup" + "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" ) @@ -18,6 +23,10 @@ type ProxyList struct { proxies []*Proxy } +type UrlTestCallback interface { + OnResult(name string, delay int) +} + func (p Proxy) GetAllLength() int { return len(p.All) } @@ -61,3 +70,50 @@ func QueryAllProxies() (*ProxyList, error) { return &ProxyList{proxies: result}, nil } + +func SetSelectedProxy(name, proxy string) bool { + p := tunnel.Instance().Proxies()[name] + if p == nil { + return false + } + + pb, ok := p.(*outbound.Proxy) + if !ok { + return false + } + + selector, ok := pb.ProxyAdapter.(*outboundgroup.Selector) + if !ok { + return false + } + + if err := selector.Set(proxy); err != nil { + return false + } + + log.Infoln("Set " + name + " -> " + proxy) + + return true +} + +func StartUrlTest(name, url string, timeout int, callback UrlTestCallback) { + go func() { + p := tunnel.Instance().Proxies()[name] + + if p == nil { + callback.OnResult(name, -1) + return + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout)) + defer cancel() + + delay, err := p.URLTest(ctx, url) + if ctx.Err() != nil || err != nil { + callback.OnResult(name, -1) + return + } + + callback.OnResult(name, int(delay)) + }() +} diff --git a/core/src/main/golang/bridge/statistics.go b/core/src/main/golang/bridge/statistics.go new file mode 100644 index 0000000000..402a3ec77d --- /dev/null +++ b/core/src/main/golang/bridge/statistics.go @@ -0,0 +1,112 @@ +package bridge + +import ( + "time" + + "github.com/Dreamacro/clash/log" + "github.com/Dreamacro/clash/tunnel" +) + +type EventPoll struct { + onStop func() +} + +func (e *EventPoll) Stop() { + e.onStop() +} + +type Traffic interface { + OnEvent(down int64, up int64) +} + +type Bandwidth interface { + OnEvent(bandwidth int64) +} + +type Logs interface { + OnEvent(level, payload string) +} + +func PollTraffic(traffic Traffic) *EventPoll { + stopChannel := make(chan int, 1) + ticker := time.NewTicker(time.Second) + + go func() { + defer close(stopChannel) + defer log.Infoln("Traffic Poll Stopped") + + for { + select { + case <-stopChannel: + return + case <-ticker.C: + up, down := tunnel.DefaultManager.Now() + traffic.OnEvent(down, up) + } + } + }() + + return &EventPoll{ + onStop: func() { + stopChannel <- 0 + }, + } +} + +func PollBandwidth(bandwidth Bandwidth) *EventPoll { + stopChannel := make(chan int, 1) + ticker := time.NewTicker(time.Second) + + go func() { + defer close(stopChannel) + defer log.Infoln("Bandwidth Poll Stopped") + + for { + select { + case <-stopChannel: + return + case <-ticker.C: + s := tunnel.DefaultManager.Snapshot() + bandwidth.OnEvent(s.DownloadTotal + s.UploadTotal) + } + } + }() + + return &EventPoll{ + onStop: func() { + stopChannel <- 0 + }, + } +} + +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 + }, + } +} diff --git a/core/src/main/golang/go.mod b/core/src/main/golang/go.mod index f0053a4aa5..68e9756766 100644 --- a/core/src/main/golang/go.mod +++ b/core/src/main/golang/go.mod @@ -4,6 +4,7 @@ go 1.13 require ( github.com/Dreamacro/clash v0.0.0 // local + github.com/go-chi/render v1.0.1 github.com/google/go-cmp v0.3.1 // indirect golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d // indirect golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index 948f95d815..59c455410d 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -94,3 +94,8 @@ func LoadFromFile(path string) error { return nil } + +func CheckValid(data string) error { + _, err := config.Parse([]byte(data)) + return err +} diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 43b11c7b38..021fbd0824 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -2,9 +2,16 @@ package com.github.kr328.clash.core import android.content.Context import bridge.Bridge +import bridge.EventPoll +import bridge.Logs +import com.github.kr328.clash.core.event.BandwidthEvent +import com.github.kr328.clash.core.event.LogEvent import com.github.kr328.clash.core.event.ProcessEvent +import com.github.kr328.clash.core.event.TrafficEvent import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.Proxy +import java.io.FileOutputStream +import java.lang.Exception import java.lang.IllegalStateException class Clash( @@ -18,12 +25,28 @@ class Clash( const val DEFAULT_URL_TEST_URL = "https://www.gstatic.com/generate_204" } + class Poll(private val poll: EventPoll) { + fun stop() { + poll.stop() + } + } + private var currentProcess = ProcessEvent.STOPPED init { val home = context.filesDir.resolve(CLASH_DIR) + val countryDatabase = home.resolve("Country.mmdb") - home.mkdirs() + home.resolve("ui").mkdirs() + + if ( context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime > + countryDatabase.lastModified() ) { + FileOutputStream(countryDatabase).use { output -> + context.assets.open("Country.mmdb").use { input -> + input.copyTo(output) + } + } + } Bridge.init(home.absolutePath) } @@ -72,17 +95,27 @@ class Clash( Bridge.loadProfileFile(path) } + fun checkProfileValid(data: String): String? { + return try { + Bridge.checkProfileValid(data) + null + } + catch (e: Exception) { + e.message + } + } + fun queryProxies(): List { enforceStarted() val list = Bridge.queryAllProxies() val result = mutableListOf() - for (i in 0..list.proxiesLength) { + for (i in 0 until list.proxiesLength) { val p = list.getProxiesElement(i) val all = mutableListOf() - for (index in 0..p.allLength) { + for (index in 0 until p.allLength) { all.add(p.getAllElement(index)) } @@ -92,7 +125,7 @@ class Clash( Proxy.Type.fromString(p.type), p.now, all, - p.delay + if ( p.delay < Short.MAX_VALUE ) p.delay else 0 ) ) } @@ -100,6 +133,16 @@ class Clash( return result } + fun setSelectedProxy(name: String, selected: String): Boolean { + return Bridge.setSelectedProxy(name, selected) + } + + fun startUrlTest(name: String, callback: (String, Long) -> Unit) { + Bridge.startUrlTest(name, DEFAULT_URL_TEST_URL, DEFAULT_URL_TEST_TIMEOUT.toLong()) { n, delay -> + callback(n, delay) + } + } + fun queryGeneral(): General { val t = Bridge.queryGeneral() @@ -107,6 +150,24 @@ class Clash( t.httpPort.toInt(), t.socksPort.toInt(), t.redirectPort.toInt()) } + fun pollTraffic(onEvent: (TrafficEvent) -> Unit): Poll { + return Poll(Bridge.pollTraffic { down, up -> + onEvent(TrafficEvent(down, up)) + }) + } + + fun pollBandwidth(onEvent: (BandwidthEvent) -> Unit): Poll { + return Poll(Bridge.pollBandwidth { + onEvent(BandwidthEvent(it)) + }) + } + + fun pollLogs(onEvent: (LogEvent) -> Unit): Poll { + return Poll(Bridge.pollLogs { level, payload -> + onEvent(LogEvent(LogEvent.Level.fromString(level), payload)) + }) + } + private fun loadDefault() { Bridge.loadProfileDefault() } diff --git a/core/src/main/java/com/github/kr328/clash/core/event/Event.kt b/core/src/main/java/com/github/kr328/clash/core/event/Event.kt index fb30b9c0f0..06a284b829 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/Event.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/Event.kt @@ -3,7 +3,7 @@ package com.github.kr328.clash.core.event interface Event { companion object { const val EVENT_LOG = 1 - const val EVENT_SPEED = 3 + const val EVENT_TRAFFIC = 3 const val EVENT_BANDWIDTH = 4 } } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt index 1c21fab132..fe98e5e394 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt @@ -10,10 +10,11 @@ import kotlinx.serialization.internal.StringDescriptor data class LogEvent(val level: Level, val message: String, val time: Long = System.currentTimeMillis()) : Event, Parcelable { companion object { - const val DEBUG_VALUE = 1 - const val INFO_VALUE = 2 - const val WARN_VALUE = 3 - const val ERROR_VALUE = 4 + const val DEBUG_VALUE = "debug" + const val INFO_VALUE = "info" + const val WARN_VALUE = "warning" + const val ERROR_VALUE = "error" + const val UNKNOWN_VALUE = "unknown" @JvmField val CREATOR = object : Parcelable.Creator { @@ -27,33 +28,24 @@ data class LogEvent(val level: Level, val message: String, val time: Long = Syst } } - @Serializable(LevelSerializer::class) - enum class Level(val value: Int) { - DEBUG(DEBUG_VALUE), INFO( - INFO_VALUE - ), - WARN(WARN_VALUE), ERROR( - ERROR_VALUE - ) - } - - class LevelSerializer : KSerializer { - override val descriptor: SerialDescriptor - get() = StringDescriptor - - override fun deserialize(decoder: Decoder): Level { - return when (val value = decoder.decodeInt()) { - DEBUG_VALUE -> Level.DEBUG - INFO_VALUE -> Level.INFO - WARN_VALUE -> Level.WARN - ERROR_VALUE -> Level.ERROR - else -> throw IllegalArgumentException("Invalid level type $value") + enum class Level { + DEBUG, + INFO, + WARN, + ERROR, + UNKNOWN; + + companion object { + fun fromString(type: String): Level { + return when ( type ) { + DEBUG_VALUE -> DEBUG + INFO_VALUE -> INFO + WARN_VALUE -> WARN + ERROR_VALUE -> ERROR + else -> UNKNOWN + } } } - - override fun serialize(encoder: Encoder, obj: Level) { - encoder.encodeInt(obj.value) - } } override fun writeToParcel(parcel: Parcel, flags: Int) { diff --git a/core/src/main/java/com/github/kr328/clash/core/event/SpeedEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/TrafficEvent.kt similarity index 65% rename from core/src/main/java/com/github/kr328/clash/core/event/SpeedEvent.kt rename to core/src/main/java/com/github/kr328/clash/core/event/TrafficEvent.kt index ae0834d923..2086736881 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/SpeedEvent.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/TrafficEvent.kt @@ -6,7 +6,7 @@ import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable @Serializable -data class SpeedEvent(val down: Long, val up: Long) : Event, Parcelable { +data class TrafficEvent(val down: Long, val up: Long) : Event, Parcelable { override fun writeToParcel(parcel: Parcel, flags: Int) { Parcels.dump(serializer(), this, parcel) } @@ -15,12 +15,12 @@ data class SpeedEvent(val down: Long, val up: Long) : Event, Parcelable { return 0 } - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): SpeedEvent { + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): TrafficEvent { return Parcels.load(serializer(), parcel) } - override fun newArray(size: Int): Array { + override fun newArray(size: Int): Array { return arrayOfNulls(size) } } diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt b/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt index b411b7bc88..dd266fe206 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt @@ -3,6 +3,7 @@ package com.github.kr328.clash.core.model import android.os.Parcel import android.os.Parcelable import com.github.kr328.clash.core.serialization.Parcels +import com.github.kr328.clash.core.utils.Log import kotlinx.serialization.Serializable import java.lang.IllegalStateException @@ -13,9 +14,9 @@ fun List.compress(): CompressedProxyList { val elements = this.map { CompressedProxyList.Element( - name = nameMap[it.name] ?: throw IllegalStateException("Unknown proxy $it"), + name = nameMap[it.name] ?: throw IllegalStateException("Unknown proxy ${it.name}"), type = it.type, - now = nameMap[it.now] ?: throw IllegalStateException("Unknown proxy $it"), + now = nameMap[it.now] ?: -1, all = it.all.mapNotNull { name -> nameMap[name] }, delay = it.delay ) @@ -38,7 +39,7 @@ data class CompressedProxyList(val proxyName: Map, val elements: Li Proxy( name = proxyName[it.name] ?: throw IllegalStateException("Unknown proxy $it"), type = it.type, - now = proxyName[it.now] ?: throw IllegalStateException("Unknown proxy $it"), + now = proxyName[it.now] ?: "", all = it.all.mapNotNull { index -> proxyName[index] }, delay = it.delay ) diff --git a/service/build.gradle b/service/build.gradle index db1a326504..8c6c1d702a 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -15,7 +15,6 @@ android { versionCode 1 versionName "1.0" - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } diff --git a/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl b/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl index a3c8d8035a..dd25fe92b6 100644 --- a/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl @@ -2,7 +2,7 @@ package com.github.kr328.clash.core.event; parcelable ProcessEvent; parcelable LogEvent; -parcelable SpeedEvent; +parcelable TrafficEvent; parcelable BandwidthEvent; parcelable ErrorEvent; parcelable ProfileChangedEvent; diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl index a23fee9b10..dfbab90baa 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl @@ -6,7 +6,7 @@ interface IClashEventObserver { void onProcessEvent(in ProcessEvent event); void onLogEvent(in LogEvent event); void onErrorEvent(in ErrorEvent event); - void onSpeedEvent(in SpeedEvent event); + void onTrafficEvent(in TrafficEvent event); void onBandwidthEvent(in BandwidthEvent event); void onProfileChanged(in ProfileChangedEvent event); void onProfileReloaded(in ProfileReloadEvent event); 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 98166b3ce7..b8fd001dae 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 @@ -26,4 +26,7 @@ interface IClashService { CompressedProxyList queryAllProxies(); General queryGeneral(); void startUrlTest(in String[] proxies, IUrlTestCallback callback); + + // Utils + String checkProfileValid(in ParcelFileDescriptor pipe); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt index b9fd4ba5d6..49dbcc6087 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt @@ -1,10 +1,9 @@ package com.github.kr328.clash.service -import android.content.Context import com.github.kr328.clash.core.event.* class ClashEventBridge(val service: ClashService): ClashProfileService.Master, - ClashEventService.Master, ClashEventPuller.Master { + ClashEventService.Master, ClashEventPoll.Master { val eventService: ClashEventService = ClashEventService(this) override fun preformProfileChanged() { @@ -23,15 +22,15 @@ class ClashEventBridge(val service: ClashService): ClashProfileService.Master, eventService.performProcessEvent(event) } - override fun onLogPulled(event: LogEvent) { + override fun onLogEvent(event: LogEvent) { eventService.performLogEvent(event) } - override fun onSpeedPulled(event: SpeedEvent) { + override fun onTrafficEvent(event: TrafficEvent) { eventService.performSpeedEvent(event) } - override fun onBandwidthPulled(event: BandwidthEvent) { + override fun onBandwidthEvent(event: BandwidthEvent) { eventService.performBandwidthEvent(event) } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt new file mode 100644 index 0000000000..3744244aa6 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt @@ -0,0 +1,78 @@ +package com.github.kr328.clash.service + +import android.os.Handler +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.event.BandwidthEvent +import com.github.kr328.clash.core.event.LogEvent +import com.github.kr328.clash.core.event.TrafficEvent + +class ClashEventPoll(private val clash: Clash, private val master: Master) { + interface Master { + fun onLogEvent(event: LogEvent) + fun onTrafficEvent(event: TrafficEvent) + fun onBandwidthEvent(event: BandwidthEvent) + } + + private val handler = Handler() + + private var traffic: Clash.Poll? = null + private var bandwidth: Clash.Poll? = null + private var logs: Clash.Poll? = null + + fun startTrafficPoll() { + handler.post { + if ( traffic != null ) + return@post + + traffic = clash.pollTraffic { + master.onTrafficEvent(it) + } + } + } + + fun stopTrafficPoll() { + handler.post { + traffic?.stop() + + traffic = null + } + } + + fun startBandwidthPoll() { + handler.post { + if ( bandwidth != null ) + return@post + + bandwidth = clash.pollBandwidth { + master.onBandwidthEvent(it) + } + } + } + + fun stopBandwidthPoll() { + handler.post { + bandwidth?.stop() + + bandwidth = null + } + } + + fun startLogsPoll() { + handler.post { + if ( logs != null ) + return@post + + logs = clash.pollLogs { + master.onLogEvent(it) + } + } + } + + fun stopLogPoll() { + handler.post { + logs?.stop() + + logs = null + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventPuller.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventPuller.kt deleted file mode 100644 index ad35862f3e..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventPuller.kt +++ /dev/null @@ -1,134 +0,0 @@ -package com.github.kr328.clash.service - -import android.net.LocalSocket -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.core.event.BandwidthEvent -import com.github.kr328.clash.core.event.LogEvent -import com.github.kr328.clash.core.event.SpeedEvent -import com.github.kr328.clash.core.utils.Log -import kotlin.concurrent.thread - -class ClashEventPuller(private val clash: Clash, private val master: Master) { - interface Master { - fun onLogPulled(event: LogEvent) - fun onSpeedPulled(event: SpeedEvent) - fun onBandwidthPulled(event: BandwidthEvent) - } - - private var logSocket: LocalSocket? = null - private var speedSocket: LocalSocket? = null - private var bandwidthSocket: LocalSocket? = null - - fun startLogPuller() { - synchronized(LogEvent::class) { - if (logSocket != null) - return - } - - thread { - clash.pullLogsEvent({ - synchronized(LogEvent::class) { - if (logSocket != null) - it.close() - else - logSocket = it - - Log.i("log puller started") - } - }) { - master.onLogPulled(it) - } - - synchronized(LogEvent::class) { - Log.i("log puller stopped") - - logSocket = null - } - } - } - - fun startSpeedPull() { - synchronized(SpeedEvent::class) { - if (speedSocket != null) - return - } - - thread { - clash.pullSpeedEvent({ - synchronized(SpeedEvent::class) { - if (speedSocket != null) { - it.close() - return@pullSpeedEvent - } else { - speedSocket = it - Log.i("Speed puller started") - } - - } - }) { - master.onSpeedPulled(it) - } - - synchronized(SpeedEvent::class) { - Log.i("Speed puller stopped") - - speedSocket = null - } - } - } - - fun startBandwidthPull() { - synchronized(BandwidthEvent::class) { - if (bandwidthSocket != null) - return - } - - thread { - clash.pullBandwidthEvent({ - synchronized(BandwidthEvent::class) { - if (bandwidthSocket != null) { - it.close() - return@pullBandwidthEvent - } else { - bandwidthSocket = it - Log.i("Bandwidth puller started") - } - - } - }) { - master.onBandwidthPulled(it) - } - - synchronized(BandwidthEvent::class) { - Log.i("Bandwidth puller stopped") - - bandwidthSocket = null - } - } - } - - fun stopLogPull() { - synchronized(LogEvent::class) { - logSocket.closeSilent() - } - } - - fun stopSpeedPull() { - synchronized(SpeedEvent::class) { - speedSocket.closeSilent() - } - } - - fun stopBandwidthPull() { - synchronized(BandwidthEvent::class) { - bandwidthSocket.closeSilent() - } - } - - private fun LocalSocket?.closeSilent() { - runCatching { - this?.outputStream?.write(0) - this?.close() - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt index 87dd73c5c1..70e4a7ac8f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt @@ -11,7 +11,7 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub() companion object { private val EVENT_SET = - setOf(Event.EVENT_LOG, Event.EVENT_SPEED, Event.EVENT_BANDWIDTH) + setOf(Event.EVENT_LOG, Event.EVENT_TRAFFIC, Event.EVENT_BANDWIDTH) } private data class EventObserverRecord( @@ -43,11 +43,11 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub() } } - fun performSpeedEvent(event: SpeedEvent) { + fun performSpeedEvent(event: TrafficEvent) { handler.submit { observers.values.forEach { - if (it.acquiredEvent.contains(Event.EVENT_SPEED)) - it.observer.onSpeedEvent(event) + if (it.acquiredEvent.contains(Event.EVENT_TRAFFIC)) + it.observer.onTrafficEvent(event) } } } 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 3b45749320..1388a8e2d4 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 @@ -20,9 +20,9 @@ class ClashService : Service(), IClashEventObserver { ClashServiceImpl(this) } - val events: ClashEventService + private val events: ClashEventService get() = instance.eventService - val clash: Clash + private val clash: Clash get() = instance.clash //private lateinit var puller: ClashEventPuller @@ -35,7 +35,7 @@ class ClashService : Service(), IClashEventObserver { instance.eventService.registerEventObserver( ClashService::class.java.name, this@ClashService, - intArrayOf(Event.EVENT_SPEED) + intArrayOf(Event.EVENT_TRAFFIC) ) Intent.ACTION_SCREEN_OFF -> instance.eventService.registerEventObserver( @@ -55,27 +55,42 @@ class ClashService : Service(), IClashEventObserver { if ( instance.clash.getCurrentProcessStatus() == ProcessEvent.STOPPED ) return - + when ( event ) { + Event.EVENT_BANDWIDTH -> + instance.eventPoll.startBandwidthPoll() + Event.EVENT_TRAFFIC -> + instance.eventPoll.startTrafficPoll() + Event.EVENT_LOG -> + instance.eventPoll.startLogsPoll() + } } fun releaseEvent(event: Int) { if ( instance.clash.getCurrentProcessStatus() == ProcessEvent.STOPPED ) return - + when ( event ) { + Event.EVENT_BANDWIDTH -> + instance.eventPoll.stopBandwidthPoll() + Event.EVENT_TRAFFIC -> + instance.eventPoll.stopTrafficPoll() + Event.EVENT_LOG -> + instance.eventPoll.stopLogPoll() + } } override fun onCreate() { super.onCreate() - //puller = ClashEventPuller(clash, this) + // Init instance + instance notification = ClashNotification(this) instance.eventService.registerEventObserver( ClashService::class.java.name, this@ClashService, - intArrayOf(Event.EVENT_SPEED) + intArrayOf(Event.EVENT_TRAFFIC) ) registerReceiver(screenReceiver, IntentFilter().apply { @@ -99,6 +114,8 @@ class ClashService : Service(), IClashEventObserver { override fun onDestroy() { instance.clash.stop() + instance.shutdown() + executor.shutdown() unregisterReceiver(screenReceiver) @@ -120,7 +137,7 @@ class ClashService : Service(), IClashEventObserver { instance.eventService.recastEventRequirement() } ProcessEvent.STOPPED -> { - instance.eventService.performSpeedEvent(SpeedEvent(0, 0)) + instance.eventService.performSpeedEvent(TrafficEvent(0, 0)) instance.eventService.performBandwidthEvent(BandwidthEvent(0)) notification.cancel() @@ -140,7 +157,7 @@ class ClashService : Service(), IClashEventObserver { val active = instance.profileService.queryActiveProfile() if (active == null) { - sendError(ErrorEvent.Type.PROFILE_LOAD, "No profile activated") + events.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, "No active profile")) clash.stop() return@submit } @@ -150,12 +167,21 @@ class ClashService : Service(), IClashEventObserver { try { clash.loadProfile(active.file) + instance.profileService.queryProfileSelected(active.id).mapNotNull { + if ( clash.setSelectedProxy(it.key, it.value) ) + null + else + it.key + }.toList().apply { + instance.profileService.removeCurrentProfileProxy(this) + } + notification.setProfile(active.name) events.performProfileReloadEvent(ProfileReloadEvent()) } catch (e: Exception) { clash.stop() - sendError(ErrorEvent.Type.PROFILE_LOAD, e.message) + events.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, e.message ?: e.toString())) Log.w("Load profile failure", e) } } @@ -165,7 +191,7 @@ class ClashService : Service(), IClashEventObserver { sendBroadcast(Intent(Constants.CLASH_RELOAD_BROADCAST_ACTION).setPackage(packageName)) } - override fun onSpeedEvent(event: SpeedEvent?) { + override fun onTrafficEvent(event: TrafficEvent?) { notification.setSpeed(event?.up ?: 0, event?.down ?: 0) } @@ -177,8 +203,4 @@ class ClashService : Service(), IClashEventObserver { return this@ClashService } } - - private fun sendError(type: ErrorEvent.Type, message: String?) { - events.performErrorEvent(ErrorEvent(type, message ?: "Unknown")) - } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt index 33e7569544..eb53efb043 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash.service +import android.os.ParcelFileDescriptor import com.github.kr328.clash.callback.IUrlTestCallback import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.event.ErrorEvent @@ -8,14 +9,17 @@ import com.github.kr328.clash.core.model.CompressedProxyList import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.compress import com.github.kr328.clash.core.utils.Log +import java.io.FileInputStream import java.io.IOException +import java.util.concurrent.atomic.AtomicInteger -class ClashServiceImpl(val clashService: ClashService) : IClashService.Stub() { +class ClashServiceImpl(clashService: ClashService) : IClashService.Stub() { private val bridge: ClashEventBridge = ClashEventBridge(clashService) val clash: Clash = Clash(clashService, bridge::onProcessChanged) val profileService = ClashProfileService(clashService, bridge) val settingService = ClashSettingService(clashService) + val eventPoll = ClashEventPoll(clash, bridge) val eventService: ClashEventService get() = bridge.eventService @@ -23,7 +27,12 @@ class ClashServiceImpl(val clashService: ClashService) : IClashService.Stub() { require(proxy != null && selected != null) try { - profileService.setCurrentProfileProxy(proxy, selected) + if ( clash.setSelectedProxy(proxy, selected) ) + profileService.setCurrentProfileProxy(proxy, selected) + else + eventService.performErrorEvent( + ErrorEvent(ErrorEvent.Type.SET_PROXY_SELECTED, "Unable to set $proxy -> $selected") + ) } catch (e: IOException) { Log.w("Set proxy failure", e) @@ -52,6 +61,31 @@ class ClashServiceImpl(val clashService: ClashService) : IClashService.Stub() { override fun startUrlTest(proxies: Array?, callback: IUrlTestCallback?) { require(proxies != null && callback != null) + + val count = AtomicInteger(proxies.size) + + proxies.forEach { + clash.startUrlTest(it) { n, d -> + callback.onResult(n, d) + + count.getAndDecrement() + + if ( count.get() == 0 ) + callback.onResult(null, 0) + } + } + } + + override fun checkProfileValid(pipe: ParcelFileDescriptor?): String? { + require(pipe != null) + + val data = FileInputStream(pipe.fileDescriptor).use { + String(it.readBytes()) + } + + pipe.close() + + return clash.checkProfileValid(data) } override fun start() { @@ -78,10 +112,6 @@ class ClashServiceImpl(val clashService: ClashService) : IClashService.Stub() { return clash.getCurrentProcessStatus() } - fun getClashInstance(): Clash { - return clash - } - fun shutdown() { eventService.shutdown() } 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 f8fbc509f8..52ded3a95c 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 @@ -7,7 +7,6 @@ import android.content.Intent import android.content.ServiceConnection import android.net.VpnService import android.os.* -import bridge.Bridge import com.github.kr328.clash.core.event.* import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.net.DefaultNetworkObserver @@ -22,7 +21,7 @@ class TunService : VpnService(), IClashEventObserver { private var start = true private lateinit var fileDescriptor: ParcelFileDescriptor - private lateinit var clash: IClashService + private lateinit var clash: ClashServiceImpl private lateinit var defaultNetworkObserver: DefaultNetworkObserver private lateinit var settings: ClashSettingService private val connection = object : ServiceConnection { @@ -35,7 +34,7 @@ class TunService : VpnService(), IClashEventObserver { service ) ?: throw NullPointerException() - this@TunService.clash = clash + this@TunService.clash = (clash as ClashServiceImpl) start = true @@ -105,7 +104,7 @@ class TunService : VpnService(), IClashEventObserver { else stopSelf() - Bridge.stopTunDevice() + clash.clash.stopTunDevice() Log.i("STOPPED") } @@ -113,14 +112,14 @@ class TunService : VpnService(), IClashEventObserver { start = false if ( settings.isDnsHijackingEnabled ) { - Bridge.startTunDevice(fileDescriptor.fd.toLong(), VPN_MTU.toLong(), + clash.clash.startTunDevice(fileDescriptor.fd, VPN_MTU, "$PRIVATE_VLAN4_CLIENT/30", "0.0.0.0" ) { protect(it.toInt()) } } else { - Bridge.startTunDevice(fileDescriptor.fd.toLong(), VPN_MTU.toLong(), + clash.clash.startTunDevice(fileDescriptor.fd, VPN_MTU, "$PRIVATE_VLAN4_CLIENT/30", PRIVATE_VLAN_DNS ) { protect(it.toInt()) @@ -199,7 +198,7 @@ class TunService : VpnService(), IClashEventObserver { return this } - override fun onSpeedEvent(event: SpeedEvent?) {} + override fun onTrafficEvent(event: TrafficEvent?) {} override fun onBandwidthEvent(event: BandwidthEvent?) {} override fun onLogEvent(event: LogEvent?) {} override fun onErrorEvent(event: ErrorEvent?) {} diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index 10b3d54399..66bcd8d1e5 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -1,9 +1,10 @@ package com.github.kr328.clash.service.data -import android.net.Uri import android.os.Parcel import android.os.Parcelable -import androidx.room.* +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.PrimaryKey import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable From 62910f5e7ed4d0d2b8f6e40a9c9cdd36bb6e0e13 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 17 Dec 2019 13:51:32 +0800 Subject: [PATCH 046/358] try fix bugs & disable ipv6 for default dns --- app/build.gradle | 4 ++-- .../java/com/github/kr328/clash/ImportFileActivity.kt | 11 ++++++++--- .../java/com/github/kr328/clash/ImportUrlActivity.kt | 8 +++----- .../java/com/github/kr328/clash/ProfilesActivity.kt | 6 +++--- .../com/github/kr328/clash/SettingProxyActivity.kt | 4 ++-- app/src/main/res/xml/setting_proxy.xml | 4 +++- core/consumer-rules.pro | 6 ++++++ core/src/main/golang/profile/load.go | 2 +- 8 files changed, 28 insertions(+), 17 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 45507803b6..c3337efa7f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10018 - versionName "1.0.18-alpha" + versionCode 10019 + versionName "1.0.19-alpha" } buildTypes { release { diff --git a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt index 88e12ec83b..b4cbfc746d 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt @@ -104,13 +104,13 @@ class ImportFileActivity : BaseActivity() { FileOutputStream(pipe[1].fileDescriptor).use { it.write(data.toByteArray()) } - - pipe[0].close() - pipe[1].close() } val error = it.checkProfileValid(pipe[0]) + pipe[0].close() + pipe[1].close() + if (error != null) throw Exception(error) @@ -149,6 +149,11 @@ class ImportFileActivity : BaseActivity() { ).show() } } + + runOnUiThread { + activity_import_file_save.visibility = View.VISIBLE + activity_import_file_saving.visibility = View.GONE + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt index 0f97d7dd1c..c1647c6053 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt @@ -109,13 +109,13 @@ class ImportUrlActivity : BaseActivity() { FileOutputStream(pipe[1].fileDescriptor).use { it.write(data.toByteArray()) } - - pipe[0].close() - pipe[1].close() } val error = it.checkProfileValid(pipe[0]) + pipe[0].close() + pipe[1].close() + if ( error != null ) throw Exception(error) @@ -154,8 +154,6 @@ class ImportUrlActivity : BaseActivity() { } } } - - } catch (e: Exception) { diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index dbfe65b5e3..917593cc6b 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -165,13 +165,13 @@ class ProfilesActivity : BaseActivity() { FileOutputStream(pipe[1].fileDescriptor).use { it.write(data.toByteArray()) } - - pipe[0].close() - pipe[1].close() } val error = it.checkProfileValid(pipe[0]) + pipe[0].close() + pipe[1].close() + if ( error != null ) throw Exception(error) diff --git a/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt index 65d2d789d9..563fecb5bb 100644 --- a/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt @@ -28,7 +28,7 @@ class SettingProxyActivity : BaseActivity() { (requireActivity() as SettingProxyActivity).runClash { val settings = it.settingService - val ipv6 = settings.isIPv6Enabled + val ipv6 = false val privateNetwork = settings.isBypassPrivateNetwork val dnsHijacking = settings.isDnsHijackingEnabled @@ -48,7 +48,7 @@ class SettingProxyActivity : BaseActivity() { val settings = it.settingService settings.isIPv6Enabled = - findPreference(KEY_IPV6_SUPPORT)?.isChecked ?: true + findPreference(KEY_IPV6_SUPPORT)?.isChecked ?: false settings.isBypassPrivateNetwork = findPreference(KEY_BYPASS_PRIVATE_NETWORK)?.isChecked ?: true diff --git a/app/src/main/res/xml/setting_proxy.xml b/app/src/main/res/xml/setting_proxy.xml index 12efeb1aa6..2d3d0ddea9 100644 --- a/app/src/main/res/xml/setting_proxy.xml +++ b/app/src/main/res/xml/setting_proxy.xml @@ -7,8 +7,10 @@ android:summary="@string/proxy_setting_vpn_setting_bypass_private_network_summary" /> + android:visibility="gone" + android:enabled="false"/> Date: Tue, 17 Dec 2019 13:59:54 +0800 Subject: [PATCH 047/358] fix bugs --- app/build.gradle | 4 ++-- .../com/github/kr328/clash/MainActivity.kt | 2 +- core/src/main/golang/bridge/statistics.go | 20 +++++++++++++++---- core/src/main/golang/clash | 2 +- .../kr328/clash/core/utils/ByteFormatter.kt | 6 +++--- .../kr328/clash/service/ClashNotification.kt | 1 + 6 files changed, 24 insertions(+), 11 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index c3337efa7f..d3db07eccc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10019 - versionName "1.0.19-alpha" + versionCode 10020 + versionName "1.0.20-alpha" } buildTypes { release { diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 5e480fd0fe..42c8bac9ed 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -90,7 +90,7 @@ class MainActivity : BaseActivity() { activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_started) activity_main_clash_status_title.text = getString(R.string.clash_status_started) activity_main_clash_status_summary.text = - getString(R.string.clash_status_forwarded_traffic, "0 Bytes") + getString(R.string.clash_status_forwarded_traffic, ByteFormatter.byteToString(0)) activity_main_clash_proxies.visibility = View.VISIBLE activity_main_clash_logs.visibility = View.VISIBLE } diff --git a/core/src/main/golang/bridge/statistics.go b/core/src/main/golang/bridge/statistics.go index 402a3ec77d..f38ce271a6 100644 --- a/core/src/main/golang/bridge/statistics.go +++ b/core/src/main/golang/bridge/statistics.go @@ -31,6 +31,13 @@ func PollTraffic(traffic Traffic) *EventPoll { stopChannel := make(chan int, 1) ticker := time.NewTicker(time.Second) + tick := func() { + up, down := tunnel.DefaultManager.Now() + traffic.OnEvent(down, up) + } + + tick() + go func() { defer close(stopChannel) defer log.Infoln("Traffic Poll Stopped") @@ -40,8 +47,7 @@ func PollTraffic(traffic Traffic) *EventPoll { case <-stopChannel: return case <-ticker.C: - up, down := tunnel.DefaultManager.Now() - traffic.OnEvent(down, up) + tick() } } }() @@ -57,6 +63,13 @@ func PollBandwidth(bandwidth Bandwidth) *EventPoll { stopChannel := make(chan int, 1) ticker := time.NewTicker(time.Second) + tick := func() { + s := tunnel.DefaultManager.Snapshot() + bandwidth.OnEvent(s.DownloadTotal + s.UploadTotal) + } + + tick() + go func() { defer close(stopChannel) defer log.Infoln("Bandwidth Poll Stopped") @@ -66,8 +79,7 @@ func PollBandwidth(bandwidth Bandwidth) *EventPoll { case <-stopChannel: return case <-ticker.C: - s := tunnel.DefaultManager.Snapshot() - bandwidth.OnEvent(s.DownloadTotal + s.UploadTotal) + tick() } } }() diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index 78b0f8c718..b6166ab57d 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit 78b0f8c71846dd8cc800648a82eff8d40a6b3868 +Subproject commit b6166ab57d1e944003a00e541f54e40939935db3 diff --git a/core/src/main/java/com/github/kr328/clash/core/utils/ByteFormatter.kt b/core/src/main/java/com/github/kr328/clash/core/utils/ByteFormatter.kt index ec4879ff5e..bcb09ee62e 100644 --- a/core/src/main/java/com/github/kr328/clash/core/utils/ByteFormatter.kt +++ b/core/src/main/java/com/github/kr328/clash/core/utils/ByteFormatter.kt @@ -4,11 +4,11 @@ object ByteFormatter { fun byteToString(bytes: Long): String { return when { bytes > 1024 * 1024 * 1024 -> - String.format("%.2f GB", (bytes.toDouble() / 1024 / 1024 / 1024)) + String.format("%.2f GiB", (bytes.toDouble() / 1024 / 1024 / 1024)) bytes > 1024 * 1024 -> - String.format("%.2f MB", (bytes.toDouble() / 1024 / 1024)) + String.format("%.2f MiB", (bytes.toDouble() / 1024 / 1024)) bytes > 1024 -> - String.format("%.2f KB", (bytes.toDouble() / 1024)) + String.format("%.2f KiB", (bytes.toDouble() / 1024)) else -> "$bytes Bytes" } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 8618f7d2f0..3817f581f3 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -25,6 +25,7 @@ class ClashNotification(private val context: Service) { .setOngoing(true) .setColor(context.getColor(R.color.colorAccentService)) //.setColorized(true) + .setOnlyAlertOnce(true) .setShowWhen(false) .setContentIntent( PendingIntent.getActivity( From a31f864ab80062feb041ebeb191ea910a9056315 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 17 Dec 2019 14:25:11 +0800 Subject: [PATCH 048/358] fix bugs --- app/build.gradle | 4 ++-- .../main/java/com/github/kr328/clash/ImportFileActivity.kt | 3 ++- app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt | 3 ++- app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index d3db07eccc..204b1350d3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10020 - versionName "1.0.20-alpha" + versionCode 10021 + versionName "1.0.21-alpha" } buildTypes { release { diff --git a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt index b4cbfc746d..9948e40d92 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt @@ -104,12 +104,13 @@ class ImportFileActivity : BaseActivity() { FileOutputStream(pipe[1].fileDescriptor).use { it.write(data.toByteArray()) } + + pipe[1].close() } val error = it.checkProfileValid(pipe[0]) pipe[0].close() - pipe[1].close() if (error != null) throw Exception(error) diff --git a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt index c1647c6053..da238ff85a 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt @@ -109,12 +109,13 @@ class ImportUrlActivity : BaseActivity() { FileOutputStream(pipe[1].fileDescriptor).use { it.write(data.toByteArray()) } + + pipe[1].close() } val error = it.checkProfileValid(pipe[0]) pipe[0].close() - pipe[1].close() if ( error != null ) throw Exception(error) diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 917593cc6b..91ec84d077 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -165,12 +165,12 @@ class ProfilesActivity : BaseActivity() { FileOutputStream(pipe[1].fileDescriptor).use { it.write(data.toByteArray()) } + pipe[1].close() } val error = it.checkProfileValid(pipe[0]) pipe[0].close() - pipe[1].close() if ( error != null ) throw Exception(error) From 85072e45203ff8d8111883af13ba1ab876f16eed Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 19 Dec 2019 21:02:13 +0800 Subject: [PATCH 049/358] fix broadcast on sysstack --- app/build.gradle | 4 +-- .../github/kr328/clash/ClashStartService.kt | 2 ++ core/src/main/golang/tun/tun.go | 12 +++---- .../java/com/github/kr328/clash/core/Clash.kt | 9 ++++-- .../github/kr328/clash/service/TunService.kt | 8 +++-- service/src/main/res/values/arrays.xml | 31 +++++++++++++++++-- 6 files changed, 50 insertions(+), 16 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 204b1350d3..08b518416a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10021 - versionName "1.0.21-alpha" + versionCode 10025 + versionName "1.0.25-alpha" } buildTypes { release { diff --git a/app/src/main/java/com/github/kr328/clash/ClashStartService.kt b/app/src/main/java/com/github/kr328/clash/ClashStartService.kt index 6997842af4..092200e45e 100644 --- a/app/src/main/java/com/github/kr328/clash/ClashStartService.kt +++ b/app/src/main/java/com/github/kr328/clash/ClashStartService.kt @@ -40,6 +40,8 @@ class ClashStartService : Service() { startForeground(NOTIFICATION_ID, notification) thread { + Thread.sleep(5000) + ServiceUtils.startProxyService(this) stopSelf() diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go index 414fb1f54e..64a8b68731 100644 --- a/core/src/main/golang/tun/tun.go +++ b/core/src/main/golang/tun/tun.go @@ -5,7 +5,6 @@ import ( "strconv" "syscall" - "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/global" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/proxy/tun" @@ -16,9 +15,8 @@ type Callback interface { } var tunInstance *tun.TUN -var dnsGateway net.IP -func StartTunDevice(fd, mtu int, gateway string, dns string, callback Callback) error { +func StartTunDevice(fd, mtu int, gateway string, dnsGateway string, callback Callback) error { if tunInstance != nil { return nil } @@ -41,7 +39,9 @@ func StartTunDevice(fd, mtu int, gateway string, dns string, callback Callback) }) } - t, err := tun.NewTunProxy("fd://"+strconv.Itoa(fd)+"?mtu="+strconv.Itoa(mtu), *n) + dnsGatewayIP := net.ParseIP(dnsGateway) + + t, err := tun.NewTunProxy("fd://"+strconv.Itoa(fd)+"?mtu="+strconv.Itoa(mtu), *n, dnsGatewayIP) if err != nil { global.DefaultDialer.Control = nil @@ -51,8 +51,6 @@ func StartTunDevice(fd, mtu int, gateway string, dns string, callback Callback) } tunInstance = t - dnsGateway = net.ParseIP(dns) - ResetDnsRedirect() log.Infoln("Android tun started") @@ -79,5 +77,5 @@ func ResetDnsRedirect() { return } - tunInstance.ReCreateDNSServer(dns.DefaultResolver, dnsGateway) + tunInstance.ResetDnsServer() } diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 021fbd0824..2aa0d1e9a7 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -3,7 +3,6 @@ package com.github.kr328.clash.core import android.content.Context import bridge.Bridge import bridge.EventPoll -import bridge.Logs import com.github.kr328.clash.core.event.BandwidthEvent import com.github.kr328.clash.core.event.LogEvent import com.github.kr328.clash.core.event.ProcessEvent @@ -77,7 +76,13 @@ class Clash( loadDefault() } - fun startTunDevice(fd: Int, mtu: Int, gateway: String, dns: String, onSocket: (Int) -> Unit) { + fun startTunDevice( + fd: Int, + mtu: Int, + gateway: String, + dns: String, + onSocket: (Int) -> Unit + ) { enforceStarted() Bridge.startTunDevice(fd.toLong(), mtu.toLong(), gateway, dns) { 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 52ded3a95c..5f4a34812f 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 @@ -15,7 +15,7 @@ class TunService : VpnService(), IClashEventObserver { companion object { // from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/bg/VpnService.kt private const val VPN_MTU = 1500 - private const val PRIVATE_VLAN_DNS = "172.19.0.2" // sync with tun/tun.go/dnsServerAddress + private const val PRIVATE_VLAN_DNS = "172.19.0.2" private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1" } @@ -112,14 +112,16 @@ class TunService : VpnService(), IClashEventObserver { start = false if ( settings.isDnsHijackingEnabled ) { - clash.clash.startTunDevice(fileDescriptor.fd, VPN_MTU, + clash.clash.startTunDevice( + fileDescriptor.fd, VPN_MTU, "$PRIVATE_VLAN4_CLIENT/30", "0.0.0.0" ) { protect(it.toInt()) } } else { - clash.clash.startTunDevice(fileDescriptor.fd, VPN_MTU, + clash.clash.startTunDevice( + fileDescriptor.fd, VPN_MTU, "$PRIVATE_VLAN4_CLIENT/30", PRIVATE_VLAN_DNS ) { protect(it.toInt()) diff --git a/service/src/main/res/values/arrays.xml b/service/src/main/res/values/arrays.xml index 7777245026..67aaa821b5 100644 --- a/service/src/main/res/values/arrays.xml +++ b/service/src/main/res/values/arrays.xml @@ -18,7 +18,6 @@ 160.0.0.0/5 168.0.0.0/6 172.0.0.0/12 - 172.19.0.0/30 172.32.0.0/11 172.64.0.0/10 172.128.0.0/9 @@ -38,7 +37,35 @@ 196.0.0.0/6 200.0.0.0/5 208.0.0.0/4 - 224.0.0.0/3 + 224.0.0.0/4 + 240.0.0.0/5 + 248.0.0.0/6 + 252.0.0.0/7 + 254.0.0.0/8 + 255.0.0.0/9 + 255.128.0.0/10 + 255.192.0.0/11 + 255.224.0.0/12 + 255.240.0.0/13 + 255.248.0.0/14 + 255.252.0.0/15 + 255.254.0.0/16 + 255.255.0.0/17 + 255.255.128.0/18 + 255.255.192.0/19 + 255.255.224.0/20 + 255.255.240.0/21 + 255.255.248.0/22 + 255.255.252.0/23 + 255.255.254.0/24 + 255.255.255.0/25 + 255.255.255.128/26 + 255.255.255.192/27 + 255.255.255.224/28 + 255.255.255.240/29 + 255.255.255.248/30 + 255.255.255.252/31 + 255.255.255.254/32 From a49899add358fe155a1471329f22c724d6fbdc14 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 19 Dec 2019 21:03:00 +0800 Subject: [PATCH 050/358] update clash core --- core/src/main/golang/clash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index b6166ab57d..c00e907d95 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit b6166ab57d1e944003a00e541f54e40939935db3 +Subproject commit c00e907d953d71c4c2abf603c1c7dbf82287b4b3 From ed630363291426bfa9486bf6cfdbf85612175519 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 19 Dec 2019 21:44:15 +0800 Subject: [PATCH 051/358] fix crash on proxy only mode --- .../main/java/com/github/kr328/clash/service/ClashService.kt | 4 ++++ 1 file changed, 4 insertions(+) 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 1388a8e2d4..60b2073e04 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 @@ -12,6 +12,7 @@ import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.event.* import com.github.kr328.clash.core.utils.Log import java.util.concurrent.Executors +import kotlin.concurrent.thread class ClashService : Service(), IClashEventObserver { private val executor = Executors.newSingleThreadExecutor() @@ -102,6 +103,9 @@ class ClashService : Service(), IClashEventObserver { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) + notification.setVpn(false) + notification.show() + instance.clash.start() return START_NOT_STICKY From 1915b005a9ad434e1be467f8a97c1f780a799e6a Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 19 Dec 2019 21:52:55 +0800 Subject: [PATCH 052/358] change start activity flags --- .../java/com/github/kr328/clash/service/ClashNotification.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 3817f581f3..0483c442b2 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -37,7 +37,7 @@ class ClashNotification(private val context: Service) { MAIN_ACTIVITY_NAME ) ).setFlags( - Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP + Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK ), PendingIntent.FLAG_UPDATE_CURRENT ) From 166558433eff903969ef3fb405410def88924f72 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 19 Dec 2019 21:54:31 +0800 Subject: [PATCH 053/358] decrease start on boot delay --- app/build.gradle | 4 ++-- app/src/main/java/com/github/kr328/clash/ClashStartService.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 08b518416a..3d1cc4a4df 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10025 - versionName "1.0.25-alpha" + versionCode 10026 + versionName "1.0.26-alpha" } buildTypes { release { diff --git a/app/src/main/java/com/github/kr328/clash/ClashStartService.kt b/app/src/main/java/com/github/kr328/clash/ClashStartService.kt index 092200e45e..1a2325a33b 100644 --- a/app/src/main/java/com/github/kr328/clash/ClashStartService.kt +++ b/app/src/main/java/com/github/kr328/clash/ClashStartService.kt @@ -40,7 +40,7 @@ class ClashStartService : Service() { startForeground(NOTIFICATION_ID, notification) thread { - Thread.sleep(5000) + Thread.sleep(1000) ServiceUtils.startProxyService(this) From a287e287b9fcbba4a0ee716244d68e9b6786bee7 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 19 Dec 2019 22:35:11 +0800 Subject: [PATCH 054/358] fix tun --- app/build.gradle | 4 ++-- service/src/main/res/values/arrays.xml | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 3d1cc4a4df..7d9c0a7667 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10026 - versionName "1.0.26-alpha" + versionCode 10027 + versionName "1.0.27-alpha" } buildTypes { release { diff --git a/service/src/main/res/values/arrays.xml b/service/src/main/res/values/arrays.xml index 67aaa821b5..79f4af4183 100644 --- a/service/src/main/res/values/arrays.xml +++ b/service/src/main/res/values/arrays.xml @@ -18,6 +18,7 @@ 160.0.0.0/5 168.0.0.0/6 172.0.0.0/12 + 172.19.0.0/30 172.32.0.0/11 172.64.0.0/10 172.128.0.0/9 From 382f083fd1f82fe70e06c7af2f52c59ff96c9657 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Fri, 20 Dec 2019 10:51:55 +0800 Subject: [PATCH 055/358] fix crash on tile start reset clash bandwidth on start add version name to about dialog --- app/build.gradle | 4 +- .../com/github/kr328/clash/MainActivity.kt | 10 +- app/src/main/res/layout/dialog_about.xml | 2 +- core/src/main/golang/bridge/init.go | 9 ++ core/src/main/golang/bridge/profiles.go | 4 - core/src/main/golang/clash | 2 +- .../java/com/github/kr328/clash/core/Clash.kt | 8 +- .../kr328/clash/service/ClashEventPoll.kt | 6 + .../kr328/clash/service/ClashEventService.kt | 132 ++++++++++-------- .../kr328/clash/service/ClashServiceImpl.kt | 1 + .../github/kr328/clash/service/TunService.kt | 14 +- 11 files changed, 111 insertions(+), 81 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 7d9c0a7667..bf8fd5e0ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10027 - versionName "1.0.27-alpha" + versionCode 10028 + versionName "1.0.28-alpha" } buildTypes { release { diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 42c8bac9ed..1b504ea432 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -3,8 +3,12 @@ package com.github.kr328.clash import android.app.Activity import android.app.AlertDialog import android.content.Intent +import android.content.pm.PackageInfo import android.os.Bundle +import android.view.LayoutInflater import android.view.View +import android.view.ViewGroup +import android.widget.TextView import com.github.kr328.clash.core.event.* import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.utils.ByteFormatter @@ -194,7 +198,11 @@ class MainActivity : BaseActivity() { } private fun showAboutDialog() { - AlertDialog.Builder(this).setView(R.layout.dialog_about).show() + val view = LayoutInflater.from(this).inflate(R.layout.dialog_about, window.decorView as ViewGroup?, false) + + view.findViewById(android.R.id.summary).text = packageManager.getPackageInfo(packageName, 0).let(PackageInfo::versionName) + + AlertDialog.Builder(this).setView(view).show() } override fun onErrorEvent(event: ErrorEvent?) { diff --git a/app/src/main/res/layout/dialog_about.xml b/app/src/main/res/layout/dialog_about.xml index e2601be899..2db02def07 100644 --- a/app/src/main/res/layout/dialog_about.xml +++ b/app/src/main/res/layout/dialog_about.xml @@ -19,9 +19,9 @@ android:layout_height="wrap_content" android:layout_toEndOf="@android:id/icon" /> \ No newline at end of file diff --git a/core/src/main/golang/bridge/init.go b/core/src/main/golang/bridge/init.go index f7baf7dcce..e1fa614393 100644 --- a/core/src/main/golang/bridge/init.go +++ b/core/src/main/golang/bridge/init.go @@ -2,6 +2,15 @@ package bridge import "github.com/Dreamacro/clash/constant" +import "github.com/kr328/cfa/profile" + +import "github.com/Dreamacro/clash/tunnel" + func Init(home string) { constant.SetHomeDir(home) } + +func Reset() { + profile.LoadDefault() + tunnel.DefaultManager.Reset() +} diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go index 1245beecad..5decb71fd3 100644 --- a/core/src/main/golang/bridge/profiles.go +++ b/core/src/main/golang/bridge/profiles.go @@ -6,10 +6,6 @@ func LoadProfileFile(path string) error { return profile.LoadFromFile(path) } -func LoadProfileDefault() { - profile.LoadDefault() -} - func CheckProfileValid(profileData string) error { return profile.CheckValid(profileData) } diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index c00e907d95..c351009e6d 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit c00e907d953d71c4c2abf603c1c7dbf82287b4b3 +Subproject commit c351009e6d5b208650e13ed2f9492568740211ee diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 2aa0d1e9a7..d524e2935d 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -62,7 +62,7 @@ class Clash( listener(currentProcess) - loadDefault() + Bridge.reset() } fun stop() { @@ -73,7 +73,7 @@ class Clash( listener(currentProcess) - loadDefault() + Bridge.reset() } fun startTunDevice( @@ -173,10 +173,6 @@ class Clash( }) } - private fun loadDefault() { - Bridge.loadProfileDefault() - } - private fun enforceStarted() { if ( currentProcess == ProcessEvent.STOPPED ) throw IllegalStateException("Clash Stopped") diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt index 3744244aa6..0415b46f4f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt @@ -75,4 +75,10 @@ class ClashEventPoll(private val clash: Clash, private val master: Master) { logs = null } } + + fun shutdown() { + stopTrafficPoll() + stopBandwidthPoll() + stopLogPoll() + } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt index 70e4a7ac8f..782a0b7492 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt @@ -20,79 +20,87 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub() ) private val observers = mutableMapOf() - private val handler = Executors.newSingleThreadExecutor() + private val executor = Executors.newSingleThreadExecutor() private var currentProcessEvent = ProcessEvent.STOPPED fun performProcessEvent(event: ProcessEvent) { - handler.submit { - currentProcessEvent = event + if (!executor.isShutdown) + executor.submit { + currentProcessEvent = event - observers.values.forEach { - it.observer.onProcessEvent(event) + observers.values.forEach { + it.observer.onProcessEvent(event) + } } - } } fun performLogEvent(event: LogEvent) { - handler.submit { - observers.values.forEach { - if (it.acquiredEvent.contains(Event.EVENT_LOG)) - it.observer.onLogEvent(event) + if (!executor.isShutdown) + executor.submit { + observers.values.forEach { + if (it.acquiredEvent.contains(Event.EVENT_LOG)) + it.observer.onLogEvent(event) + } } - } } fun performSpeedEvent(event: TrafficEvent) { - handler.submit { - observers.values.forEach { - if (it.acquiredEvent.contains(Event.EVENT_TRAFFIC)) - it.observer.onTrafficEvent(event) + if (!executor.isShutdown) + executor.submit { + observers.values.forEach { + if (it.acquiredEvent.contains(Event.EVENT_TRAFFIC)) + it.observer.onTrafficEvent(event) + } } - } } fun performBandwidthEvent(event: BandwidthEvent) { - handler.submit { - observers.values.forEach { - if ( it.acquiredEvent.contains(Event.EVENT_BANDWIDTH) ) - it.observer.onBandwidthEvent(event) + if (!executor.isShutdown) + executor.submit { + observers.values.forEach { + if (it.acquiredEvent.contains(Event.EVENT_BANDWIDTH)) + it.observer.onBandwidthEvent(event) + } } - } } fun performErrorEvent(event: ErrorEvent) { - handler.submit { - observers.values.forEach { - it.observer.onErrorEvent(event) + if (!executor.isShutdown) + executor.submit { + observers.values.forEach { + it.observer.onErrorEvent(event) + } } - } } fun performProfileChangedEvent(event: ProfileChangedEvent) { - handler.submit { - observers.values.forEach { - it.observer.onProfileChanged(event) + if (!executor.isShutdown) + executor.submit { + observers.values.forEach { + it.observer.onProfileChanged(event) + } } - } } fun performProfileReloadEvent(event: ProfileReloadEvent) { - handler.submit { - observers.values.forEach { - it.observer.onProfileReloaded(event) + if (!executor.isShutdown) + executor.submit { + observers.values.forEach { + it.observer.onProfileReloaded(event) + } } - } } override fun unregisterEventObserver(id: String?) { - handler.submit { - require(id != null) + if (!executor.isShutdown) + executor.submit { + require(id != null) - observers.remove(id) + observers.remove(id) - recastEventRequirement() - } + recastEventRequirement() + } } override fun registerEventObserver( @@ -100,38 +108,40 @@ class ClashEventService(private val master: Master) : IClashEventService.Stub() observer: IClashEventObserver?, events: IntArray? ) { - handler.submit { - require(id != null && observer != null && events != null) + if (!executor.isShutdown) + executor.submit { + require(id != null && observer != null && events != null) - val initial = !observers.containsKey(id) + val initial = !observers.containsKey(id) - observers[id] = EventObserverRecord(observer, events.toSet()) + observers[id] = EventObserverRecord(observer, events.toSet()) - observer.asBinder().linkToDeath({ - unregisterEventObserver(id) - }, 0) + observer.asBinder().linkToDeath({ + unregisterEventObserver(id) + }, 0) - recastEventRequirement() + recastEventRequirement() - if (initial) { - observer.onProcessEvent(currentProcessEvent) + if (initial) { + observer.onProcessEvent(currentProcessEvent) + } } - } } - fun shutdown() { - handler.shutdown() + fun recastEventRequirement() { + if (!executor.isShutdown) + executor.submit { + val req = observers.values.flatMap { + it.acquiredEvent + }.toSet() + val rel = EVENT_SET - req + + req.forEach(master::acquireEvent) + rel.forEach(master::releaseEvent) + } } - fun recastEventRequirement() { - handler.submit { - val req = observers.values.flatMap { - it.acquiredEvent - }.toSet() - val rel = EVENT_SET - req - - req.forEach(master::acquireEvent) - rel.forEach(master::releaseEvent) - } + fun shutdown() { + executor.shutdown() } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt index eb53efb043..df3388232f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt @@ -113,6 +113,7 @@ class ClashServiceImpl(clashService: ClashService) : IClashService.Stub() { } fun shutdown() { + eventPoll.shutdown() eventService.shutdown() } } \ 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 5f4a34812f..9969fafbbf 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 @@ -10,13 +10,17 @@ 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 +import java.net.Inet4Address +import java.net.InetAddress class TunService : VpnService(), IClashEventObserver { companion object { // from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/bg/VpnService.kt private const val VPN_MTU = 1500 - private const val PRIVATE_VLAN_DNS = "172.19.0.2" - private const val PRIVATE_VLAN4_CLIENT = "172.19.0.1" + private const val PRIVATE_VLAN4_SUBNET = 30 + private const val PRIVATE_VLAN4_CLIENT = "172.255.255.253" + private const val PRIVATE_VLAN_DNS = "172.255.255.254" + private const val VLAN4_ANY = "0.0.0.0" } private var start = true @@ -114,7 +118,7 @@ class TunService : VpnService(), IClashEventObserver { if ( settings.isDnsHijackingEnabled ) { clash.clash.startTunDevice( fileDescriptor.fd, VPN_MTU, - "$PRIVATE_VLAN4_CLIENT/30", "0.0.0.0" + "$PRIVATE_VLAN4_CLIENT/$PRIVATE_VLAN4_SUBNET", VLAN4_ANY ) { protect(it.toInt()) } @@ -122,7 +126,7 @@ class TunService : VpnService(), IClashEventObserver { else { clash.clash.startTunDevice( fileDescriptor.fd, VPN_MTU, - "$PRIVATE_VLAN4_CLIENT/30", PRIVATE_VLAN_DNS + "$PRIVATE_VLAN4_CLIENT/$PRIVATE_VLAN4_SUBNET", PRIVATE_VLAN_DNS ) { protect(it.toInt()) } @@ -195,7 +199,7 @@ class TunService : VpnService(), IClashEventObserver { } private fun Builder.addAddress(): Builder { - addAddress(PRIVATE_VLAN4_CLIENT, 30) + addAddress(PRIVATE_VLAN4_CLIENT, PRIVATE_VLAN4_SUBNET) return this } From bc3f7d0549fb4ce455d6e381ab6333104c0482f7 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Fri, 20 Dec 2019 11:01:45 +0800 Subject: [PATCH 056/358] use 172.31.255.252/30 instead of 172.19.0.0/30 --- .../main/java/com/github/kr328/clash/service/TunService.kt | 4 ++-- service/src/main/res/values/arrays.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) 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 9969fafbbf..f4e5f4bb69 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 @@ -18,8 +18,8 @@ class TunService : VpnService(), IClashEventObserver { // from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/bg/VpnService.kt private const val VPN_MTU = 1500 private const val PRIVATE_VLAN4_SUBNET = 30 - private const val PRIVATE_VLAN4_CLIENT = "172.255.255.253" - private const val PRIVATE_VLAN_DNS = "172.255.255.254" + private const val PRIVATE_VLAN4_CLIENT = "172.31.255.253" + private const val PRIVATE_VLAN_DNS = "172.31.255.254" private const val VLAN4_ANY = "0.0.0.0" } diff --git a/service/src/main/res/values/arrays.xml b/service/src/main/res/values/arrays.xml index 79f4af4183..97ae10fcde 100644 --- a/service/src/main/res/values/arrays.xml +++ b/service/src/main/res/values/arrays.xml @@ -18,7 +18,6 @@ 160.0.0.0/5 168.0.0.0/6 172.0.0.0/12 - 172.19.0.0/30 172.32.0.0/11 172.64.0.0/10 172.128.0.0/9 @@ -67,6 +66,7 @@ 255.255.255.248/30 255.255.255.252/31 255.255.255.254/32 + 172.31.255.252/30 From 90456263ae256a65fdab47295d91de6584c8745f Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 21 Dec 2019 10:59:06 +0800 Subject: [PATCH 057/358] add google play & split apk detect --- .../com/github/kr328/clash/MainApplication.kt | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 7a21d7ba77..9561e2f2b2 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -8,12 +8,17 @@ import com.github.kr328.clash.core.utils.Log import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric + class MainApplication : Application() { companion object { const val KEY_PROXY_MODE = "key_proxy_mode" const val PROXY_MODE_VPN = "vpn" const val PROXY_MODE_PROXY_ONLY = "proxy_only" + val GOOGLE_PLAY_INSTALLER = listOf("com.android.vending", "com.google.android.feedback") + const val CRASHLYTICS_GOOGLE_PLAY_KEY = "install_from_google" + const val CRASHLYTICS_SPLIT_APK_KEY = "split_apk" + lateinit var instance: MainApplication } @@ -33,6 +38,9 @@ class MainApplication : Application() { Fabric.with(this, Crashlytics()) } + Crashlytics.setBool(CRASHLYTICS_GOOGLE_PLAY_KEY, detectFromPlay()) + Crashlytics.setBool(CRASHLYTICS_SPLIT_APK_KEY, detectSplitArchive()) + Log.handler = object: Log.LogHandler { override fun info(message: String, throwable: Throwable?) { android.util.Log.i(Constants.TAG, message, throwable) @@ -67,4 +75,14 @@ class MainApplication : Application() { } } } + + private fun detectFromPlay(): Boolean { + val installer = packageManager.getInstallerPackageName(packageName) + return installer != null && GOOGLE_PLAY_INSTALLER.contains(installer) + } + + private fun detectSplitArchive(): Boolean { + val split = applicationInfo.splitSourceDirs + return split != null && split.isNotEmpty() + } } \ No newline at end of file From 292ba5af95f58bdd3283b7442e8a7a8bd20608d7 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 21 Dec 2019 11:55:13 +0800 Subject: [PATCH 058/358] add feedback id (use Build.ID & PackageArchive.lastUpdateTime) --- .../github/kr328/clash/FeedbackActivity.kt | 22 ++++++++++++++++ .../com/github/kr328/clash/MainApplication.kt | 26 +++++++++++++++++-- app/src/main/res/values/strings.xml | 5 ++++ app/src/main/res/xml-zh/feedback.xml | 5 ++++ app/src/main/res/xml/feedback.xml | 5 ++++ 5 files changed, 61 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt b/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt index f23d1e1ff2..5f8ab06dbb 100644 --- a/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt @@ -1,15 +1,37 @@ package com.github.kr328.clash +import android.content.ClipData +import android.content.ClipboardManager import android.os.Bundle +import android.widget.Toast import androidx.annotation.Keep +import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import kotlinx.android.synthetic.main.activity_feedback.* class FeedbackActivity : BaseActivity() { + companion object { + const val KEY_FEEDBACK_ID = "feedback_id" + } + @Keep class Fragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.feedback, rootKey) + + findPreference(KEY_FEEDBACK_ID)?.apply { + summary = MainApplication.userIdentifier + + onPreferenceClickListener = Preference.OnPreferenceClickListener { p -> + val data = ClipData.newPlainText("userIdentifier", summary) + + requireContext().getSystemService(ClipboardManager::class.java)?.setPrimaryClip(data) + + Toast.makeText(requireContext(), R.string.feedback_feedback_id_copied, Toast.LENGTH_LONG).show() + + true + } + } } } diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 9561e2f2b2..6bbb678ba3 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,11 +2,14 @@ package com.github.kr328.clash import android.app.Application import android.content.Context +import android.os.Build import com.crashlytics.android.Crashlytics import com.github.kr328.clash.core.Constants import com.github.kr328.clash.core.utils.Log import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric +import java.security.MessageDigest +import kotlin.experimental.and class MainApplication : Application() { @@ -16,10 +19,28 @@ class MainApplication : Application() { const val PROXY_MODE_PROXY_ONLY = "proxy_only" val GOOGLE_PLAY_INSTALLER = listOf("com.android.vending", "com.google.android.feedback") - const val CRASHLYTICS_GOOGLE_PLAY_KEY = "install_from_google" + const val CRASHLYTICS_FROM_PLAY_KEY = "install_from_google" const val CRASHLYTICS_SPLIT_APK_KEY = "split_apk" + val userIdentifier: String by lazy { + val archive = instance.packageManager.getPackageInfo(instance.packageName, 0) + val encoder = MessageDigest.getInstance("md5") + + encoder.digest((Build.ID + archive.lastUpdateTime).toByteArray()).toHexString() + } + lateinit var instance: MainApplication + + private fun ByteArray.toHexString(): String { + return this.map { + Integer.toHexString(it.toInt() and 0xff) + }.map { + if ( it.length < 2 ) + "0$it" + else + it + }.joinToString(separator = "") + } } override fun attachBaseContext(base: Context?) { @@ -38,8 +59,9 @@ class MainApplication : Application() { Fabric.with(this, Crashlytics()) } - Crashlytics.setBool(CRASHLYTICS_GOOGLE_PLAY_KEY, detectFromPlay()) + Crashlytics.setBool(CRASHLYTICS_FROM_PLAY_KEY, detectFromPlay()) Crashlytics.setBool(CRASHLYTICS_SPLIT_APK_KEY, detectSplitArchive()) + Crashlytics.setUserIdentifier(userIdentifier) Log.handler = object: Log.LogHandler { override fun info(message: String, throwable: Throwable?) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e780640e8..63624bbb8e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -94,6 +94,11 @@ Not Implemented + ID + Feedback ID + Unknown + Copied + Source & Issues Upstream Github diff --git a/app/src/main/res/xml-zh/feedback.xml b/app/src/main/res/xml-zh/feedback.xml index 8a0d4396fa..78428e92f4 100644 --- a/app/src/main/res/xml-zh/feedback.xml +++ b/app/src/main/res/xml-zh/feedback.xml @@ -1,5 +1,10 @@ + + + diff --git a/app/src/main/res/xml/feedback.xml b/app/src/main/res/xml/feedback.xml index b76a61b516..8e4ba50074 100644 --- a/app/src/main/res/xml/feedback.xml +++ b/app/src/main/res/xml/feedback.xml @@ -1,5 +1,10 @@ + + + From a4151fb198e2ff7bffc4286c4277965ee25589df Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 21 Dec 2019 11:56:02 +0800 Subject: [PATCH 059/358] cleanup code & update version code --- app/build.gradle | 4 +- .../kr328/clash/BootCompleteReceiver.kt | 1 - .../kr328/clash/CreateProfileActivity.kt | 2 +- .../github/kr328/clash/FeedbackActivity.kt | 11 +++-- .../github/kr328/clash/ImportUrlActivity.kt | 49 ++++++++++--------- .../com/github/kr328/clash/LogActivity.kt | 20 +++++--- .../com/github/kr328/clash/MainActivity.kt | 13 +++-- .../com/github/kr328/clash/MainApplication.kt | 5 +- .../github/kr328/clash/ProfilesActivity.kt | 25 +++++----- .../com/github/kr328/clash/ProxyActivity.kt | 11 +++-- .../kr328/clash/SettingApplicationActivity.kt | 5 +- .../com/github/kr328/clash/TileService.kt | 13 ++--- .../github/kr328/clash/adapter/FormAdapter.kt | 7 ++- .../github/kr328/clash/adapter/LogAdapter.kt | 12 +++-- .../kr328/clash/adapter/ProfileAdapter.kt | 35 +++++++------ .../kr328/clash/adapter/ProxyAdapter.kt | 10 ++-- .../com/github/kr328/clash/model/ClashRule.kt | 20 ++++++-- .../com/github/kr328/clash/model/ListProxy.kt | 8 ++- .../github/kr328/clash/utils/ServiceUtils.kt | 2 +- .../kr328/clash/view/MarqueeTextView.kt | 2 +- 20 files changed, 154 insertions(+), 101 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index bf8fd5e0ee..11e1a0e127 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10028 - versionName "1.0.28-alpha" + versionCode 10029 + versionName "1.0.29-alpha" } buildTypes { release { diff --git a/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt b/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt index 7c666817c2..798c66d103 100644 --- a/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt +++ b/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt @@ -3,7 +3,6 @@ package com.github.kr328.clash import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.os.Build import com.github.kr328.clash.utils.ServiceUtils class BootCompleteReceiver : BroadcastReceiver() { diff --git a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt index 13300886eb..a29e5a618e 100644 --- a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt @@ -54,7 +54,7 @@ class CreateProfileActivity : BaseActivity() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if ( requestCode == IMPORT_REQUEST_CODE && resultCode == Activity.RESULT_OK ) { + if (requestCode == IMPORT_REQUEST_CODE && resultCode == Activity.RESULT_OK) { finish() return } diff --git a/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt b/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt index 5f8ab06dbb..4b0119276c 100644 --- a/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt @@ -25,9 +25,14 @@ class FeedbackActivity : BaseActivity() { onPreferenceClickListener = Preference.OnPreferenceClickListener { p -> val data = ClipData.newPlainText("userIdentifier", summary) - requireContext().getSystemService(ClipboardManager::class.java)?.setPrimaryClip(data) - - Toast.makeText(requireContext(), R.string.feedback_feedback_id_copied, Toast.LENGTH_LONG).show() + requireContext().getSystemService(ClipboardManager::class.java) + ?.setPrimaryClip(data) + + Toast.makeText( + requireContext(), + R.string.feedback_feedback_id_copied, + Toast.LENGTH_LONG + ).show() true } diff --git a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt index da238ff85a..517429c565 100644 --- a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt @@ -6,18 +6,13 @@ import android.os.Bundle import android.os.ParcelFileDescriptor import android.view.View import androidx.recyclerview.widget.LinearLayoutManager -import com.charleskorn.kaml.Yaml -import com.charleskorn.kaml.YamlConfiguration import com.charleskorn.kaml.YamlException import com.github.kr328.clash.adapter.FormAdapter -import com.github.kr328.clash.model.ClashProfile import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.utils.FileUtils import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_import_url.* import java.io.FileOutputStream -import java.net.InetSocketAddress -import java.net.Proxy import java.net.URL import kotlin.concurrent.thread @@ -54,9 +49,10 @@ class ImportUrlActivity : BaseActivity() { checkAndInsert() } - if ( intent.action == Intent.ACTION_VIEW + if (intent.action == Intent.ACTION_VIEW && intent.data?.scheme == "clash" - && intent.data?.host == "install-config") { + && intent.data?.host == "install-config" + ) { (elements[1] as FormAdapter.TextType).content = intent.data?.getQueryParameter("url") ?: "" } @@ -78,11 +74,11 @@ class ImportUrlActivity : BaseActivity() { val name = elements[0] as FormAdapter.TextType val url = elements[1] as FormAdapter.TextType - if ( name.content.isBlank() ) { + if (name.content.isBlank()) { return } - if ( url.content.isBlank() ) { + if (url.content.isBlank()) { return } @@ -94,7 +90,7 @@ class ImportUrlActivity : BaseActivity() { try { val connection = URL(url.content).openConnection() - val data = with (connection) { + val data = with(connection) { connectTimeout = DEFAULT_TIMEOUT connect() @@ -117,22 +113,29 @@ class ImportUrlActivity : BaseActivity() { pipe[0].close() - if ( error != null ) + if (error != null) throw Exception(error) val cache = - FileUtils.generateRandomFile(filesDir.resolve(Constants.PROFILES_DIR), ".yaml") + FileUtils.generateRandomFile( + filesDir.resolve(Constants.PROFILES_DIR), + ".yaml" + ) FileOutputStream(cache).use { outputStream -> outputStream.write(data.toByteArray()) } runClash { clash -> - clash.profileService.addProfile(ClashProfileEntity(name.content, - ClashProfileEntity.urlToken(url.content), - cache.absolutePath, - false, - System.currentTimeMillis())) + clash.profileService.addProfile( + ClashProfileEntity( + name.content, + ClashProfileEntity.urlToken(url.content), + cache.absolutePath, + false, + System.currentTimeMillis() + ) + ) } runOnUiThread { @@ -140,12 +143,15 @@ class ImportUrlActivity : BaseActivity() { finish() } - } - catch (e: Exception) { + } catch (e: Exception) { runOnUiThread { Snackbar.make( activity_import_url_root, - getString(R.string.clash_profile_invalid, e.message?.replace(YamlException::class.java.name + ":", "") ?: "Unknown"), + getString( + R.string.clash_profile_invalid, + e.message?.replace(YamlException::class.java.name + ":", "") + ?: "Unknown" + ), Snackbar.LENGTH_LONG ).show() @@ -155,8 +161,7 @@ class ImportUrlActivity : BaseActivity() { } } } - } - catch (e: Exception) { + } catch (e: Exception) { } } diff --git a/app/src/main/java/com/github/kr328/clash/LogActivity.kt b/app/src/main/java/com/github/kr328/clash/LogActivity.kt index ff339ee7d8..45036f8b2d 100644 --- a/app/src/main/java/com/github/kr328/clash/LogActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/LogActivity.kt @@ -46,9 +46,11 @@ class LogActivity : BaseActivity() { activity_logs_list.adapter?.notifyDataSetChanged() runClash { - it.eventService.registerEventObserver(LogActivity::class.java.name, + it.eventService.registerEventObserver( + LogActivity::class.java.name, this, - intArrayOf(Event.EVENT_LOG)) + intArrayOf(Event.EVENT_LOG) + ) } } @@ -70,18 +72,20 @@ class LogActivity : BaseActivity() { handler.post { buffer.addFirst(event) - if ( syncLog ) { + if (syncLog) { activity_logs_list.adapter!!.notifyItemInserted(0) - if ( activity_logs_list.computeVerticalScrollOffset() < 30 ) + if (activity_logs_list.computeVerticalScrollOffset() < 30) activity_logs_list.scrollToPosition(0) } - if ( buffer.size() >= MAX_EVENT_COUNT ) { + if (buffer.size() >= MAX_EVENT_COUNT) { buffer.removeFromEnd(buffer.size() - MAX_EVENT_COUNT - 1) - if ( syncLog ) { - activity_logs_list.adapter?.notifyItemRangeRemoved(MAX_EVENT_COUNT - 1, - buffer.size() - MAX_EVENT_COUNT - 1 ) + if (syncLog) { + activity_logs_list.adapter?.notifyItemRangeRemoved( + MAX_EVENT_COUNT - 1, + buffer.size() - MAX_EVENT_COUNT - 1 + ) } } } diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 1b504ea432..28e8cb9e90 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -94,7 +94,10 @@ class MainActivity : BaseActivity() { activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_started) activity_main_clash_status_title.text = getString(R.string.clash_status_started) activity_main_clash_status_summary.text = - getString(R.string.clash_status_forwarded_traffic, ByteFormatter.byteToString(0)) + getString( + R.string.clash_status_forwarded_traffic, + ByteFormatter.byteToString(0) + ) activity_main_clash_proxies.visibility = View.VISIBLE activity_main_clash_logs.visibility = View.VISIBLE } @@ -116,7 +119,7 @@ class MainActivity : BaseActivity() { val general = it.queryGeneral() runOnUiThread { - when ( general.mode ) { + when (general.mode) { General.Mode.DIRECT -> activity_main_clash_proxies_summary.text = getText(R.string.clash_proxy_manage_summary_direct) @@ -198,9 +201,11 @@ class MainActivity : BaseActivity() { } private fun showAboutDialog() { - val view = LayoutInflater.from(this).inflate(R.layout.dialog_about, window.decorView as ViewGroup?, false) + val view = LayoutInflater.from(this) + .inflate(R.layout.dialog_about, window.decorView as ViewGroup?, false) - view.findViewById(android.R.id.summary).text = packageManager.getPackageInfo(packageName, 0).let(PackageInfo::versionName) + view.findViewById(android.R.id.summary).text = + packageManager.getPackageInfo(packageName, 0).let(PackageInfo::versionName) AlertDialog.Builder(this).setView(view).show() } diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 6bbb678ba3..0ef3d223c3 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -9,7 +9,6 @@ import com.github.kr328.clash.core.utils.Log import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric import java.security.MessageDigest -import kotlin.experimental.and class MainApplication : Application() { @@ -35,7 +34,7 @@ class MainApplication : Application() { return this.map { Integer.toHexString(it.toInt() and 0xff) }.map { - if ( it.length < 2 ) + if (it.length < 2) "0$it" else it @@ -63,7 +62,7 @@ class MainApplication : Application() { Crashlytics.setBool(CRASHLYTICS_SPLIT_APK_KEY, detectSplitArchive()) Crashlytics.setUserIdentifier(userIdentifier) - Log.handler = object: Log.LogHandler { + Log.handler = object : Log.LogHandler { override fun info(message: String, throwable: Throwable?) { android.util.Log.i(Constants.TAG, message, throwable) } diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 91ec84d077..224a07bc95 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -9,19 +9,14 @@ import android.view.View import android.widget.PopupMenu import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager -import com.charleskorn.kaml.Yaml -import com.charleskorn.kaml.YamlConfiguration import com.github.kr328.clash.adapter.ProfileAdapter import com.github.kr328.clash.core.event.ErrorEvent import com.github.kr328.clash.core.event.ProfileChangedEvent -import com.github.kr328.clash.model.ClashProfile import com.github.kr328.clash.service.data.ClashProfileEntity import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_profiles.* import java.io.File import java.io.FileOutputStream -import java.net.InetSocketAddress -import java.net.Proxy import java.net.URL import kotlin.concurrent.thread @@ -35,10 +30,12 @@ class ProfilesActivity : BaseActivity() { setSupportActionBar(activity_profiles_toolbar) activity_profiles_main_list.layoutManager = LinearLayoutManager(this) - activity_profiles_main_list.adapter = ProfileAdapter(this, + activity_profiles_main_list.adapter = ProfileAdapter( + this, this::onProfileClick, this::onOperateClick, - this::onProfileLongClick) { + this::onProfileLongClick + ) { startActivity(Intent(this, CreateProfileActivity::class.java)) } } @@ -114,7 +111,7 @@ class ProfilesActivity : BaseActivity() { .setCancelable(false) .show() - updateProfile(profile) + updateProfile(profile) } ClashProfileEntity.isFileToken(profile.token) -> { Snackbar.make( @@ -150,7 +147,7 @@ class ProfilesActivity : BaseActivity() { try { val connection = URL(url).openConnection() - val data = with (connection) { + val data = with(connection) { connectTimeout = ImportUrlActivity.DEFAULT_TIMEOUT connect() @@ -172,7 +169,7 @@ class ProfilesActivity : BaseActivity() { pipe[0].close() - if ( error != null ) + if (error != null) throw Exception(error) FileOutputStream(profile.file).use { outputStream -> @@ -182,8 +179,7 @@ class ProfilesActivity : BaseActivity() { runClash { clash -> clash.profileService.touchProfile(profile.id) } - } - catch (e: Exception) { + } catch (e: Exception) { runOnUiThread { Snackbar.make( activity_profiles_root, @@ -194,7 +190,7 @@ class ProfilesActivity : BaseActivity() { } runOnUiThread { - if ( dialog?.isShowing == true ) + if (dialog?.isShowing == true) dialog?.dismiss() } } @@ -202,6 +198,7 @@ class ProfilesActivity : BaseActivity() { } override fun onErrorEvent(event: ErrorEvent?) { - Snackbar.make(activity_profiles_root, event?.message ?: "Unknown", Snackbar.LENGTH_LONG).show() + Snackbar.make(activity_profiles_root, event?.message ?: "Unknown", Snackbar.LENGTH_LONG) + .show() } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt index 8446a35400..ff9fcab20a 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt @@ -39,18 +39,18 @@ class ProxyActivity : BaseActivity() { val adapter = (activity_proxies_list.adapter as ProxyAdapter) runClash { - val proxies = adapter.elements.subList(position, position+size) + val proxies = adapter.elements.subList(position, position + size) .filterIsInstance() - .mapIndexed { index, proxy -> proxy.name to IndexedValue(index, proxy)} + .mapIndexed { index, proxy -> proxy.name to IndexedValue(index, proxy) } .toMap() for ((_, p) in proxies) { p.value.delay = -1 } - it.startUrlTest(proxies.keys.toTypedArray(), object: IUrlTestCallback.Stub() { + it.startUrlTest(proxies.keys.toTypedArray(), object : IUrlTestCallback.Stub() { override fun onResult(proxy: String?, delay: Long) { - if ( proxy == null ) { + if (proxy == null) { (adapter.elements[position] as ListProxy.ListProxyHeader).urlTest = false runOnUiThread { @@ -177,6 +177,7 @@ class ProxyActivity : BaseActivity() { } override fun onErrorEvent(event: ErrorEvent?) { - Snackbar.make(activity_proxies_root, event?.message ?: "Unknown", Snackbar.LENGTH_LONG).show() + Snackbar.make(activity_proxies_root, event?.message ?: "Unknown", Snackbar.LENGTH_LONG) + .show() } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt index c127a10222..7ca62e03ee 100644 --- a/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt @@ -50,7 +50,7 @@ class SettingApplicationActivity : BaseActivity() { } override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { - if ( !isAdded ) + if (!isAdded) return false when (preference?.key) { @@ -60,8 +60,7 @@ class SettingApplicationActivity : BaseActivity() { thread { try { setBootCompleteReceiverEnabled(enabled) - } - catch (e: Exception) { + } catch (e: Exception) { Log.w("Set boot complete failure", e) } } diff --git a/app/src/main/java/com/github/kr328/clash/TileService.kt b/app/src/main/java/com/github/kr328/clash/TileService.kt index 3ec689c4cc..4fb81921d1 100644 --- a/app/src/main/java/com/github/kr328/clash/TileService.kt +++ b/app/src/main/java/com/github/kr328/clash/TileService.kt @@ -7,10 +7,8 @@ import android.content.IntentFilter import android.service.quicksettings.Tile import android.service.quicksettings.TileService import com.github.kr328.clash.core.event.ProcessEvent -import com.github.kr328.clash.core.event.ProfileReloadEvent import com.github.kr328.clash.service.ClashService import com.github.kr328.clash.service.Constants -import com.github.kr328.clash.service.IClashEventObserver import com.github.kr328.clash.service.IClashService import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.utils.ServiceUtils @@ -41,7 +39,7 @@ class TileService : TileService() { } private fun refreshStatus() { - if ( qsTile == null ) + if (qsTile == null) return val current = getCurrentStatus() @@ -86,11 +84,14 @@ class TileService : TileService() { private fun getCurrentStatus(): Pair { val service = - IClashService.Stub.asInterface(clashStatusReceiver - .peekService(this, Intent(this, ClashService::class.java))) + IClashService.Stub.asInterface( + clashStatusReceiver + .peekService(this, Intent(this, ClashService::class.java)) + ) return runCatching { - (service?.currentProcessStatus ?: ProcessEvent.STOPPED) to service?.profileService?.queryActiveProfile() + (service?.currentProcessStatus + ?: ProcessEvent.STOPPED) to service?.profileService?.queryActiveProfile() }.getOrNull() ?: ProcessEvent.STOPPED to null } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt index f8d9e34121..50fc57b244 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt @@ -127,7 +127,12 @@ class FormAdapter( return elements[position].javaClass.hashCode() } - private fun showTextEditDialog(initial: String, title: String, hint: String, callback: (String) -> Unit) { + private fun showTextEditDialog( + initial: String, + title: String, + hint: String, + callback: (String) -> Unit + ) { MaterialAlertDialogBuilder(activity) .setTitle(title) .setView(R.layout.dialog_text_edit) diff --git a/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt index d728a19b56..f5fbd1ab8c 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt @@ -12,8 +12,10 @@ import com.github.kr328.clash.core.event.LogEvent import java.text.SimpleDateFormat import java.util.* -class LogAdapter(private val content: Context, - private val buffer: CircularArray) : RecyclerView.Adapter() { +class LogAdapter( + private val content: Context, + private val buffer: CircularArray +) : RecyclerView.Adapter() { private val formatter = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) class Holder(view: View) : RecyclerView.ViewHolder(view) { @@ -23,8 +25,10 @@ class LogAdapter(private val content: Context, } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return Holder(LayoutInflater.from(content) - .inflate(R.layout.adapter_log, parent, false)) + return Holder( + LayoutInflater.from(content) + .inflate(R.layout.adapter_log, parent, false) + ) } override fun getItemCount(): Int { diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt index 62b74267cc..e4b0fbcf0c 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt @@ -11,11 +11,13 @@ import com.github.kr328.clash.view.RadioFatItem import java.text.SimpleDateFormat import java.util.* -class ProfileAdapter(private val context: Context, - private val onClick: (ClashProfileEntity) -> Unit, - private val onOperateClick: (ClashProfileEntity) -> Unit, - private val onLongClicked: (View,ClashProfileEntity) -> Unit, - private val onNewProfile: () -> Unit) : +class ProfileAdapter( + private val context: Context, + private val onClick: (ClashProfileEntity) -> Unit, + private val onOperateClick: (ClashProfileEntity) -> Unit, + private val onLongClicked: (View, ClashProfileEntity) -> Unit, + private val onNewProfile: () -> Unit +) : RecyclerView.Adapter() { var profiles: Array = emptyArray() @@ -23,7 +25,7 @@ class ProfileAdapter(private val context: Context, class NewProfileHolder(val view: FatItem) : RecyclerView.ViewHolder(view) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - if ( viewType == 0 ) { + if (viewType == 0) { return NewProfileHolder(FatItem(context).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, @@ -47,7 +49,7 @@ class ProfileAdapter(private val context: Context, } override fun onBindViewHolder(raw: RecyclerView.ViewHolder, position: Int) { - if ( position == profiles.size ) { + if (position == profiles.size) { val holder = raw as NewProfileHolder holder.view.icon = context.getDrawable(R.drawable.ic_new_profile) @@ -80,9 +82,10 @@ class ProfileAdapter(private val context: Context, } val now = Calendar.getInstance() - val formatter = if ( profileUpdateDate.get(Calendar.YEAR) == now.get(Calendar.YEAR) && + val formatter = if (profileUpdateDate.get(Calendar.YEAR) == now.get(Calendar.YEAR) && profileUpdateDate.get(Calendar.MONTH) == now.get(Calendar.MONTH) && - profileUpdateDate.get(Calendar.DAY_OF_MONTH) == now.get(Calendar.DAY_OF_MONTH) ) + profileUpdateDate.get(Calendar.DAY_OF_MONTH) == now.get(Calendar.DAY_OF_MONTH) + ) SimpleDateFormat("HH:mm:ss", Locale.getDefault()) else SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) @@ -90,19 +93,23 @@ class ProfileAdapter(private val context: Context, when { ClashProfileEntity.isFileToken(current.token) -> { holder.view.operation = context.getDrawable(R.drawable.ic_edit) - holder.view.summary = context.getString(R.string.clash_profile_item_summary_file, - formatter.format(profileUpdateDate.time)) + holder.view.summary = context.getString( + R.string.clash_profile_item_summary_file, + formatter.format(profileUpdateDate.time) + ) } ClashProfileEntity.isUrlToken(current.token) -> { holder.view.operation = context.getDrawable(R.drawable.ic_sync) - holder.view.summary = context.getString(R.string.clash_profile_item_summary_url, - formatter.format(profileUpdateDate.time)) + holder.view.summary = context.getString( + R.string.clash_profile_item_summary_url, + formatter.format(profileUpdateDate.time) + ) } } } override fun getItemViewType(position: Int): Int { - return if ( position == profiles.size ) + return if (position == profiles.size) 0 else 1 diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt index 495a5e5d44..3bdcd05ea5 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt @@ -13,9 +13,11 @@ import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.model.ListProxy import com.google.android.material.card.MaterialCardView -class ProxyAdapter(private val context: Context, - private val onSelect: (String, String) -> Unit, - private val onUrlTest: (Int, Int) -> Unit) : +class ProxyAdapter( + private val context: Context, + private val onSelect: (String, String) -> Unit, + private val onUrlTest: (Int, Int) -> Unit +) : RecyclerView.Adapter() { var elements: List = emptyList() var clickable: Boolean = false @@ -141,7 +143,7 @@ class ProxyAdapter(private val context: Context, holder.test.visibility = if (current.type == Proxy.Type.SELECT) View.VISIBLE else View.GONE holder.test.setOnClickListener { - if ( current.urlTest ) + if (current.urlTest) return@setOnClickListener val indexed = elements.withIndex() diff --git a/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt b/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt index 37ed8a192b..3b6665d39c 100644 --- a/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt +++ b/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt @@ -5,7 +5,12 @@ import kotlinx.serialization.* import kotlinx.serialization.internal.StringDescriptor @Serializable(ClashRule.Serializer::class) -data class ClashRule(val matcher: Matcher, val pattern: String, val target: String, val extras: List) { +data class ClashRule( + val matcher: Matcher, + val pattern: String, + val target: String, + val extras: List +) { enum class Matcher { DOMAIN_SUFFIX, DOMAIN_KEYWORD, @@ -76,7 +81,12 @@ data class ClashRule(val matcher: Matcher, val pattern: String, val target: Stri ClashRule(Matcher.fromString(rule[0]), rule[1], rule[2], emptyList()) } else -> { - ClashRule(Matcher.fromString(rule[0]), rule[1], rule[2], rule.subList(3, rule.size)) + ClashRule( + Matcher.fromString(rule[0]), + rule[1], + rule[2], + rule.subList(3, rule.size) + ) } } } @@ -85,7 +95,11 @@ data class ClashRule(val matcher: Matcher, val pattern: String, val target: Stri if (obj.matcher == Matcher.MATCH) { encoder.encodeString("${obj.matcher},${obj.target}") } else { - encoder.encodeString("${obj.matcher},${obj.pattern},${obj.target},${obj.extras.joinToString(",")}") + encoder.encodeString( + "${obj.matcher},${obj.pattern},${obj.target},${obj.extras.joinToString( + "," + )}" + ) } } } diff --git a/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt b/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt index 5f751ee159..116dd5b282 100644 --- a/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt +++ b/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt @@ -3,7 +3,13 @@ package com.github.kr328.clash.model import com.github.kr328.clash.core.model.Proxy interface ListProxy { - data class ListProxyHeader(val name: String, val type: Proxy.Type, val now: String, var nowIndex: Int, var urlTest: Boolean = false) : + data class ListProxyHeader( + val name: String, + val type: Proxy.Type, + val now: String, + var nowIndex: Int, + var urlTest: Boolean = false + ) : ListProxy data class ListProxyItem( diff --git a/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt index 9e68f1a5b1..fdfb403fb5 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt @@ -19,7 +19,7 @@ object ServiceUtils { MainApplication.PROXY_MODE_VPN -> { val prepare = VpnService.prepare(context) - if ( prepare != null ) + if (prepare != null) return prepare context.startService(Intent(context, TunService::class.java)) diff --git a/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt b/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt index c4c8282465..3a09e2a954 100644 --- a/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt +++ b/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt @@ -10,7 +10,7 @@ class MarqueeTextView @JvmOverloads constructor( attributeSet: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0 -): TextView(context, attributeSet, defStyleAttr, defStyleRes) { +) : TextView(context, attributeSet, defStyleAttr, defStyleRes) { init { ellipsize = TextUtils.TruncateAt.MARQUEE } From 0ef8734856cde8cb6978eee36f0cc4710ce69605 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 22 Dec 2019 15:55:46 +0800 Subject: [PATCH 060/358] fix udp reject --- .../java/com/github/kr328/clash/MainApplication.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 0ef3d223c3..8305328d05 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -5,10 +5,15 @@ import android.content.Context import android.os.Build import com.crashlytics.android.Crashlytics import com.github.kr328.clash.core.Constants +import com.github.kr328.clash.core.utils.ByteFormatter import com.github.kr328.clash.core.utils.Log import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric +import java.io.File +import java.lang.Exception import java.security.MessageDigest +import java.util.zip.ZipFile +import kotlin.concurrent.thread class MainApplication : Application() { @@ -20,6 +25,7 @@ class MainApplication : Application() { val GOOGLE_PLAY_INSTALLER = listOf("com.android.vending", "com.google.android.feedback") const val CRASHLYTICS_FROM_PLAY_KEY = "install_from_google" const val CRASHLYTICS_SPLIT_APK_KEY = "split_apk" + const val CRASHLYTICS_BASE_SIZE_KEY = "base_size" val userIdentifier: String by lazy { val archive = instance.packageManager.getPackageInfo(instance.packageName, 0) @@ -60,6 +66,7 @@ class MainApplication : Application() { Crashlytics.setBool(CRASHLYTICS_FROM_PLAY_KEY, detectFromPlay()) Crashlytics.setBool(CRASHLYTICS_SPLIT_APK_KEY, detectSplitArchive()) + Crashlytics.setString(CRASHLYTICS_BASE_SIZE_KEY, getBaseApkSize()) Crashlytics.setUserIdentifier(userIdentifier) Log.handler = object : Log.LogHandler { @@ -106,4 +113,10 @@ class MainApplication : Application() { val split = applicationInfo.splitSourceDirs return split != null && split.isNotEmpty() } + + private fun getBaseApkSize(): String { + val size = File(applicationInfo.sourceDir).length() + + return ByteFormatter.byteToString(size) + } } \ No newline at end of file From 00163fbfd4d0f61e52dbfde25fae52eb8ba08cfe Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 11 Jan 2020 14:15:10 +0800 Subject: [PATCH 061/358] update submodule --- core/src/main/golang/clash | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index c351009e6d..d0ba150ed9 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit c351009e6d5b208650e13ed2f9492568740211ee +Subproject commit d0ba150ed93122efe3516a89ad5c5ff21d323789 From 2ba82ebd97bdb8bf73867e9d77bea752b714c5ea Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 11 Jan 2020 21:05:59 +0800 Subject: [PATCH 062/358] migrate to netstack --- app/build.gradle | 13 +- build.gradle | 2 +- buildSrc/src/main/java/GolangBindTask.kt | 26 +++- buildSrc/src/main/java/MMDBDowloadTask.kt | 2 +- core/build.gradle | 33 +---- core/src/main/golang/bridge/init.go | 4 +- core/src/main/golang/bridge/tun.go | 8 +- core/src/main/golang/go.mod | 6 +- core/src/main/golang/go.sum | 12 ++ core/src/main/golang/profile/load.go | 2 +- core/src/main/golang/tun/tun.go | 50 ++----- .../com/github/kr328/clash/core/BaseClash.kt | 72 ---------- .../java/com/github/kr328/clash/core/Clash.kt | 10 +- .../github/kr328/clash/core/ClashProcess.kt | 133 ------------------ .../github/kr328/clash/service/TunService.kt | 26 ++-- 15 files changed, 80 insertions(+), 319 deletions(-) delete mode 100644 core/src/main/java/com/github/kr328/clash/core/BaseClash.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt diff --git a/app/build.gradle b/app/build.gradle index 11e1a0e127..9beda172e3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -13,8 +13,8 @@ android { applicationId "com.github.kr328.clash" minSdkVersion 24 targetSdkVersion 29 - versionCode 10029 - versionName "1.0.29-alpha" + versionCode 10035 + versionName "1.0.35-alpha" } buildTypes { release { @@ -42,18 +42,19 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "androidx.lifecycle:lifecycle-livedata:2.1.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0" - implementation 'androidx.browser:browser:1.0.0' + implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.core:core-ktx:1.2.0-rc01' - implementation 'androidx.fragment:fragment-ktx:1.2.0-rc03' + implementation 'androidx.fragment:fragment-ktx:1.2.0-rc05' implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta3' + implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation "androidx.room:room-runtime:$room_version" implementation "androidx.preference:preference-ktx:1.1.0" + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" implementation "com.charleskorn.kaml:kaml:0.14.0" - implementation "com.google.android.material:material:1.2.0-alpha02" + implementation "com.google.android.material:material:1.2.0-alpha03" implementation 'com.google.firebase:firebase-analytics:17.2.1' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' } diff --git a/build.gradle b/build.gradle index f21d4e71b4..835c2fccd1 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ buildscript { ext { kotlin_version = '1.3.61' - room_version = '2.2.2' + room_version = '2.2.3' } repositories { google() diff --git a/buildSrc/src/main/java/GolangBindTask.kt b/buildSrc/src/main/java/GolangBindTask.kt index 6aaf2bd856..3452a023e9 100644 --- a/buildSrc/src/main/java/GolangBindTask.kt +++ b/buildSrc/src/main/java/GolangBindTask.kt @@ -16,11 +16,9 @@ open class GolangBindTask : DefaultTask() { require ( github.com/kr328/cfa v0.0.0 // redirect - github.com/Dreamacro/clash v0.0.0 // redirect ) replace github.com/kr328/cfa v0.0.0 => {SOURCE_PATH} - replace github.com/Dreamacro/clash v0.0.0 => {SOURCE_PATH}/clash """.trimIndent() private val STUB_GO_FILE_CONTENT = """ package main @@ -29,6 +27,8 @@ open class GolangBindTask : DefaultTask() { func main() {} """.trimIndent() + private val REGEX_REPLACE_TARGET_LOCAL = Regex("=>\\s+\\./") + private val REGEX_REPLACE_SOURCE_VERSION = Regex("v.+\\s+=>") } private val javaOutput: File @@ -90,7 +90,7 @@ open class GolangBindTask : DefaultTask() { "go get golang.org/x/mobile/cmd/gomobile".exec() FileWriter(goBindPath.resolve("go.mod")).use { - it.write(STUB_GO_MOD_CONTENT.replace("{SOURCE_PATH}", sourcePath.absolutePath)) + it.write(appendReplace(sourcePath)) } FileWriter(goBindPath.resolve("main.go")).use { it.write(STUB_GO_FILE_CONTENT) @@ -152,6 +152,26 @@ open class GolangBindTask : DefaultTask() { ?: throw GradleException("Android SDK not found.") } + private fun appendReplace(source: File): String { + val replaces = source.walk() + .filter { it.name == "go.mod" } + .flatMap { file -> + file.readLines() + .asSequence() + .filter { line -> line.startsWith("replace") } + .map { replace -> + replace.replace(REGEX_REPLACE_TARGET_LOCAL, "=> " + file.parentFile.absolutePath + "/") + } + .map { replace -> + replace.replace(REGEX_REPLACE_SOURCE_VERSION, " =>") + } + } + .joinToString("\n") + + return STUB_GO_MOD_CONTENT.replace("{SOURCE_PATH}", source.absolutePath) + + "\n\n" + replaces + } + private fun String.exec(pwd: File = File(".")) { val process = with(ProcessBuilder()) { if (Os.isFamily(Os.FAMILY_WINDOWS)) diff --git a/buildSrc/src/main/java/MMDBDowloadTask.kt b/buildSrc/src/main/java/MMDBDowloadTask.kt index 45af3a24b3..7e6991b881 100644 --- a/buildSrc/src/main/java/MMDBDowloadTask.kt +++ b/buildSrc/src/main/java/MMDBDowloadTask.kt @@ -8,7 +8,7 @@ import java.net.URL open class MMDBDowloadTask : DefaultTask() { companion object { - const val URL = "http://geolite.maxmind.com/download/geoip/database/GeoLite2-Country.tar.gz" + const val URL = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb" } var output: String = "" diff --git a/core/build.gradle b/core/build.gradle index 1c754e1427..c02b234e45 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -45,41 +45,16 @@ dependencies { afterEvaluate { def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) { onlyIf { - System.currentTimeMillis() - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > 24 * 3600 * 1000L + System.currentTimeMillis() - file("$buildDir/intermediates/dynamic_assets/Country.mmdb").lastModified() > 24 * 3600 * 1000L } - output = "$buildDir/intermediates/cache/Country.tar.gz" - } - - def es = tasks.register("extractMMDB", Copy.class) { - onlyIf { - file("$buildDir/intermediates/cache/Country.tar.gz").lastModified() > file("$buildDir/intermediates/dynamic_assets/Country.mmdb").lastModified() - } - - from(tarTree("$buildDir/intermediates/cache/Country.tar.gz")) { - include "**/*.mmdb" - } - into "$buildDir/intermediates/cache/mmdb" - - doLast { - file("$buildDir/intermediates/dynamic_assets").mkdirs() - fileTree("$buildDir/intermediates/cache/mmdb").visit { FileVisitDetails details -> - if ( details.path.endsWith("Country.mmdb") ) - details.file.renameTo(file("$buildDir/intermediates/dynamic_assets/Country.mmdb")) - } - } - - dependsOn ds - } - - for ( task in tasks ) { - if ( task.name.contains("generate") && task.name.contains("Assets") ) - task.dependsOn(es) + output = "$buildDir/intermediates/dynamic_assets/Country.mmdb" } def gs = tasks.register("golangBind", GolangBindTask.class) - preBuild.dependsOn(gs) + preBuild.dependsOn(ds, gs) + } repositories { diff --git a/core/src/main/golang/bridge/init.go b/core/src/main/golang/bridge/init.go index e1fa614393..9b301f6f08 100644 --- a/core/src/main/golang/bridge/init.go +++ b/core/src/main/golang/bridge/init.go @@ -6,11 +6,11 @@ import "github.com/kr328/cfa/profile" import "github.com/Dreamacro/clash/tunnel" -func Init(home string) { +func SetBaseDir(home string) { constant.SetHomeDir(home) } func Reset() { profile.LoadDefault() - tunnel.DefaultManager.Reset() + tunnel.DefaultManager.ResetStatistic() } diff --git a/core/src/main/golang/bridge/tun.go b/core/src/main/golang/bridge/tun.go index 4bd4865e18..7cc527816c 100644 --- a/core/src/main/golang/bridge/tun.go +++ b/core/src/main/golang/bridge/tun.go @@ -4,12 +4,8 @@ import ( "github.com/kr328/cfa/tun" ) -type TunCallback interface { - OnNewSocket(fd int) -} - -func StartTunDevice(fd, mtu int, gateway string, dns string, callback TunCallback) error { - return tun.StartTunDevice(fd, mtu, gateway, dns, callback) +func StartTunDevice(fd, mtu int, dns string) error { + return tun.StartTunDevice(fd, mtu, dns) } func StopTunDevice() { diff --git a/core/src/main/golang/go.mod b/core/src/main/golang/go.mod index 68e9756766..0624a48d83 100644 --- a/core/src/main/golang/go.mod +++ b/core/src/main/golang/go.mod @@ -7,7 +7,9 @@ require ( github.com/go-chi/render v1.0.1 github.com/google/go-cmp v0.3.1 // indirect golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d // indirect - golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 + golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 ) -replace github.com/Dreamacro/clash v0.0.0 => ./clash +replace github.com/Dreamacro/clash => ./clash + +replace github.com/google/netstack => github.com/comzyh/netstack v0.0.0-20191217044024-67c27819ada4 diff --git a/core/src/main/golang/go.sum b/core/src/main/golang/go.sum index 36689209c6..be4383c0e8 100644 --- a/core/src/main/golang/go.sum +++ b/core/src/main/golang/go.sum @@ -10,6 +10,8 @@ github.com/Dreamacro/go-shadowsocks2 v0.1.5 h1:BizWSjmwzAyQoslz6YhJYMiAGT99j9cnm github.com/Dreamacro/go-shadowsocks2 v0.1.5/go.mod h1:LSXCjyHesPY3pLjhwff1mQX72ItcBT/N2xNC685cYeU= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= +github.com/comzyh/netstack v0.0.0-20191217044024-67c27819ada4 h1:30ykXB9NWubvyVWE5pe/YakDgEdu6wJkBZlZYDtV464= +github.com/comzyh/netstack v0.0.0-20191217044024-67c27819ada4/go.mod h1:jMMWEkl1smElz5KtnrIWDHxc8gtMIO/Pd8+pLyGRzT8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -46,14 +48,20 @@ github.com/miekg/dns v1.1.22 h1:Jm64b3bO9kP43ddLjL2EY3Io6bmy1qGb9Xxz6TqS6rc= github.com/miekg/dns v1.1.22/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.24 h1:6G8Eop/HM8hpagajbn0rFQvAKZWiiCa8P6N2I07+wwI= github.com/miekg/dns v1.1.24/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= +github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU= github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE= github.com/oschwald/geoip2-golang v1.3.0 h1:D+Hsdos1NARPbzZ2aInUHZL+dApIzo8E0ErJVsWcku8= github.com/oschwald/geoip2-golang v1.3.0/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE= +github.com/oschwald/geoip2-golang v1.4.0 h1:5RlrjCgRyIGDz/mBmPfnAF4h8k0IAcRv9PvrpOfz+Ug= +github.com/oschwald/geoip2-golang v1.4.0/go.mod h1:8QwxJvRImBH+Zl6Aa6MaIcs5YdlZSTKtzmPGzQqi9ng= github.com/oschwald/maxminddb-golang v1.3.0 h1:oTh8IBSj10S5JNlUDg5WjJ1QdBMdeaZIkPEVfESSWgE= github.com/oschwald/maxminddb-golang v1.3.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY= github.com/oschwald/maxminddb-golang v1.5.0 h1:rmyoIV6z2/s9TCJedUuDiKht2RN12LWJ1L7iRGtWY64= github.com/oschwald/maxminddb-golang v1.5.0/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY= +github.com/oschwald/maxminddb-golang v1.6.0 h1:KAJSjdHQ8Kv45nFIbtoLGrGWqHFajOIm7skTyz/+Dls= +github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k= @@ -98,6 +106,8 @@ golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933 h1:e6HwijUxhDe+hPNjZQQn9bA5P golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663 h1:Dd5RoEW+yQi+9DMybroBctIdyiwuNT7sJFMC27/6KxI= golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= @@ -111,6 +121,8 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe h1:6fAMxZRR6sl1Uq8U61gxU+kPT golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9 h1:ZBzSG/7F4eNKz2L3GE9o300RX0Az1Bw5HF7PDraD+qU= golang.org/x/sys v0.0.0-20191128015809-6d18c012aee9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191224085550-c709ea063b76 h1:Dho5nD6R3PcW2SH1or8vS0dszDaXRxIw55lBX7XiE5g= +golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index 52b59263e5..672bb2a25f 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -68,7 +68,7 @@ func LoadFromFile(path string) error { if dns.DefaultResolver == nil { _, ipnet, _ := net.ParseCIDR("198.18.0.1/16") - pool, _ := fakeip.New(ipnet, 1000) + pool, _ := fakeip.New(ipnet, 1000, nil) var defaultDNSResolver = dns.New(dns.Config{ Main: []dns.NameServer{ diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go index 64a8b68731..b36baa3af0 100644 --- a/core/src/main/golang/tun/tun.go +++ b/core/src/main/golang/tun/tun.go @@ -1,55 +1,28 @@ package tun import ( - "net" "strconv" - "syscall" - "github.com/Dreamacro/clash/global" + "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/proxy/tun" ) -type Callback interface { - OnNewSocket(fd int) -} - -var tunInstance *tun.TUN +var tunInstance *tun.TunAdapter +var dnsAddress string -func StartTunDevice(fd, mtu int, gateway string, dnsGateway string, callback Callback) error { +func StartTunDevice(fd, mtu int, dns string) error { if tunInstance != nil { return nil } - ip, n, err := net.ParseCIDR(gateway) + t, err := tun.NewTunProxy("fd://" + strconv.Itoa(fd) + "?mtu=" + strconv.Itoa(mtu)) if err != nil { return err } - n.IP = ip.To4() - - global.DefaultDialer.Control = func(network, address string, c syscall.RawConn) error { - return c.Control(func(fd uintptr) { - callback.OnNewSocket(int(fd)) - }) - } - global.DefaultListenConfig.Control = func(network, address string, c syscall.RawConn) error { - return c.Control(func(fd uintptr) { - callback.OnNewSocket(int(fd)) - }) - } - - dnsGatewayIP := net.ParseIP(dnsGateway) - - t, err := tun.NewTunProxy("fd://"+strconv.Itoa(fd)+"?mtu="+strconv.Itoa(mtu), *n, dnsGatewayIP) - - if err != nil { - global.DefaultDialer.Control = nil - global.DefaultListenConfig.Control = nil - - return err - } - tunInstance = t + tunInstance = &t + dnsAddress = dns + ":53" ResetDnsRedirect() @@ -64,12 +37,7 @@ func StopTunDevice() { return } - t.Close() - - global.DefaultDialer.Control = nil - global.DefaultListenConfig.Control = nil - - tunInstance = nil + (*t).Close() } func ResetDnsRedirect() { @@ -77,5 +45,5 @@ func ResetDnsRedirect() { return } - tunInstance.ResetDnsServer() + (*tunInstance).ReCreateDNSServer(dns.DefaultResolver, dnsAddress) } diff --git a/core/src/main/java/com/github/kr328/clash/core/BaseClash.kt b/core/src/main/java/com/github/kr328/clash/core/BaseClash.kt deleted file mode 100644 index 104d3b43c1..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/BaseClash.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.github.kr328.clash.core - -import android.net.LocalSocket -import android.net.LocalSocketAddress -import com.github.kr328.clash.core.utils.Log -import java.io.DataInputStream -import java.io.DataOutputStream -import java.io.File - -@Suppress("SameParameterValue") -abstract class BaseClash(private val controllerPath: File) { - protected fun runControl( - command: Int, - block: (LocalSocket, DataInputStream, DataOutputStream) -> R - ): R { - val socket = LocalSocket() - - socket.connect( - LocalSocketAddress( - controllerPath.absolutePath, - LocalSocketAddress.Namespace.FILESYSTEM - ) - ) - - val input = DataInputStream(socket.inputStream) - val output = DataOutputStream(socket.outputStream) - - output.writeInt(command) - - val result = block(socket, input, output) - - runCatching { - socket.close() - } - - return result - } - - protected fun runControl(command: Int) { - return runControl(command) { _, _, _ -> } - } - - protected fun runControlNoException( - command: Int, - block: (LocalSocket, DataInputStream, DataOutputStream) -> R - ): R? { - return try { - runControl(command, block) - } catch (ignored: Exception) { null } - } - - protected fun runControlNoException(command: Int) { - try { - runControl(command) - } catch (ignored: Exception) {} - } - - protected fun DataOutputStream.writeString(string: String) { - val buffer = string.toByteArray(Charsets.UTF_8) - this.writeInt(buffer.size) - this.write(buffer) - } - - protected fun DataInputStream.readString(): String { - val len = this.readInt() - val buffer = ByteArray(len) - - this.readFully(buffer) - - return String(buffer) - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index d524e2935d..d3a1edb6eb 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -47,7 +47,7 @@ class Clash( } } - Bridge.init(home.absolutePath) + Bridge.setBaseDir(home.absolutePath) } fun getCurrentProcessStatus(): ProcessEvent { @@ -79,15 +79,11 @@ class Clash( fun startTunDevice( fd: Int, mtu: Int, - gateway: String, - dns: String, - onSocket: (Int) -> Unit + dns: String ) { enforceStarted() - Bridge.startTunDevice(fd.toLong(), mtu.toLong(), gateway, dns) { - onSocket(it.toInt()) - } + Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns) } fun stopTunDevice() { diff --git a/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt b/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt deleted file mode 100644 index 90e9ec9c74..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/ClashProcess.kt +++ /dev/null @@ -1,133 +0,0 @@ -package com.github.kr328.clash.core - -import android.content.Context -import com.github.kr328.clash.core.event.ProcessEvent -import com.github.kr328.clash.core.utils.Log -import java.io.File -import java.io.FileOutputStream -import java.io.IOException -import kotlin.concurrent.thread - -class ClashProcess( - private val context: Context, - private val clashDir: File, - private val controllerPath: File, - private val listener: (ProcessEvent) -> Unit -) { - companion object { - private const val CONTROLLER_STATUS_PREFIX = "[CONTROLLER]" - - private val PID_PATTERN = Regex("\\[PID]\\s*(\\d+)") - private val CONTROLLER_ERROR_PATTERN = Regex("\\[CONTROLLER] ERROR=\\{(.+)\\}") - } - - private var pid: Int = 0 - private var process: Process? = null - - @Synchronized - fun start() { - if (pid > 0) - return - - try { - clashDir.mkdirs() - controllerPath.parentFile?.mkdirs() - controllerPath.delete() - - extractMMDB() - - val clashPath = - File(context.applicationInfo.nativeLibraryDir, "libclash.so").absolutePath - - val p = ProcessBuilder().apply { - command(clashPath, controllerPath.absolutePath) - directory(clashDir) - }.start() - - Log.i("Starting clash [$clashPath]") - - val reader = p.inputStream.bufferedReader() - var line = "" - - // Parse pid - var currentPid = 0 - while (reader.readLine()?.apply { line = this.trim() } != null) { - Log.i(line) - - if (PID_PATTERN.matchEntire(line)?.apply { - currentPid = groups[1]!!.value.toInt() - } != null) - break - } - - while (reader.readLine()?.apply { line = this.trim() } != null) { - Log.i(line) - - if (line.startsWith(CONTROLLER_STATUS_PREFIX)) { - val error = CONTROLLER_ERROR_PATTERN.matchEntire(line)?.groups?.get(1)?.value - - if (error != null) - throw IOException("Controller: $error") - - break - } - } - - process = p - pid = currentPid - - listener(ProcessEvent.STARTED) - - Log.i("Clash started pid = $pid") - - thread { - // Redirect stdout to log - while (reader.readLine()?.apply { line = this } != null) { - Log.i(line.trim()) - } - - synchronized(this@ClashProcess) { - p.destroy() - process = null - pid = -1 - } - - listener(ProcessEvent.STOPPED) - } - } catch (e: Exception) { - listener(ProcessEvent.STOPPED) - throw e - } - } - - @Synchronized - fun getProcessStatus(): ProcessEvent { - return if (pid > 0) - ProcessEvent.STARTED - else - ProcessEvent.STOPPED - } - - @Synchronized - fun stop() { - if ( getProcessStatus() == ProcessEvent.STOPPED ) - listener(ProcessEvent.STOPPED) - - android.os.Process.killProcess(pid) - } - - private fun extractMMDB() { - if (context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime - < clashDir.resolve("Country.mmdb").lastModified() - ) - return - - clashDir.resolve("ui").mkdirs() - - context.resources.assets.open("Country.mmdb").use { input -> - FileOutputStream(clashDir.resolve("Country.mmdb")).use { output -> - input.copyTo(output) - } - } - } -} \ 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 f4e5f4bb69..f9c56b8107 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 @@ -10,8 +10,6 @@ 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 -import java.net.Inet4Address -import java.net.InetAddress class TunService : VpnService(), IClashEventObserver { companion object { @@ -118,18 +116,14 @@ class TunService : VpnService(), IClashEventObserver { if ( settings.isDnsHijackingEnabled ) { clash.clash.startTunDevice( fileDescriptor.fd, VPN_MTU, - "$PRIVATE_VLAN4_CLIENT/$PRIVATE_VLAN4_SUBNET", VLAN4_ANY - ) { - protect(it.toInt()) - } + VLAN4_ANY + ) } else { clash.clash.startTunDevice( fileDescriptor.fd, VPN_MTU, - "$PRIVATE_VLAN4_CLIENT/$PRIVATE_VLAN4_SUBNET", PRIVATE_VLAN_DNS - ) { - protect(it.toInt()) - } + PRIVATE_VLAN_DNS + ) } fileDescriptor.close() @@ -170,11 +164,13 @@ class TunService : VpnService(), IClashEventObserver { addDisallowedApplication(app) } } + addDisallowedApplication(packageName) } ClashSettingService.ACCESS_CONTROL_MODE_ALLOW -> { addAllowedApplication(packageName) - for ( app in (settings.accessControlApps.toSet() - - resources.getStringArray(R.array.default_disallow_application)) ) { + for ( app in settings.accessControlApps.toSet() - + resources.getStringArray(R.array.default_disallow_application) - + setOf(packageName) ) { runCatching { addAllowedApplication(app) }.onFailure { @@ -183,15 +179,15 @@ class TunService : VpnService(), IClashEventObserver { } } ClashSettingService.ACCESS_CONTROL_MODE_DISALLOW -> { - for ( app in (settings.accessControlApps.toSet() + - resources.getStringArray(R.array.default_disallow_application) - - listOf(packageName)) ) { + for ( app in settings.accessControlApps.toSet() + + resources.getStringArray(R.array.default_disallow_application) ) { runCatching { addDisallowedApplication(app) }.onFailure { Log.w("Package $app not found") } } + addDisallowedApplication(packageName) } } From 1eb6f59167bb82409d80c4825c8b017cd1430898 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 11 Jan 2020 21:25:22 +0800 Subject: [PATCH 063/358] trim gopath for output --- buildSrc/src/main/java/GolangBindTask.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/buildSrc/src/main/java/GolangBindTask.kt b/buildSrc/src/main/java/GolangBindTask.kt index 3452a023e9..59d92c1e8b 100644 --- a/buildSrc/src/main/java/GolangBindTask.kt +++ b/buildSrc/src/main/java/GolangBindTask.kt @@ -90,7 +90,7 @@ open class GolangBindTask : DefaultTask() { "go get golang.org/x/mobile/cmd/gomobile".exec() FileWriter(goBindPath.resolve("go.mod")).use { - it.write(appendReplace(sourcePath)) + it.write(buildStubGoModule(sourcePath)) } FileWriter(goBindPath.resolve("main.go")).use { it.write(STUB_GO_FILE_CONTENT) @@ -102,7 +102,7 @@ open class GolangBindTask : DefaultTask() { .copyRecursively(goPath.resolve("src"), overwrite = true) "gomobile init".exec(goBuildPath) - "gomobile bind -target=android github.com/kr328/cfa/bridge".exec(goBuildPath) + "gomobile bind -target=android \"-gcflags=all=-trimpath=$goPath\" github.com/kr328/cfa/bridge".exec(goBuildPath) nativeOutput.deleteRecursively() javaOutput.deleteRecursively() @@ -152,7 +152,7 @@ open class GolangBindTask : DefaultTask() { ?: throw GradleException("Android SDK not found.") } - private fun appendReplace(source: File): String { + private fun buildStubGoModule(source: File): String { val replaces = source.walk() .filter { it.name == "go.mod" } .flatMap { file -> From 19ddb8643069e266f2f0150d377779cb78f71006 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 12 Jan 2020 23:43:52 +0800 Subject: [PATCH 064/358] [WIP] refactor clash bridge --- buildSrc/src/main/java/GolangBindTask.kt | 1 + core/src/main/golang/bridge/init.go | 14 +- core/src/main/golang/bridge/profiles.go | 12 +- core/src/main/golang/bridge/proxies.go | 176 ++++++++++++------ core/src/main/golang/clash | 2 +- core/src/main/golang/profile/download.go | 73 ++++++++ core/src/main/golang/profile/load.go | 28 ++- .../java/com/github/kr328/clash/core/Clash.kt | 88 ++++----- .../github/kr328/clash/core/model/Proxy.kt | 2 - .../kr328/clash/core/model/ProxyGroup.kt | 12 ++ .../clash/core/transact/ProxyCollections.kt | 7 + .../kr328/clash/service/ClashServiceImpl.kt | 11 +- 12 files changed, 283 insertions(+), 143 deletions(-) create mode 100644 core/src/main/golang/profile/download.go create mode 100644 core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt create mode 100644 core/src/main/java/com/github/kr328/clash/core/transact/ProxyCollections.kt diff --git a/buildSrc/src/main/java/GolangBindTask.kt b/buildSrc/src/main/java/GolangBindTask.kt index 59d92c1e8b..e09140465f 100644 --- a/buildSrc/src/main/java/GolangBindTask.kt +++ b/buildSrc/src/main/java/GolangBindTask.kt @@ -84,6 +84,7 @@ open class GolangBindTask : DefaultTask() { else environment.put("PATH", System.getenv("PATH") + ":" + goPath.resolve("bin")) + goPath.resolve("src/github.com/kr328").deleteRecursively() goBindPath.deleteRecursively() goBindPath.mkdirs() diff --git a/core/src/main/golang/bridge/init.go b/core/src/main/golang/bridge/init.go index 9b301f6f08..321b174dcf 100644 --- a/core/src/main/golang/bridge/init.go +++ b/core/src/main/golang/bridge/init.go @@ -1,13 +1,13 @@ package bridge -import "github.com/Dreamacro/clash/constant" +import ( + "github.com/Dreamacro/clash/component/mmdb" + "github.com/Dreamacro/clash/tunnel" + "github.com/kr328/cfa/profile" +) -import "github.com/kr328/cfa/profile" - -import "github.com/Dreamacro/clash/tunnel" - -func SetBaseDir(home string) { - constant.SetHomeDir(home) +func LoadMMDB(data []byte) { + mmdb.LoadFromBytes(data) } func Reset() { diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go index 5decb71fd3..f10d66c3d2 100644 --- a/core/src/main/golang/bridge/profiles.go +++ b/core/src/main/golang/bridge/profiles.go @@ -2,10 +2,14 @@ package bridge import "github.com/kr328/cfa/profile" -func LoadProfileFile(path string) error { - return profile.LoadFromFile(path) +func LoadProfileFile(path, baseDir string) error { + return profile.LoadFromFile(path, baseDir) } -func CheckProfileValid(profileData string) error { - return profile.CheckValid(profileData) +func DownloadProfileAndCheck(url, output string) error { + return profile.DownloadAndCheck(url, output) +} + +func SaveProfileAndCheck(data []byte, output string) error { + return profile.SaveAndCheck(data, output) } diff --git a/core/src/main/golang/bridge/proxies.go b/core/src/main/golang/bridge/proxies.go index 6d23a73054..71ca774550 100644 --- a/core/src/main/golang/bridge/proxies.go +++ b/core/src/main/golang/bridge/proxies.go @@ -1,74 +1,150 @@ package bridge import ( - "context" - "encoding/json" - "time" + "sync" "github.com/Dreamacro/clash/adapters/outbound" "github.com/Dreamacro/clash/adapters/outboundgroup" + "github.com/Dreamacro/clash/adapters/provider" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" ) -type Proxy struct { +type ProxyItem struct { Name string - Type string `json:"type"` - All []string `json:"all"` - Now string `json:"now"` + Type string Delay int } -type ProxyList struct { - proxies []*Proxy -} +type ProxyGroupItem struct { + Name string + Type string + Current string + Delay int -type UrlTestCallback interface { - OnResult(name string, delay int) + providers []provider.ProxyProvider } -func (p Proxy) GetAllLength() int { - return len(p.All) +type ProxyGroupCollection interface { + Add(proxy *ProxyGroupItem) bool } -func (p Proxy) GetAllElement(index int) string { - return p.All[index] +type ProxyCollection interface { + Add(proxy *ProxyItem) bool } -func (pl *ProxyList) GetProxiesLength() int { - return len(pl.proxies) +type UrlTestCallback interface { + Done() } -func (pl *ProxyList) GetProxiesElement(index int) *Proxy { - return pl.proxies[index] +func (p *ProxyGroupItem) QueryAllProxies(collection ProxyCollection) { + for _, v := range p.providers { + for _, p := range v.Proxies() { + collection.Add( + &ProxyItem{ + Name: p.Name(), + Type: p.Type().String(), + Delay: int(p.LastDelay()), + }, + ) + } + } } -func QueryAllProxies() (*ProxyList, error) { - ps := tunnel.Instance().Proxies() - result := make([]*Proxy, len(ps)) - currentIndex := 0 - - for k, v := range ps { - current := &Proxy{ - Name: k, - Type: v.Type().String(), - All: []string{}, - Delay: int(v.LastDelay()), +func StartUrlTest(group string, callback UrlTestCallback) { + go func() { + defer callback.Done() + + p := tunnel.Instance().Proxies()[group] + + pa, ok := p.(*outbound.Proxy) + if !ok { + return } - data, err := v.MarshalJSON() - if err != nil { - return nil, err + var providers []provider.ProxyProvider + + switch group := pa.ProxyAdapter.(type) { + case *outboundgroup.Fallback: + providers = group.GetProviders() + case *outboundgroup.URLTest: + providers = group.GetProviders() + case *outboundgroup.LoadBalance: + providers = group.GetProviders() + case *outboundgroup.Selector: + providers = group.GetProviders() + default: + return } - json.Unmarshal(data, current) + wg := &sync.WaitGroup{} + wg.Add(len(providers)) - result[currentIndex] = current + for _, v := range providers { + go func(p provider.ProxyProvider) { + p.HealthCheck() + wg.Done() + }(v) + } - currentIndex++ - } + wg.Wait() + }() +} + +func QueryAllProxyGroups(collection ProxyGroupCollection) { + ps := tunnel.Instance().Proxies() - return &ProxyList{proxies: result}, nil + for _, p := range ps { + pa, ok := p.(*outbound.Proxy) + if !ok { + continue + } + + switch group := pa.ProxyAdapter.(type) { + case *outboundgroup.Fallback: + collection.Add( + &ProxyGroupItem{ + Name: group.Name(), + Type: group.Type().String(), + Current: group.Now(), + Delay: int(p.LastDelay()), + providers: group.GetProviders(), + }, + ) + case *outboundgroup.URLTest: + collection.Add( + &ProxyGroupItem{ + Name: group.Name(), + Type: group.Type().String(), + Current: group.Now(), + Delay: int(p.LastDelay()), + providers: group.GetProviders(), + }, + ) + case *outboundgroup.LoadBalance: + collection.Add( + &ProxyGroupItem{ + Name: group.Name(), + Type: group.Type().String(), + Current: "", + Delay: int(p.LastDelay()), + providers: group.GetProviders(), + }, + ) + case *outboundgroup.Selector: + collection.Add( + &ProxyGroupItem{ + Name: group.Name(), + Type: group.Type().String(), + Current: group.Now(), + Delay: int(p.LastDelay()), + providers: group.GetProviders(), + }, + ) + default: + continue + } + } } func SetSelectedProxy(name, proxy string) bool { @@ -95,25 +171,3 @@ func SetSelectedProxy(name, proxy string) bool { return true } - -func StartUrlTest(name, url string, timeout int, callback UrlTestCallback) { - go func() { - p := tunnel.Instance().Proxies()[name] - - if p == nil { - callback.OnResult(name, -1) - return - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*time.Duration(timeout)) - defer cancel() - - delay, err := p.URLTest(ctx, url) - if ctx.Err() != nil || err != nil { - callback.OnResult(name, -1) - return - } - - callback.OnResult(name, int(delay)) - }() -} diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index d0ba150ed9..7adba59744 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit d0ba150ed93122efe3516a89ad5c5ff21d323789 +Subproject commit 7adba597440b5d2d54677731c0df7a86b564e90f diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go new file mode 100644 index 0000000000..167624f884 --- /dev/null +++ b/core/src/main/golang/profile/download.go @@ -0,0 +1,73 @@ +package profile + +import ( + "context" + "errors" + "io/ioutil" + "net" + "net/http" + + "github.com/Dreamacro/clash/adapters/inbound" + "github.com/Dreamacro/clash/component/socks5" + "github.com/Dreamacro/clash/config" + "github.com/Dreamacro/clash/constant" + "github.com/Dreamacro/clash/tunnel" +) + +const defaultFileMode = 0600 + +var client = &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { + if network != "tcp" && network != "tcp4" && network != "tcp6" { + return nil, errors.New("Unsupported network type " + network) + } + + client, server := net.Pipe() + + tunnel.Instance().Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP, constant.TCP)) + + go func() { + if ctx == nil || ctx.Done() == nil { + return + } + + <-ctx.Done() + + client.Close() + server.Close() + }() + + return client, nil + }, + }, +} + +func DownloadAndCheck(url, output string) error { + response, err := client.Get(url) + if err != nil { + return err + } + + defer response.Body.Close() + data, err := ioutil.ReadAll(response.Body) + if err != nil { + return err + } + + _, err = config.Parse(data) + if err != nil { + return err + } + + return ioutil.WriteFile(output, data, defaultFileMode) +} + +func SaveAndCheck(data []byte, output string) error { + _, err := config.Parse(data) + if err != nil { + return err + } + + return ioutil.WriteFile(output, data, defaultFileMode) +} diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index 672bb2a25f..e12d0f6241 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -1,10 +1,12 @@ package profile import ( + "io/ioutil" "net" "github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/config" + "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/hub/executor" "github.com/kr328/cfa/tun" @@ -40,12 +42,29 @@ func LoadDefault() { } // LoadFromFile - load file -func LoadFromFile(path string) error { - cfg, err := executor.ParseWithPath(path) +func LoadFromFile(path, baseDir string) error { + data, err := ioutil.ReadFile(path) if err != nil { return err } + rawCfg, err := config.UnmarshalRawConfig(data) + if err != nil { + return err + } + + rawCfg.ExternalController = "" + rawCfg.ExternalUI = "" + + fallbackBaseDir := constant.Path.HomeDir() + constant.SetHomeDir(baseDir) + + cfg, err := config.ParseRawConfig(rawCfg) + if err != nil { + constant.SetHomeDir(fallbackBaseDir) + return err + } + executor.ApplyConfig(cfg, true) if dns.DefaultResolver == nil && cfg.DNS.Enable { @@ -94,8 +113,3 @@ func LoadFromFile(path string) error { return nil } - -func CheckValid(data string) error { - _, err := config.Parse([]byte(data)) - return err -} diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index d3a1edb6eb..a345eb165f 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -9,9 +9,12 @@ import com.github.kr328.clash.core.event.ProcessEvent import com.github.kr328.clash.core.event.TrafficEvent import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.Proxy -import java.io.FileOutputStream -import java.lang.Exception +import com.github.kr328.clash.core.model.ProxyGroup +import com.github.kr328.clash.core.transact.ProxyCollectionImpl +import com.github.kr328.clash.core.transact.ProxyGroupCollectionImpl +import java.io.InputStream import java.lang.IllegalStateException +import java.util.concurrent.CompletableFuture class Clash( context: Context, @@ -33,21 +36,10 @@ class Clash( private var currentProcess = ProcessEvent.STOPPED init { - val home = context.filesDir.resolve(CLASH_DIR) - val countryDatabase = home.resolve("Country.mmdb") + val country = context.assets.open("Country.mmdb") + .use(InputStream::readBytes) - home.resolve("ui").mkdirs() - - if ( context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime > - countryDatabase.lastModified() ) { - FileOutputStream(countryDatabase).use { output -> - context.assets.open("Country.mmdb").use { input -> - input.copyTo(output) - } - } - } - - Bridge.setBaseDir(home.absolutePath) + Bridge.loadMMDB(country) } fun getCurrentProcessStatus(): ProcessEvent { @@ -90,58 +82,50 @@ class Clash( Bridge.stopTunDevice() } - fun loadProfile(path: String) { + fun loadProfile(path: String, baseDir: String) { enforceStarted() - Bridge.loadProfileFile(path) + Bridge.loadProfileFile(path, baseDir) } - fun checkProfileValid(data: String): String? { - return try { - Bridge.checkProfileValid(data) - null - } - catch (e: Exception) { - e.message - } + fun downloadProfile(url: String, output: String) { + Bridge.downloadProfileAndCheck(url, output) } - fun queryProxies(): List { - enforceStarted() - - val list = Bridge.queryAllProxies() - val result = mutableListOf() + fun saveProfile(data: ByteArray, output: String) { + Bridge.saveProfileAndCheck(data, output) + } - for (i in 0 until list.proxiesLength) { - val p = list.getProxiesElement(i) - val all = mutableListOf() + fun queryProxyGroups(): List { + enforceStarted() - for (index in 0 until p.allLength) { - all.add(p.getAllElement(index)) + return ProxyGroupCollectionImpl().also { Bridge.queryAllProxyGroups(it) } + .filterNotNull() + .map { group -> + ProxyGroup(group.name, + Proxy.Type.fromString(group.type), + group.delay, + group.current, + ProxyCollectionImpl().also { pc -> + group.queryAllProxies(pc) + }.filterNotNull().map { + Proxy(it.name, Proxy.Type.fromString(it.type), it.delay) + }) } - - result.add( - Proxy( - p.name, - Proxy.Type.fromString(p.type), - p.now, - all, - if ( p.delay < Short.MAX_VALUE ) p.delay else 0 - ) - ) - } - - return result } fun setSelectedProxy(name: String, selected: String): Boolean { return Bridge.setSelectedProxy(name, selected) } - fun startUrlTest(name: String, callback: (String, Long) -> Unit) { - Bridge.startUrlTest(name, DEFAULT_URL_TEST_URL, DEFAULT_URL_TEST_TIMEOUT.toLong()) { n, delay -> - callback(n, delay) + fun startUrlTest(name: String): CompletableFuture { + val future = CompletableFuture() + + Bridge.startUrlTest(name) { + future.complete(Unit) } + + return future } fun queryGeneral(): General { diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt b/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt index 19cf097730..edf783525d 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt @@ -6,8 +6,6 @@ import kotlinx.serialization.Serializable data class Proxy( val name: String, val type: Type, - val now: String, - val all: List, val delay: Long ) { enum class Type { diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt new file mode 100644 index 0000000000..aa22ba82e3 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt @@ -0,0 +1,12 @@ +package com.github.kr328.clash.core.model + +import kotlinx.serialization.Serializable + +@Serializable +data class ProxyGroup( + val name: String, + val type: Proxy.Type, + val delay: Long, + val current: String, + val proxies: List +) \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/transact/ProxyCollections.kt b/core/src/main/java/com/github/kr328/clash/core/transact/ProxyCollections.kt new file mode 100644 index 0000000000..ecbe6674d4 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/transact/ProxyCollections.kt @@ -0,0 +1,7 @@ +package com.github.kr328.clash.core.transact + +import bridge.* +import java.util.* + +class ProxyCollectionImpl : LinkedList(), ProxyCollection +class ProxyGroupCollectionImpl: LinkedList(), ProxyGroupCollection \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt index df3388232f..8697572778 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt @@ -48,7 +48,7 @@ class ClashServiceImpl(clashService: ClashService) : IClashService.Stub() { override fun queryAllProxies(): CompressedProxyList { return try { - clash.queryProxies().compress() + clash.queryProxyGroups().compress() } catch (e: Exception) { Log.w("Query proxies", e) @@ -65,14 +65,7 @@ class ClashServiceImpl(clashService: ClashService) : IClashService.Stub() { val count = AtomicInteger(proxies.size) proxies.forEach { - clash.startUrlTest(it) { n, d -> - callback.onResult(n, d) - - count.getAndDecrement() - - if ( count.get() == 0 ) - callback.onResult(null, 0) - } + clash.startUrlTest(it) } } From a44bb05345ec7bb6327dbd753975f4e8a4e54462 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 14 Jan 2020 15:45:57 +0800 Subject: [PATCH 065/358] [WIP] refactor services --- core/src/main/golang/bridge/callback.go | 5 ++ core/src/main/golang/bridge/profiles.go | 63 ++++++++++++++++-- core/src/main/golang/bridge/proxies.go | 6 +- core/src/main/golang/profile/download.go | 37 ++++++++++- .../java/com/github/kr328/clash/core/Clash.kt | 40 +++++------ .../kr328/clash/core/model/Compression.kt | 66 ------------------- .../clash/core/transact/DoneCallbackImpl.kt | 18 +++++ 7 files changed, 136 insertions(+), 99 deletions(-) delete mode 100644 core/src/main/java/com/github/kr328/clash/core/model/Compression.kt create mode 100644 core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt diff --git a/core/src/main/golang/bridge/callback.go b/core/src/main/golang/bridge/callback.go index ac94c40bdb..457e114fff 100644 --- a/core/src/main/golang/bridge/callback.go +++ b/core/src/main/golang/bridge/callback.go @@ -1 +1,6 @@ package bridge + +type DoneCallback interface { + Done() + DoneWithError(error) +} diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go index f10d66c3d2..e5c98b4240 100644 --- a/core/src/main/golang/bridge/profiles.go +++ b/core/src/main/golang/bridge/profiles.go @@ -1,15 +1,64 @@ package bridge -import "github.com/kr328/cfa/profile" +import ( + "github.com/kr328/cfa/profile" + "sync" +) -func LoadProfileFile(path, baseDir string) error { - return profile.LoadFromFile(path, baseDir) +var mutex sync.Mutex + +func LoadProfileFile(path, baseDir string, callback DoneCallback) { + go func() { + mutex.Lock() + defer mutex.Unlock() + + err := profile.LoadFromFile(path, baseDir) + if err != nil { + callback.DoneWithError(err) + } else { + callback.Done() + } + }() } -func DownloadProfileAndCheck(url, output string) error { - return profile.DownloadAndCheck(url, output) +func DownloadProfileAndCheck(url, output, baseDir string, callback DoneCallback) { + go func() { + mutex.Lock() + defer mutex.Unlock() + + err := profile.DownloadAndCheck(url, output, baseDir) + if err != nil { + callback.DoneWithError(err) + } else { + callback.Done() + } + }() } -func SaveProfileAndCheck(data []byte, output string) error { - return profile.SaveAndCheck(data, output) +func SaveProfileAndCheck(data []byte, output, baseDir string, callback DoneCallback) { + go func() { + mutex.Lock() + defer mutex.Unlock() + + err := profile.SaveAndCheck(data, output, baseDir) + if err != nil { + callback.DoneWithError(err) + } else { + callback.Done() + } + }() +} + +func MoveProfileAndCheck(source, target, baseDir string, callback DoneCallback) { + go func() { + mutex.Lock() + defer mutex.Unlock() + + err := profile.MoveAndCheck(source, target, baseDir) + if err != nil { + callback.DoneWithError(err) + } else { + callback.Done() + } + }() } diff --git a/core/src/main/golang/bridge/proxies.go b/core/src/main/golang/bridge/proxies.go index 71ca774550..118c31c1d3 100644 --- a/core/src/main/golang/bridge/proxies.go +++ b/core/src/main/golang/bridge/proxies.go @@ -33,10 +33,6 @@ type ProxyCollection interface { Add(proxy *ProxyItem) bool } -type UrlTestCallback interface { - Done() -} - func (p *ProxyGroupItem) QueryAllProxies(collection ProxyCollection) { for _, v := range p.providers { for _, p := range v.Proxies() { @@ -51,7 +47,7 @@ func (p *ProxyGroupItem) QueryAllProxies(collection ProxyCollection) { } } -func StartUrlTest(group string, callback UrlTestCallback) { +func StartUrlTest(group string, callback DoneCallback) { go func() { defer callback.Done() diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go index 167624f884..e5e1c21355 100644 --- a/core/src/main/golang/profile/download.go +++ b/core/src/main/golang/profile/download.go @@ -6,6 +6,7 @@ import ( "io/ioutil" "net" "net/http" + "os" "github.com/Dreamacro/clash/adapters/inbound" "github.com/Dreamacro/clash/component/socks5" @@ -43,7 +44,11 @@ var client = &http.Client{ }, } -func DownloadAndCheck(url, output string) error { +func DownloadAndCheck(url, output, baseDir string) error { + original := constant.Path.HomeDir() + constant.SetHomeDir(baseDir) + defer constant.SetHomeDir(original) + response, err := client.Get(url) if err != nil { return err @@ -63,7 +68,11 @@ func DownloadAndCheck(url, output string) error { return ioutil.WriteFile(output, data, defaultFileMode) } -func SaveAndCheck(data []byte, output string) error { +func SaveAndCheck(data []byte, output, baseDir string) error { + original := constant.Path.HomeDir() + constant.SetHomeDir(baseDir) + defer constant.SetHomeDir(original) + _, err := config.Parse(data) if err != nil { return err @@ -71,3 +80,27 @@ func SaveAndCheck(data []byte, output string) error { return ioutil.WriteFile(output, data, defaultFileMode) } + +func MoveAndCheck(source, target, baseDir string) error { + original := constant.Path.HomeDir() + constant.SetHomeDir(baseDir) + defer constant.SetHomeDir(original) + + buf, err := ioutil.ReadFile(source) + if err != nil { + return err + } + + _, err = config.Parse(buf) + if err != nil { + return err + } + + if err := ioutil.WriteFile(target, buf, defaultFileMode); err != nil { + return err + } + + os.Remove(source) + + return nil +} diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index a345eb165f..cfc7fe17b5 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -10,6 +10,7 @@ import com.github.kr328.clash.core.event.TrafficEvent import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.core.model.ProxyGroup +import com.github.kr328.clash.core.transact.DoneCallbackImpl import com.github.kr328.clash.core.transact.ProxyCollectionImpl import com.github.kr328.clash.core.transact.ProxyGroupCollectionImpl import java.io.InputStream @@ -20,13 +21,6 @@ class Clash( context: Context, private val listener: (ProcessEvent) -> Unit ) { - companion object { - const val CLASH_DIR = "clash" - - const val DEFAULT_URL_TEST_TIMEOUT = 5000 - const val DEFAULT_URL_TEST_URL = "https://www.gstatic.com/generate_204" - } - class Poll(private val poll: EventPoll) { fun stop() { poll.stop() @@ -82,18 +76,30 @@ class Clash( Bridge.stopTunDevice() } - fun loadProfile(path: String, baseDir: String) { + fun loadProfile(path: String, baseDir: String): CompletableFuture { enforceStarted() - Bridge.loadProfileFile(path, baseDir) + return DoneCallbackImpl().apply { + Bridge.loadProfileFile(path, baseDir, this) + } } - fun downloadProfile(url: String, output: String) { - Bridge.downloadProfileAndCheck(url, output) + fun downloadProfile(url: String, output: String, baseDir: String): CompletableFuture { + return DoneCallbackImpl().apply { + Bridge.downloadProfileAndCheck(url, output, baseDir,this) + } } - fun saveProfile(data: ByteArray, output: String) { - Bridge.saveProfileAndCheck(data, output) + fun saveProfile(data: ByteArray, output: String, baseDir: String): CompletableFuture { + return DoneCallbackImpl().apply { + Bridge.saveProfileAndCheck(data, output, baseDir, this) + } + } + + fun moveProfile(source: String, target: String, baseDir: String): CompletableFuture { + return DoneCallbackImpl().apply { + Bridge.moveProfileAndCheck(source, target, baseDir, this) + } } fun queryProxyGroups(): List { @@ -119,13 +125,9 @@ class Clash( } fun startUrlTest(name: String): CompletableFuture { - val future = CompletableFuture() - - Bridge.startUrlTest(name) { - future.complete(Unit) + return DoneCallbackImpl().apply { + Bridge.startUrlTest(name, this) } - - return future } fun queryGeneral(): General { diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt b/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt deleted file mode 100644 index dd266fe206..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/Compression.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.github.kr328.clash.core.model - -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.Parcels -import com.github.kr328.clash.core.utils.Log -import kotlinx.serialization.Serializable -import java.lang.IllegalStateException - -fun List.compress(): CompressedProxyList { - val nameMap = this.toList().mapIndexed { index, proxy -> - proxy.name to index - }.toMap() - - val elements = this.map { - CompressedProxyList.Element( - name = nameMap[it.name] ?: throw IllegalStateException("Unknown proxy ${it.name}"), - type = it.type, - now = nameMap[it.now] ?: -1, - all = it.all.mapNotNull { name -> nameMap[name] }, - delay = it.delay - ) - } - - return CompressedProxyList(nameMap.entries.associate { (k, v) -> v to k }, elements) -} - -@Serializable -data class CompressedProxyList(val proxyName: Map, val elements: List): Parcelable { - @Serializable - data class Element(val name: Int, - val type: Proxy.Type, - val now: Int, - val all: List, - val delay: Long) - - fun uncompress(): List { - return elements.map { - Proxy( - name = proxyName[it.name] ?: throw IllegalStateException("Unknown proxy $it"), - type = it.type, - now = proxyName[it.now] ?: "", - all = it.all.mapNotNull { index -> proxyName[index] }, - delay = it.delay - ) - } - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): CompressedProxyList { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt new file mode 100644 index 0000000000..0f3a77cd6e --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt @@ -0,0 +1,18 @@ +package com.github.kr328.clash.core.transact + +import bridge.DoneCallback +import java.lang.Exception +import java.util.concurrent.CompletableFuture + +class DoneCallbackImpl : DoneCallback, CompletableFuture() { + override fun doneWithError(e: Exception?) { + if ( e == null ) + complete(Unit) + else + completeExceptionally(e) + } + + override fun done() { + complete(Unit) + } +} \ No newline at end of file From fa6ac580d05fbb0c3b39da8f3941401caaeca6f3 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 16 Jan 2020 23:42:10 +0800 Subject: [PATCH 066/358] [WIP] refactor service --- .../java/com/github/kr328/clash/core/Clash.kt | 63 +++-- .../kr328/clash/core/event/BandwidthEvent.kt | 2 +- .../github/kr328/clash/core/event/Event.kt | 4 +- .../kr328/clash/core/event/EventStream.kt | 21 ++ .../github/kr328/clash/core/event/LogEvent.kt | 2 +- .../kr328/clash/core/model/ProxyGroup.kt | 24 +- .../clash/core/serialization/MergedParcels.kt | 218 ++++++++++++++++++ .../clash/callback/IUrlTestCallback.aidl | 5 - .../clash/service/IClashEventObserver.aidl | 13 -- .../clash/service/IClashEventService.aidl | 8 - ...{IClashService.aidl => IClashManager.aidl} | 20 +- .../clash/service/IClashProfileService.aidl | 12 - .../clash/service/ipc/IPCParcelables.aidl | 5 + .../service/ipc/ParcelableCompletedFuture.kt | 60 +++++ .../kr328/clash/service/ipc/ParcelablePipe.kt | 89 +++++++ .../clash/service/ipc/ParcelableResult.kt | 30 +++ 16 files changed, 495 insertions(+), 81 deletions(-) create mode 100644 core/src/main/java/com/github/kr328/clash/core/event/EventStream.kt create mode 100644 core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt delete mode 100644 service/src/main/aidl/com/github/kr328/clash/callback/IUrlTestCallback.aidl delete mode 100644 service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl delete mode 100644 service/src/main/aidl/com/github/kr328/clash/service/IClashEventService.aidl rename service/src/main/aidl/com/github/kr328/clash/service/{IClashService.aidl => IClashManager.aidl} (53%) delete mode 100644 service/src/main/aidl/com/github/kr328/clash/service/IClashProfileService.aidl create mode 100644 service/src/main/aidl/com/github/kr328/clash/service/ipc/IPCParcelables.aidl create mode 100644 service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index cfc7fe17b5..586ac7c0cd 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -3,10 +3,7 @@ package com.github.kr328.clash.core import android.content.Context import bridge.Bridge import bridge.EventPoll -import com.github.kr328.clash.core.event.BandwidthEvent -import com.github.kr328.clash.core.event.LogEvent -import com.github.kr328.clash.core.event.ProcessEvent -import com.github.kr328.clash.core.event.TrafficEvent +import com.github.kr328.clash.core.event.* import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.core.model.ProxyGroup @@ -15,6 +12,7 @@ import com.github.kr328.clash.core.transact.ProxyCollectionImpl import com.github.kr328.clash.core.transact.ProxyGroupCollectionImpl import java.io.InputStream import java.lang.IllegalStateException +import java.util.concurrent.BrokenBarrierException import java.util.concurrent.CompletableFuture class Clash( @@ -41,7 +39,7 @@ class Clash( } fun start() { - if ( currentProcess == ProcessEvent.STARTED ) + if (currentProcess == ProcessEvent.STARTED) return currentProcess = ProcessEvent.STARTED @@ -52,7 +50,7 @@ class Clash( } fun stop() { - if ( currentProcess == ProcessEvent.STOPPED ) + if (currentProcess == ProcessEvent.STOPPED) return currentProcess = ProcessEvent.STOPPED @@ -86,7 +84,7 @@ class Clash( fun downloadProfile(url: String, output: String, baseDir: String): CompletableFuture { return DoneCallbackImpl().apply { - Bridge.downloadProfileAndCheck(url, output, baseDir,this) + Bridge.downloadProfileAndCheck(url, output, baseDir, this) } } @@ -133,30 +131,49 @@ class Clash( fun queryGeneral(): General { val t = Bridge.queryGeneral() - return General(General.Mode.fromString(t.mode), - t.httpPort.toInt(), t.socksPort.toInt(), t.redirectPort.toInt()) + return General( + General.Mode.fromString(t.mode), + t.httpPort.toInt(), t.socksPort.toInt(), t.redirectPort.toInt() + ) } - fun pollTraffic(onEvent: (TrafficEvent) -> Unit): Poll { - return Poll(Bridge.pollTraffic { down, up -> - onEvent(TrafficEvent(down, up)) - }) + fun openTrafficEvent(): EventStream { + return object: EventStream() { + val traffic = Bridge.pollTraffic { down, up -> + send(TrafficEvent(down, up)) + } + + override fun onClose() { + traffic.stop() + } + } } - fun pollBandwidth(onEvent: (BandwidthEvent) -> Unit): Poll { - return Poll(Bridge.pollBandwidth { - onEvent(BandwidthEvent(it)) - }) + fun openBandwidthEvent(): EventStream { + return object: EventStream() { + val bandwidth = Bridge.pollBandwidth { + send(BandwidthEvent(it)) + } + + override fun onClose() { + bandwidth.stop() + } + } } - fun pollLogs(onEvent: (LogEvent) -> Unit): Poll { - return Poll(Bridge.pollLogs { level, payload -> - onEvent(LogEvent(LogEvent.Level.fromString(level), payload)) - }) + fun openLogEvent(): EventStream { + return object: EventStream() { + val log = Bridge.pollLogs { level, payload -> + send(LogEvent(LogEvent.Level.fromString(level), payload)) + } + + override fun onClose() { + log.stop() + } + } } private fun enforceStarted() { - if ( currentProcess == ProcessEvent.STOPPED ) - throw IllegalStateException("Clash Stopped") + check(currentProcess != ProcessEvent.STOPPED) { "Clash Stopped" } } } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/event/BandwidthEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/BandwidthEvent.kt index 62560cecdf..313b7ba22b 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/BandwidthEvent.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/BandwidthEvent.kt @@ -6,7 +6,7 @@ import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable @Serializable -data class BandwidthEvent(val total: Long): Parcelable { +data class BandwidthEvent(val total: Long): Event { override fun writeToParcel(parcel: Parcel, flags: Int) { Parcels.dump(serializer(), this, parcel) } diff --git a/core/src/main/java/com/github/kr328/clash/core/event/Event.kt b/core/src/main/java/com/github/kr328/clash/core/event/Event.kt index 06a284b829..08fa5e9102 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/Event.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/Event.kt @@ -1,6 +1,8 @@ package com.github.kr328.clash.core.event -interface Event { +import android.os.Parcelable + +interface Event: Parcelable { companion object { const val EVENT_LOG = 1 const val EVENT_TRAFFIC = 3 diff --git a/core/src/main/java/com/github/kr328/clash/core/event/EventStream.kt b/core/src/main/java/com/github/kr328/clash/core/event/EventStream.kt new file mode 100644 index 0000000000..275484e196 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/event/EventStream.kt @@ -0,0 +1,21 @@ +package com.github.kr328.clash.core.event + +import android.os.Parcelable + +abstract class EventStream { + private var on: (T) -> Unit = {} + + fun onEvent(callback: (T) -> Unit) { + on = callback + } + + fun close() { + onClose() + } + + fun send(event: T) { + on(event) + } + + abstract fun onClose() +} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt index fe98e5e394..016af6ae14 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt @@ -8,7 +8,7 @@ import kotlinx.serialization.internal.StringDescriptor @Serializable data class LogEvent(val level: Level, val message: String, val time: Long = System.currentTimeMillis()) : - Event, Parcelable { + Event { companion object { const val DEBUG_VALUE = "debug" const val INFO_VALUE = "info" diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt index aa22ba82e3..57cfa2f308 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt @@ -1,5 +1,9 @@ package com.github.kr328.clash.core.model +import android.os.Parcel +import android.os.Parcelable +import com.github.kr328.clash.core.serialization.MergedParcels +import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable @Serializable @@ -9,4 +13,22 @@ data class ProxyGroup( val delay: Long, val current: String, val proxies: List -) \ No newline at end of file +): Parcelable { + override fun writeToParcel(parcel: Parcel, flags: Int) { + MergedParcels.dump(serializer(), this, parcel) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ProxyGroup { + return MergedParcels.load(serializer(), parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt new file mode 100644 index 0000000000..98f7ff36f0 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt @@ -0,0 +1,218 @@ +package com.github.kr328.clash.core.serialization + +import android.os.Parcel +import kotlinx.serialization.* +import kotlinx.serialization.modules.EmptyModule +import kotlinx.serialization.modules.SerialModule + +object MergedParcels: AbstractSerialFormat(EmptyModule) { + fun dump(serializer: SerializationStrategy, obj: T, parcel: Parcel) { + val data = Parcel.obtain() + val encoder = ParcelsEncoder(data) + + try { + serializer.serialize(encoder, obj) + + data.setDataPosition(0) + + parcel.writeStringList(encoder.getStringList()) + parcel.appendFrom(data, 0, data.dataSize()) + } + finally { + data.recycle() + } + } + + fun load(deserializer: DeserializationStrategy, parcel: Parcel): T { + val strings = mutableListOf().apply { parcel.readStringList(this) } + return deserializer.deserialize(ParcelsDecoder(strings, parcel)) + } + + private class ParcelsEncoder(private val parcel: Parcel) : + Encoder, CompositeEncoder { + private val strings = mutableMapOf() + private var stringIndex = 0 + + fun getStringList(): List { + val result = mutableListOf() + strings.map { it.value to it.key } + .sortedBy { it.first } + .forEach { result.add(it.second) } + return result + } + + override val context: SerialModule + get() = EmptyModule + + override fun beginCollection( + desc: SerialDescriptor, + collectionSize: Int, + vararg typeParams: KSerializer<*> + ): CompositeEncoder { + encodeInt(collectionSize) + return super.beginCollection(desc, collectionSize, *typeParams) + } + + override fun encodeBooleanElement(desc: SerialDescriptor, index: Int, value: Boolean) = + encodeBoolean(value) + override fun encodeByteElement(desc: SerialDescriptor, index: Int, value: Byte) = + encodeByte(value) + override fun encodeCharElement(desc: SerialDescriptor, index: Int, value: Char) = + encodeChar(value) + override fun encodeDoubleElement(desc: SerialDescriptor, index: Int, value: Double) = + encodeDouble(value) + override fun encodeFloatElement(desc: SerialDescriptor, index: Int, value: Float) = + encodeFloat(value) + override fun encodeIntElement(desc: SerialDescriptor, index: Int, value: Int) = + encodeInt(value) + override fun encodeLongElement(desc: SerialDescriptor, index: Int, value: Long) = + encodeLong(value) + override fun encodeShortElement(desc: SerialDescriptor, index: Int, value: Short) = + encodeShort(value) + override fun encodeStringElement(desc: SerialDescriptor, index: Int, value: String) = + encodeString(value) + override fun encodeUnitElement(desc: SerialDescriptor, index: Int) = + encodeUnit() + + override fun encodeNonSerializableElement(desc: SerialDescriptor, index: Int, value: Any) = + throw IllegalArgumentException("Unsupported") + override fun encodeNullableSerializableElement( + desc: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T? + ) = encodeNullableSerializableValue(serializer, value) + override fun encodeSerializableElement( + desc: SerialDescriptor, + index: Int, + serializer: SerializationStrategy, + value: T + ) = encodeSerializableValue(serializer, value) + + override fun beginStructure( + desc: SerialDescriptor, + vararg typeParams: KSerializer<*> + ): CompositeEncoder = this + + override fun encodeBoolean(value: Boolean) = + parcel.writeByte(if ( value ) 1 else 0) + override fun encodeByte(value: Byte) = + parcel.writeByte(value) + override fun encodeChar(value: Char) = + parcel.writeInt(value.toInt()) + override fun encodeDouble(value: Double) = + parcel.writeDouble(value) + override fun encodeEnum(enumDescription: SerialDescriptor, ordinal: Int) = + parcel.writeInt(ordinal) + override fun encodeFloat(value: Float) = + parcel.writeFloat(value) + override fun encodeInt(value: Int) = + parcel.writeInt(value) + override fun encodeLong(value: Long) = + parcel.writeLong(value) + override fun encodeNotNullMark() = + encodeBoolean(true) + override fun encodeNull() = + encodeBoolean(false) + override fun encodeShort(value: Short) = + parcel.writeInt(value.toInt()) + override fun encodeUnit() {} + override fun encodeString(value: String) { + val index = strings.computeIfAbsent(value) { + stringIndex++ + } + parcel.writeInt(index) + } + } + + class ParcelsDecoder(private val strings: List, private val parcel: Parcel) : Decoder, CompositeDecoder { + override val context: SerialModule + get() = EmptyModule + override val updateMode: UpdateMode + get() = UpdateMode.BANNED + + override fun decodeElementIndex(desc: SerialDescriptor) = + CompositeDecoder.READ_ALL + override fun decodeCollectionSize(desc: SerialDescriptor) = + decodeInt() + + override fun decodeBooleanElement(desc: SerialDescriptor, index: Int) = + decodeBoolean() + override fun decodeByteElement(desc: SerialDescriptor, index: Int) = + decodeByte() + override fun decodeCharElement(desc: SerialDescriptor, index: Int) = + decodeChar() + override fun decodeDoubleElement(desc: SerialDescriptor, index: Int) = + decodeDouble() + override fun decodeFloatElement(desc: SerialDescriptor, index: Int) = + decodeFloat() + override fun decodeIntElement(desc: SerialDescriptor, index: Int) = + decodeInt() + override fun decodeShortElement(desc: SerialDescriptor, index: Int) = + decodeShort() + override fun decodeLongElement(desc: SerialDescriptor, index: Int) = + decodeLong() + override fun decodeStringElement(desc: SerialDescriptor, index: Int) = + decodeString() + override fun decodeUnitElement(desc: SerialDescriptor, index: Int) = + decodeUnit() + + override fun decodeNullableSerializableElement( + desc: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy + ) = decodeNullableSerializableValue(deserializer) + override fun decodeSerializableElement( + desc: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy + ) = decodeSerializableValue(deserializer) + + override fun updateNullableSerializableElement( + desc: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + old: T? + ) = updateNullableSerializableValue(deserializer, old) + + override fun updateSerializableElement( + desc: SerialDescriptor, + index: Int, + deserializer: DeserializationStrategy, + old: T + ) = updateSerializableValue(deserializer, old) + + override fun beginStructure( + desc: SerialDescriptor, + vararg typeParams: KSerializer<*> + ): CompositeDecoder = this + + override fun decodeBoolean() = + parcel.readByte() != 0.toByte() + override fun decodeByte() = + parcel.readByte() + override fun decodeChar() = + parcel.readInt().toChar() + override fun decodeDouble() = + parcel.readDouble() + override fun decodeEnum(enumDescription: SerialDescriptor) = + parcel.readInt() + override fun decodeFloat() = + parcel.readFloat() + override fun decodeInt() = + parcel.readInt() + override fun decodeLong() = + parcel.readLong() + override fun decodeNotNullMark() = + decodeBoolean() + override fun decodeNull() = + null + override fun decodeShort() = + parcel.readInt().toShort() + override fun decodeUnit() {} + override fun decodeString() = + strings[parcel.readInt()] + } +} + + diff --git a/service/src/main/aidl/com/github/kr328/clash/callback/IUrlTestCallback.aidl b/service/src/main/aidl/com/github/kr328/clash/callback/IUrlTestCallback.aidl deleted file mode 100644 index 5e6b8fbe6f..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/callback/IUrlTestCallback.aidl +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.kr328.clash.callback; - -interface IUrlTestCallback { - void onResult(String proxy, long delay); -} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl deleted file mode 100644 index dfbab90baa..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventObserver.aidl +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.kr328.clash.service; - -import com.github.kr328.clash.core.event.Event; - -interface IClashEventObserver { - void onProcessEvent(in ProcessEvent event); - void onLogEvent(in LogEvent event); - void onErrorEvent(in ErrorEvent event); - void onTrafficEvent(in TrafficEvent event); - void onBandwidthEvent(in BandwidthEvent event); - void onProfileChanged(in ProfileChangedEvent event); - void onProfileReloaded(in ProfileReloadEvent event); -} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashEventService.aidl deleted file mode 100644 index bb0beea1cf..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashEventService.aidl +++ /dev/null @@ -1,8 +0,0 @@ -package com.github.kr328.clash.service; - -import com.github.kr328.clash.service.IClashEventObserver; - -interface IClashEventService { - void registerEventObserver(String id, in IClashEventObserver observer, in int[] events); - void unregisterEventObserver(String id); -} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl similarity index 53% rename from service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl rename to service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index b8fd001dae..139b70d68e 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -4,29 +4,17 @@ 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.service.ipc.IPCParcelables; import com.github.kr328.clash.callback.IUrlTestCallback; import com.github.kr328.clash.core.event.Event; import com.github.kr328.clash.core.model.Packet; -interface IClashService { - // Services - IClashEventService getEventService(); - IClashProfileService getProfileService(); - IClashSettingService getSettingService(); - - // Status - ProcessEvent getCurrentProcessStatus(); - +interface IClashManager { // Control - void setSelectProxy(String proxy, String selected); - void start(); - void stop(); + ParcelableCompletedFuture setSelectProxy(String proxy, String selected); // Query CompressedProxyList queryAllProxies(); General queryGeneral(); - void startUrlTest(in String[] proxies, IUrlTestCallback callback); - - // Utils - String checkProfileValid(in ParcelFileDescriptor pipe); + ParcelableCompletedFuture startUrlTest(String group); } diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileService.aidl deleted file mode 100644 index 986c0831d5..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileService.aidl +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.kr328.clash.service; - -import com.github.kr328.clash.service.data.ClashProfileEntity; - -interface IClashProfileService { - ClashProfileEntity[] queryProfiles(); - void setActiveProfile(int id); - void addProfile(in ClashProfileEntity profile); - void removeProfile(int id); - void touchProfile(int id); - ClashProfileEntity queryActiveProfile(); -} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/ipc/IPCParcelables.aidl b/service/src/main/aidl/com/github/kr328/clash/service/ipc/IPCParcelables.aidl new file mode 100644 index 0000000000..16f4928f4a --- /dev/null +++ b/service/src/main/aidl/com/github/kr328/clash/service/ipc/IPCParcelables.aidl @@ -0,0 +1,5 @@ +package com.github.kr328.clash.service.ipc; + +parcelable ParcelablePipe; +parcelable ParcelableResult; +parcelable ParcelableCompletedFuture; diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt new file mode 100644 index 0000000000..70c9560ba9 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt @@ -0,0 +1,60 @@ +package com.github.kr328.clash.service.ipc + +import android.os.Parcel +import android.os.Parcelable +import android.os.RemoteException +import java.util.concurrent.CompletableFuture + +class ParcelableCompletedFuture(private val pipe: ParcelablePipe = ParcelablePipe()) : + CompletableFuture(), Parcelable { + constructor(parcel: Parcel) : this( + parcel.readParcelable( + ParcelableCompletedFuture::class.java.classLoader + ) ?: throw NullPointerException() + ) { + pipe.onReceive { + val result = (it ?: throw NullPointerException()) as ParcelableResult + + if ( result.exception != null ) { + completeExceptionally(RemoteException(result.exception)) + } + else { + complete(result.data) + } + } + } + + override fun complete(value: Parcelable?): Boolean { + if ( super.complete(value) ) { + pipe.send(ParcelableResult(value, null)) + return true + } + return false + } + + override fun completeExceptionally(ex: Throwable?): Boolean { + if ( super.completeExceptionally(ex) ) { + pipe.send(ParcelableResult(null, ex?.message ?: "Unknown")) + return true + } + return false + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(pipe, 0) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ParcelableCompletedFuture { + return ParcelableCompletedFuture(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt new file mode 100644 index 0000000000..e175ebe26b --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt @@ -0,0 +1,89 @@ +package com.github.kr328.clash.service.ipc + +import android.os.Binder +import android.os.IBinder +import android.os.Parcel +import android.os.Parcelable + +class ParcelablePipe() : Parcelable { + private inner class Slave: Binder() { + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + if ( code == TRANSACT_CODE_SEND_PARCELABLE ) { + receiveCallback(data.readParcelable(ParcelablePipe::class.java.classLoader)) + return true + } + return false + } + } + private inner class Master: Binder() { + override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { + if ( code == TRANSACT_CODE_SET_SLAVE ) { + slave = data.readStrongBinder() + return true + } + return false + } + } + + private var slave: IBinder? = null + private var receiveCallback: (Parcelable?) -> Unit = {} + + constructor(parcel: Parcel) : this() { + val master = parcel.readStrongBinder() + val data = Parcel.obtain() + + try { + data.writeStrongBinder(Slave()) + + master.transact(TRANSACT_CODE_SET_SLAVE, data, null, 0) + } + finally { + data.recycle() + } + } + + override fun writeToParcel(dest: Parcel?, flags: Int) { + if ( dest == null ) + return + + dest.writeStrongBinder(Master()) + } + + override fun describeContents(): Int = 0 + + fun send(parcelable: Parcelable?): Boolean { + val s = slave ?: return false + val data = Parcel.obtain() + + try { + data.writeParcelable(parcelable, 0) + + s.transact(TRANSACT_CODE_SEND_PARCELABLE, data, null, 0) + } + finally { + data.recycle() + } + + return true + } + + fun onReceive(callback: (Parcelable?) -> Unit) { + this.receiveCallback = callback + } + + companion object { + private const val TRANSACT_CODE_SET_SLAVE = 1 + private const val TRANSACT_CODE_SEND_PARCELABLE = 2 + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ParcelablePipe { + return ParcelablePipe(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt new file mode 100644 index 0000000000..cd3b28f771 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt @@ -0,0 +1,30 @@ +package com.github.kr328.clash.service.ipc + +import android.os.Parcel +import android.os.Parcelable + +data class ParcelableResult(val data: Parcelable?, val exception: String?): Parcelable { + constructor(parcel: Parcel) : this( + parcel.readParcelable(ParcelableResult::class.java.classLoader), + parcel.readString() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(data, 0) + parcel.writeString(exception) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ParcelableResult { + return ParcelableResult(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file From e979bec3217cc13cfef6f1efa98b171af75ed323 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Fri, 17 Jan 2020 00:17:40 +0800 Subject: [PATCH 067/358] [WIP] refactor service --- .../java/com/github/kr328/clash/core/Clash.kt | 2 +- .../github/kr328/clash/core/event/Event.aidl | 6 +- .../github/kr328/clash/core/model/Packet.aidl | 2 +- .../kr328/clash/service/IClashManager.aidl | 24 ++++--- .../kr328/clash/service/ClashManager.kt | 66 +++++++++++++++++++ .../kr328/clash/service/ClashServiceImpl.kt | 2 +- .../kr328/clash/service/ipc/ParcelablePipe.kt | 55 ++++++++++++---- 7 files changed, 126 insertions(+), 31 deletions(-) create mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashManager.kt diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 586ac7c0cd..b19430e786 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -122,7 +122,7 @@ class Clash( return Bridge.setSelectedProxy(name, selected) } - fun startUrlTest(name: String): CompletableFuture { + fun startHealthCheck(name: String): CompletableFuture { return DoneCallbackImpl().apply { Bridge.startUrlTest(name, this) } diff --git a/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl b/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl index dd25fe92b6..4769fec6e2 100644 --- a/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl @@ -1,9 +1,5 @@ package com.github.kr328.clash.core.event; -parcelable ProcessEvent; parcelable LogEvent; parcelable TrafficEvent; -parcelable BandwidthEvent; -parcelable ErrorEvent; -parcelable ProfileChangedEvent; -parcelable ProfileReloadEvent; \ No newline at end of file +parcelable BandwidthEvent; \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl b/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl index ba5c22586d..5477ae380f 100644 --- a/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl @@ -1,4 +1,4 @@ package com.github.kr328.clash.core.model; -parcelable CompressedProxyList; +parcelable ProxyGroup; parcelable General; \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index 139b70d68e..ca5b98d9f0 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -1,20 +1,26 @@ 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.service.ipc.IPCParcelables; -import com.github.kr328.clash.callback.IUrlTestCallback; -import com.github.kr328.clash.core.event.Event; import com.github.kr328.clash.core.model.Packet; interface IClashManager { // Control - ParcelableCompletedFuture setSelectProxy(String proxy, String selected); + boolean setSelectProxy(String proxy, String selected); + ParcelableCompletedFuture startHealthCheck(String group); // Query - CompressedProxyList queryAllProxies(); + ProxyGroup[] queryAllProxies(); General queryGeneral(); - ParcelableCompletedFuture startUrlTest(String group); + + // Profiles + ParcelableCompletedFuture addProfile(String url); + ParcelableCompletedFuture updateProfile(int id); + ParcelableCompletedFuture queryAllProfiles(); + + // Events + ParcelablePipe openBandwidthEvent(); + ParcelablePipe openLogEvent(); + + // Settings + } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt new file mode 100644 index 0000000000..6061070a3a --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -0,0 +1,66 @@ +package com.github.kr328.clash.service + +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.model.ProxyGroup +import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture +import com.github.kr328.clash.service.ipc.ParcelablePipe + +class ClashManager(val clash: Clash): IClashManager.Stub() { + override fun updateProfile(id: Int): ParcelableCompletedFuture { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun addProfile(url: String?): ParcelableCompletedFuture { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun queryAllProxies(): Array { + return clash.queryProxyGroups().toTypedArray() + } + + override fun queryGeneral(): General { + return clash.queryGeneral() + } + + override fun setSelectProxy(proxy: String?, selected: String?): Boolean { + require(proxy != null && selected != null) + + return clash.setSelectedProxy(proxy, selected) + } + + override fun openBandwidthEvent(): ParcelablePipe { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun queryAllProfiles(): ParcelableCompletedFuture { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun startHealthCheck(group: String?): ParcelableCompletedFuture { + require(group != null) + + return ParcelableCompletedFuture().apply { + clash.startHealthCheck(group).whenComplete { _: Unit?, u: Throwable? -> + if ( u != null ) + this.completeExceptionally(u) + else + this.complete(null) + } + } + } + + override fun openLogEvent(): ParcelablePipe { + return object: ParcelablePipe() { + val stream = clash.openLogEvent().apply { + onEvent { + send(it) + } + } + + override fun onClose() { + stream.close() + } + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt index 8697572778..b085a17f08 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt @@ -65,7 +65,7 @@ class ClashServiceImpl(clashService: ClashService) : IClashService.Stub() { val count = AtomicInteger(proxies.size) proxies.forEach { - clash.startUrlTest(it) + clash.startHealthCheck(it) } } diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt index e175ebe26b..f0275c3587 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt @@ -5,30 +5,40 @@ import android.os.IBinder import android.os.Parcel import android.os.Parcelable -class ParcelablePipe() : Parcelable { - private inner class Slave: Binder() { +open class ParcelablePipe private constructor(val type: Type) : Parcelable { + enum class Type { + MASTER, SLAVE + } + + private inner class Slave : Binder() { override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - if ( code == TRANSACT_CODE_SEND_PARCELABLE ) { + if (code == TRANSACT_CODE_SEND_PARCELABLE) { receiveCallback(data.readParcelable(ParcelablePipe::class.java.classLoader)) return true } return false } } - private inner class Master: Binder() { + + private inner class Master : Binder() { override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - if ( code == TRANSACT_CODE_SET_SLAVE ) { - slave = data.readStrongBinder() + if (code == TRANSACT_CODE_SET_SLAVE) { + connection = data.readStrongBinder() return true } + if (code == TRANSACT_CODE_CLOSE) { + onClose() + } return false } } - private var slave: IBinder? = null + private var connection: IBinder? = null private var receiveCallback: (Parcelable?) -> Unit = {} - constructor(parcel: Parcel) : this() { + constructor() : this(Type.MASTER) + + constructor(parcel: Parcel) : this(Type.SLAVE) { val master = parcel.readStrongBinder() val data = Parcel.obtain() @@ -36,14 +46,15 @@ class ParcelablePipe() : Parcelable { data.writeStrongBinder(Slave()) master.transact(TRANSACT_CODE_SET_SLAVE, data, null, 0) - } - finally { + + connection = master + } finally { data.recycle() } } override fun writeToParcel(dest: Parcel?, flags: Int) { - if ( dest == null ) + if (dest == null) return dest.writeStrongBinder(Master()) @@ -52,28 +63,44 @@ class ParcelablePipe() : Parcelable { override fun describeContents(): Int = 0 fun send(parcelable: Parcelable?): Boolean { - val s = slave ?: return false + if (type != Type.MASTER) + return false + + val s = connection ?: return false val data = Parcel.obtain() try { data.writeParcelable(parcelable, 0) s.transact(TRANSACT_CODE_SEND_PARCELABLE, data, null, 0) - } - finally { + } finally { data.recycle() } return true } + fun close() { + val s = connection ?: return + val data = Parcel.obtain() + + try { + s.transact(TRANSACT_CODE_CLOSE, data, null, 0) + } finally { + data.recycle() + } + } + fun onReceive(callback: (Parcelable?) -> Unit) { this.receiveCallback = callback } + open fun onClose() {} + companion object { private const val TRANSACT_CODE_SET_SLAVE = 1 private const val TRANSACT_CODE_SEND_PARCELABLE = 2 + private const val TRANSACT_CODE_CLOSE = 3 @JvmField val CREATOR = object : Parcelable.Creator { From 419c6301dd78e49fceff8bd38b87f90a8c9b2808 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 20 Jan 2020 12:09:11 +0800 Subject: [PATCH 068/358] [WIP] refactor services --- app/build.gradle | 3 +- core/src/main/golang/profile/download.go | 18 ++++- .../java/com/github/kr328/clash/core/Clash.kt | 41 ++--------- .../kr328/clash/service/IClashManager.aidl | 8 +-- .../clash/service/IClashProfileManager.aidl | 9 +++ .../kr328/clash/service/ClashManager.kt | 48 ++++++++----- .../clash/service/ClashProfileManager.kt | 24 +++++++ .../kr328/clash/service/data/ClashDatabase.kt | 4 +- .../service/data/ClashDatabaseMigrations.kt | 71 +++++++++++++++++++ .../clash/service/data/ClashProfileDao.kt | 3 + .../clash/service/data/ClashProfileEntity.kt | 36 +++++----- 11 files changed, 184 insertions(+), 81 deletions(-) create mode 100644 service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl create mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt diff --git a/app/build.gradle b/app/build.gradle index 9beda172e3..4b7af1d5d7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -53,8 +53,7 @@ dependencies { implementation "androidx.preference:preference-ktx:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" - implementation "com.charleskorn.kaml:kaml:0.14.0" implementation "com.google.android.material:material:1.2.0-alpha03" - implementation 'com.google.firebase:firebase-analytics:17.2.1' + implementation 'com.google.firebase:firebase-analytics:17.2.2' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' } diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go index e5e1c21355..19fbd49e5d 100644 --- a/core/src/main/golang/profile/download.go +++ b/core/src/main/golang/profile/download.go @@ -60,7 +60,7 @@ func DownloadAndCheck(url, output, baseDir string) error { return err } - _, err = config.Parse(data) + _, err = parseConfig(data) if err != nil { return err } @@ -73,7 +73,7 @@ func SaveAndCheck(data []byte, output, baseDir string) error { constant.SetHomeDir(baseDir) defer constant.SetHomeDir(original) - _, err := config.Parse(data) + _, err := parseConfig(data) if err != nil { return err } @@ -91,7 +91,7 @@ func MoveAndCheck(source, target, baseDir string) error { return err } - _, err = config.Parse(buf) + _, err = parseConfig(buf) if err != nil { return err } @@ -104,3 +104,15 @@ func MoveAndCheck(source, target, baseDir string) error { return nil } + +func parseConfig(data []byte) (*config.Config, error) { + raw, err := config.UnmarshalRawConfig(data) + if err != nil { + return nil, err + } + + raw.ExternalUI = "" + raw.ExternalController = "" + + return config.ParseRawConfig(raw) +} diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index b19430e786..df0d80812c 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -15,48 +15,31 @@ import java.lang.IllegalStateException import java.util.concurrent.BrokenBarrierException import java.util.concurrent.CompletableFuture -class Clash( - context: Context, - private val listener: (ProcessEvent) -> Unit -) { +object Clash{ + private var initialized = false + class Poll(private val poll: EventPoll) { fun stop() { poll.stop() } } - private var currentProcess = ProcessEvent.STOPPED + fun initialize(context: Context) { + if ( initialized ) + return + initialized = true - init { val country = context.assets.open("Country.mmdb") .use(InputStream::readBytes) Bridge.loadMMDB(country) } - fun getCurrentProcessStatus(): ProcessEvent { - return currentProcess - } - fun start() { - if (currentProcess == ProcessEvent.STARTED) - return - - currentProcess = ProcessEvent.STARTED - - listener(currentProcess) - Bridge.reset() } fun stop() { - if (currentProcess == ProcessEvent.STOPPED) - return - - currentProcess = ProcessEvent.STOPPED - - listener(currentProcess) - Bridge.reset() } @@ -65,8 +48,6 @@ class Clash( mtu: Int, dns: String ) { - enforceStarted() - Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns) } @@ -75,8 +56,6 @@ class Clash( } fun loadProfile(path: String, baseDir: String): CompletableFuture { - enforceStarted() - return DoneCallbackImpl().apply { Bridge.loadProfileFile(path, baseDir, this) } @@ -101,8 +80,6 @@ class Clash( } fun queryProxyGroups(): List { - enforceStarted() - return ProxyGroupCollectionImpl().also { Bridge.queryAllProxyGroups(it) } .filterNotNull() .map { group -> @@ -172,8 +149,4 @@ class Clash( } } } - - private fun enforceStarted() { - check(currentProcess != ProcessEvent.STOPPED) { "Clash Stopped" } - } } \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index ca5b98d9f0..4f9f9fe5e3 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -12,15 +12,11 @@ interface IClashManager { ProxyGroup[] queryAllProxies(); General queryGeneral(); - // Profiles - ParcelableCompletedFuture addProfile(String url); - ParcelableCompletedFuture updateProfile(int id); - ParcelableCompletedFuture queryAllProfiles(); - // Events ParcelablePipe openBandwidthEvent(); ParcelablePipe openLogEvent(); // Settings - + boolean putSetting(String key, String value); + String getSetting(String key); } diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl new file mode 100644 index 0000000000..63d05abcc0 --- /dev/null +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl @@ -0,0 +1,9 @@ +package com.github.kr328.clash.service; + +import com.github.kr328.clash.service.ipc.IPCParcelables; + +interface IClashProfileManager { + ParcelableCompletedFuture addProfile(String url); + ParcelableCompletedFuture updateProfile(int id); + ParcelableCompletedFuture queryAllProfiles(); +} diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index 6061070a3a..9fbd7cc94e 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -1,47 +1,50 @@ package com.github.kr328.clash.service +import android.content.Context +import android.content.SharedPreferences +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.ProxyGroup import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture import com.github.kr328.clash.service.ipc.ParcelablePipe -class ClashManager(val clash: Clash): IClashManager.Stub() { - override fun updateProfile(id: Int): ParcelableCompletedFuture { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } - - override fun addProfile(url: String?): ParcelableCompletedFuture { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } +class ClashManager(context: Context): IClashManager.Stub() { + private val settings = context.getSharedPreferences("service", Context.MODE_PRIVATE) override fun queryAllProxies(): Array { - return clash.queryProxyGroups().toTypedArray() + return Clash.queryProxyGroups().toTypedArray() } override fun queryGeneral(): General { - return clash.queryGeneral() + return Clash.queryGeneral() } override fun setSelectProxy(proxy: String?, selected: String?): Boolean { require(proxy != null && selected != null) - return clash.setSelectedProxy(proxy, selected) + return Clash.setSelectedProxy(proxy, selected) } override fun openBandwidthEvent(): ParcelablePipe { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. - } + return object: ParcelablePipe() { + val stream = Clash.openBandwidthEvent().apply { + onEvent { + send(it) + } + } - override fun queryAllProfiles(): ParcelableCompletedFuture { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun onClose() { + stream.close() + } + } } override fun startHealthCheck(group: String?): ParcelableCompletedFuture { require(group != null) return ParcelableCompletedFuture().apply { - clash.startHealthCheck(group).whenComplete { _: Unit?, u: Throwable? -> + Clash.startHealthCheck(group).whenComplete { _: Unit?, u: Throwable? -> if ( u != null ) this.completeExceptionally(u) else @@ -52,7 +55,7 @@ class ClashManager(val clash: Clash): IClashManager.Stub() { override fun openLogEvent(): ParcelablePipe { return object: ParcelablePipe() { - val stream = clash.openLogEvent().apply { + val stream = Clash.openLogEvent().apply { onEvent { send(it) } @@ -63,4 +66,15 @@ class ClashManager(val clash: Clash): IClashManager.Stub() { } } } + + override fun putSetting(key: String?, value: String?): Boolean { + settings.edit { + putString(key, value) + } + return true + } + + override fun getSetting(key: String?): String { + return settings.getString(key, "")!! + } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt new file mode 100644 index 0000000000..428bbe7f8f --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt @@ -0,0 +1,24 @@ +package com.github.kr328.clash.service + +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture + +class ClashProfileManager(private val database: ClashDatabase): IClashProfileManager.Stub() { + override fun updateProfile(id: Int): ParcelableCompletedFuture { + val entity = database.openClashProfileDao().queryProfileById(id) + + require(entity != null && entity.type == ClashProfileEntity.Type.URL) + + val result = ParcelableCompletedFuture() + } + + override fun queryAllProfiles(): ParcelableCompletedFuture { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + override fun addProfile(url: String?): ParcelableCompletedFuture { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt index 42944d74fe..9eef229d39 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt @@ -5,7 +5,7 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -@Database(version = 1, exportSchema = false, entities = [ClashProfileEntity::class, ClashProfileProxyEntity::class]) +@Database(version = 2, exportSchema = false, entities = [ClashProfileEntity::class, ClashProfileProxyEntity::class]) abstract class ClashDatabase : RoomDatabase() { abstract fun openClashProfileDao(): ClashProfileDao abstract fun openClashProfileProxyDao(): ClashProfileProxyDao @@ -19,7 +19,7 @@ abstract class ClashDatabase : RoomDatabase() { context.applicationContext, ClashDatabase::class.java, "clash-config" - ).build() + ).addMigrations(ClashDatabaseMigrations.VERSION_1_2).build() return instance ?: throw NullPointerException() } } diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt new file mode 100644 index 0000000000..5a8ba2b712 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt @@ -0,0 +1,71 @@ +package com.github.kr328.clash.service.data + +import android.content.ContentValues +import android.database.sqlite.SQLiteDatabase +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import java.security.SecureRandom +import kotlin.math.absoluteValue + +object ClashDatabaseMigrations { + val VERSION_1_2 = object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + database.beginTransaction() + + database.execSQL("ALTER TABLE profiles RENAME TO _profiles") + + database.execSQL("CREATE TABLE profiles(" + + "name TEXT NOT NULL, " + + "type INTEGER NOT NULL, " + + "uri TEXT NOT NULL, " + + "file TEXT NOT NULL, " + + "base TEXT NOT NULL" + + "active INTEGER NOT NULL, " + + "last_update INTEGER NOT NULL, " + + "id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)") + + val cursor = database.query("SELECT name, token, file, active, last_update FROM _profile") + val random = SecureRandom() + val bases = mutableSetOf() + + cursor.moveToFirst() + while ( !cursor.isAfterLast ) { + // old + val name = cursor.getString(0) + val token = cursor.getString(1) + val file = cursor.getString(2) + val active = cursor.getInt(3) + val lastUpdate = cursor.getLong(4) + + // new + val type = when { + token.startsWith("url") -> ClashProfileEntity.Type.URL.id + token.startsWith("file") -> ClashProfileEntity.Type.FILE.id + else -> ClashProfileEntity.Type.UNKNOWN.id + } + val uri = token.removePrefix("url|").removePrefix("file|") + var base = random.nextLong().absoluteValue + + while ( bases.contains(base) ) + base = random.nextLong().absoluteValue + + database.insert("profiles", + SQLiteDatabase.CONFLICT_ABORT, + ContentValues().apply { + put("name", name) + put("type", type) + put("uri", uri) + put("file", file) + put("active", active) + put("last_update", lastUpdate) + put("base", base.toString()) + }) + } + cursor.close() + + database.execSQL("DROP TABLE _profiles") + + database.endTransaction() + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt index 2a4f7d8a89..f55744b5c0 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt @@ -16,6 +16,9 @@ interface ClashProfileDao { @Query("SELECT * FROM profiles") fun queryProfiles(): Array + @Query("SELECT * FROM profiles WHERE id = :id") + fun queryProfileById(id: Int): ClashProfileEntity? + @Insert(onConflict = OnConflictStrategy.REPLACE) fun addProfile(profile: ClashProfileEntity) diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index 66bcd8d1e5..353996133b 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -5,6 +5,7 @@ import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import androidx.room.TypeConverter import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable @@ -12,12 +13,18 @@ import kotlinx.serialization.Serializable @Serializable data class ClashProfileEntity( @ColumnInfo(name = "name") val name: String, - @ColumnInfo(name = "token") val token: String, + @ColumnInfo(name = "type") val type: Type, + @ColumnInfo(name = "uri") val uri: String, @ColumnInfo(name = "file") val file: String, + @ColumnInfo(name = "base") val base: String, @ColumnInfo(name = "active") val active: Boolean, @ColumnInfo(name = "last_update") val lastUpdate: Long, @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Int = 0 ) : Parcelable { + enum class Type(val id: Int) { + URL(1), FILE(2), UNKNOWN(-1) + } + override fun writeToParcel(parcel: Parcel, flags: Int) { Parcels.dump(serializer(), this, parcel) } @@ -27,24 +34,19 @@ data class ClashProfileEntity( } companion object { - fun fileToken(content: String): String { - return "file|$content" + @TypeConverter + fun typeToInt(value: Type?): Int { + return value?.id ?: -1 } - fun urlToken(content: String): String { - return "url|$content" - } - - fun isFileToken(token: String): Boolean { - return token.startsWith("file|") - } - - fun isUrlToken(token: String): Boolean { - return token.startsWith("url|") - } - - fun getUrl(token: String): String { - return token.removePrefix("url|") + @TypeConverter + fun intToType(value: Int?): Type { + return when ( value ) { + Type.URL.id -> Type.URL + Type.FILE.id -> Type.FILE + Type.UNKNOWN.id -> Type.UNKNOWN + else -> Type.UNKNOWN + } } @JvmField From 4ee8679d2915833e0589f06bfbe93e4977992a62 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 20 Jan 2020 23:31:14 +0800 Subject: [PATCH 069/358] [WIP] refactor service --- .../java/com/github/kr328/clash/Constants.kt | 1 + .../java/com/github/kr328/clash/core/Clash.kt | 17 +-- .../clash/service/IClashProfileManager.aidl | 5 +- .../clash/service/ClashProfileManager.kt | 102 ++++++++++++++++-- .../github/kr328/clash/service/Constants.kt | 3 + .../service/ipc/ParcelableCompletedFuture.kt | 3 +- .../clash/service/util/DefaultThreadPool.kt | 6 ++ .../kr328/clash/service/util/FileUtils.kt | 21 ++++ 8 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt diff --git a/app/src/main/java/com/github/kr328/clash/Constants.kt b/app/src/main/java/com/github/kr328/clash/Constants.kt index 391d6c5617..cd98ccc8cc 100644 --- a/app/src/main/java/com/github/kr328/clash/Constants.kt +++ b/app/src/main/java/com/github/kr328/clash/Constants.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash object Constants { + const val CLASH_DIR = "clash" const val PROFILES_DIR = "profiles" } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index df0d80812c..531c7067e2 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -10,6 +10,7 @@ import com.github.kr328.clash.core.model.ProxyGroup 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 java.io.File import java.io.InputStream import java.lang.IllegalStateException import java.util.concurrent.BrokenBarrierException @@ -55,27 +56,27 @@ object Clash{ Bridge.stopTunDevice() } - fun loadProfile(path: String, baseDir: String): CompletableFuture { + fun loadProfile(path: File, baseDir: File): CompletableFuture { return DoneCallbackImpl().apply { - Bridge.loadProfileFile(path, baseDir, this) + Bridge.loadProfileFile(path.absolutePath, baseDir.absolutePath, this) } } - fun downloadProfile(url: String, output: String, baseDir: String): CompletableFuture { + fun downloadProfile(url: String, output: File, baseDir: File): CompletableFuture { return DoneCallbackImpl().apply { - Bridge.downloadProfileAndCheck(url, output, baseDir, this) + Bridge.downloadProfileAndCheck(url, output.absolutePath, baseDir.absolutePath, this) } } - fun saveProfile(data: ByteArray, output: String, baseDir: String): CompletableFuture { + fun saveProfile(data: ByteArray, output: File, baseDir: File): CompletableFuture { return DoneCallbackImpl().apply { - Bridge.saveProfileAndCheck(data, output, baseDir, this) + Bridge.saveProfileAndCheck(data, output.absolutePath, baseDir.absolutePath, this) } } - fun moveProfile(source: String, target: String, baseDir: String): CompletableFuture { + fun moveProfile(source: File, target: File, baseDir: File): CompletableFuture { return DoneCallbackImpl().apply { - Bridge.moveProfileAndCheck(source, target, baseDir, this) + Bridge.moveProfileAndCheck(source.absolutePath, target.absolutePath, baseDir.absolutePath, this) } } diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl index 63d05abcc0..9a4584064a 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl @@ -1,9 +1,10 @@ package com.github.kr328.clash.service; import com.github.kr328.clash.service.ipc.IPCParcelables; +import com.github.kr328.clash.service.data.ClashProfileEntity; interface IClashProfileManager { - ParcelableCompletedFuture addProfile(String url); + ParcelableCompletedFuture addProfile(String name, int type, String uri); ParcelableCompletedFuture updateProfile(int id); - ParcelableCompletedFuture queryAllProfiles(); + ClashProfileEntity[] queryAllProfiles(); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt index 428bbe7f8f..495b92dcd6 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt @@ -1,24 +1,114 @@ package com.github.kr328.clash.service +import android.content.Context +import android.net.Uri import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture +import com.github.kr328.clash.service.util.DefaultThreadPool +import com.github.kr328.clash.service.util.FileUtils + +class ClashProfileManager(private val context: Context, private val database: ClashDatabase) : + IClashProfileManager.Stub() { + private val clashDir = context.filesDir.resolve(Constants.CLASH_DIR) + private val profileDir = context.filesDir.resolve(Constants.PROFILES_DIR) -class ClashProfileManager(private val database: ClashDatabase): IClashProfileManager.Stub() { override fun updateProfile(id: Int): ParcelableCompletedFuture { val entity = database.openClashProfileDao().queryProfileById(id) - require(entity != null && entity.type == ClashProfileEntity.Type.URL) + require(entity != null && (entity.type == ClashProfileEntity.Type.URL || + entity.type == ClashProfileEntity.Type.FILE)) val result = ParcelableCompletedFuture() + + downloadProfile(Uri.parse(entity.uri), entity.file, entity.base, + onSuccess = { + result.complete(null) + + database.openClashProfileDao().touchProfile(id) + }, + onFailure = { + result.completeExceptionally(it) + }) + + return result } - override fun queryAllProfiles(): ParcelableCompletedFuture { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + override fun queryAllProfiles(): Array { + return database.openClashProfileDao().queryProfiles() + } + + override fun addProfile(name: String, type: Int, uri: String?): ParcelableCompletedFuture { + require(uri != null && (uri.startsWith("http") || uri.startsWith("content"))) + + val result = ParcelableCompletedFuture() + val fileName = FileUtils.generateRandomFileName(profileDir, ".yaml") + val baseDirName = FileUtils.generateRandomFileName(clashDir) + + downloadProfile(Uri.parse(uri), fileName, baseDirName, + onSuccess = { + database.openClashProfileDao().addProfile( + ClashProfileEntity( + name, + ClashProfileEntity.intToType(type), + uri, + fileName, + baseDirName, + false, + System.currentTimeMillis() + ) + ) + + result.complete(null) + }, + onFailure = { + result.completeExceptionally(it) + }) + + return result } - override fun addProfile(url: String?): ParcelableCompletedFuture { - TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + private fun downloadProfile( + uri: Uri?, + fileName: String, + baseDirName: String, + onSuccess: () -> Unit, + onFailure: (Throwable) -> Unit + ) { + require(uri != null && uri != Uri.EMPTY) + + if (uri.scheme == "http" || uri.scheme == "https") { + Clash.downloadProfile( + uri.toString(), + profileDir.resolve(fileName), + clashDir.resolve(baseDirName) + ) + .whenComplete { _, u -> + if (u != null) + onFailure(u) + else + onSuccess() + } + } else { + DefaultThreadPool.submit { + try { + val input = context.contentResolver.openInputStream(uri) + ?: throw NullPointerException("Unable to open profile") + + input.use { + Clash.saveProfile( + it.readBytes(), + profileDir.resolve(fileName), + clashDir.resolve(baseDirName) + ) + } + + onSuccess() + } catch (e: Exception) { + onFailure(e) + } + } + } } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/Constants.kt b/service/src/main/java/com/github/kr328/clash/service/Constants.kt index 925e5305a4..8e6b4ae2c1 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Constants.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Constants.kt @@ -3,4 +3,7 @@ package com.github.kr328.clash.service object Constants { const val CLASH_PROCESS_BROADCAST_ACTION = "com.github.kr328.clash.ClashService.ClashProcessEvent" const val CLASH_RELOAD_BROADCAST_ACTION = "com.github.kr328.clash.ClashService.ProfileReloadEvent" + + const val CLASH_DIR = "clash" + const val PROFILES_DIR = "profiles" } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt index 70c9560ba9..56488c5742 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt @@ -5,8 +5,9 @@ import android.os.Parcelable import android.os.RemoteException import java.util.concurrent.CompletableFuture -class ParcelableCompletedFuture(private val pipe: ParcelablePipe = ParcelablePipe()) : +class ParcelableCompletedFuture private constructor(private val pipe: ParcelablePipe) : CompletableFuture(), Parcelable { + constructor(): this(ParcelablePipe()) constructor(parcel: Parcel) : this( parcel.readParcelable( ParcelableCompletedFuture::class.java.classLoader diff --git a/service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt b/service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt new file mode 100644 index 0000000000..85a77830b2 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt @@ -0,0 +1,6 @@ +package com.github.kr328.clash.service.util + +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors + +val DefaultThreadPool: ExecutorService = Executors.newCachedThreadPool() \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt new file mode 100644 index 0000000000..aecba39fff --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt @@ -0,0 +1,21 @@ +package com.github.kr328.clash.service.util + +import java.io.File +import java.security.SecureRandom +import kotlin.math.absoluteValue + +object FileUtils { + private val random = SecureRandom() + + fun generateRandomFileName(dir: File, suffix: String = ""): String { + dir.mkdirs() + + var fileName: String + + do { + fileName = random.nextLong().absoluteValue.toString() + suffix + } while (dir.resolve(fileName).exists()) + + return fileName + } +} \ No newline at end of file From c60d8cfb53f0ce6aff190bfc81ca6c6dc70283d9 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 21 Jan 2020 13:24:58 +0800 Subject: [PATCH 070/358] [WIP] refactor service --- app/src/main/AndroidManifest.xml | 1 + .../kr328/clash/SettingAccessActivity.kt | 1 - .../kr328/clash/service/ClashEventBridge.kt | 36 --- .../kr328/clash/service/ClashEventPoll.kt | 84 ------- .../kr328/clash/service/ClashEventService.kt | 147 ------------- .../kr328/clash/service/ClashNotification.kt | 65 +++++- .../clash/service/ClashProfileService.kt | 72 ------ .../kr328/clash/service/ClashService.kt | 207 ++++-------------- .../kr328/clash/service/ClashServiceImpl.kt | 112 ---------- .../clash/service/ClashSettingService.kt | 69 ------ .../com/github/kr328/clash/service/Intents.kt | 9 + .../github/kr328/clash/service/Settings.kt | 76 +++++++ .../github/kr328/clash/service/TunService.kt | 183 ++++++---------- 13 files changed, 252 insertions(+), 810 deletions(-) delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashSettingService.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/Intents.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/Settings.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fff4b5c57a..efa0fbdba6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -23,6 +23,7 @@ android:screenOrientation="portrait"> + 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 90a585f115..ae0523d943 100644 --- a/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt @@ -9,7 +9,6 @@ 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 diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt deleted file mode 100644 index 49dbcc6087..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventBridge.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.github.kr328.clash.service - -import com.github.kr328.clash.core.event.* - -class ClashEventBridge(val service: ClashService): ClashProfileService.Master, - ClashEventService.Master, ClashEventPoll.Master { - val eventService: ClashEventService = ClashEventService(this) - - override fun preformProfileChanged() { - eventService.performProfileChangedEvent(ProfileChangedEvent()) - } - - override fun acquireEvent(event: Int) { - service.acquireEvent(event) - } - - override fun releaseEvent(event: Int) { - service.releaseEvent(event) - } - - fun onProcessChanged(event: ProcessEvent) { - eventService.performProcessEvent(event) - } - - override fun onLogEvent(event: LogEvent) { - eventService.performLogEvent(event) - } - - override fun onTrafficEvent(event: TrafficEvent) { - eventService.performSpeedEvent(event) - } - - override fun onBandwidthEvent(event: BandwidthEvent) { - eventService.performBandwidthEvent(event) - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt deleted file mode 100644 index 0415b46f4f..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventPoll.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.kr328.clash.service - -import android.os.Handler -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.core.event.BandwidthEvent -import com.github.kr328.clash.core.event.LogEvent -import com.github.kr328.clash.core.event.TrafficEvent - -class ClashEventPoll(private val clash: Clash, private val master: Master) { - interface Master { - fun onLogEvent(event: LogEvent) - fun onTrafficEvent(event: TrafficEvent) - fun onBandwidthEvent(event: BandwidthEvent) - } - - private val handler = Handler() - - private var traffic: Clash.Poll? = null - private var bandwidth: Clash.Poll? = null - private var logs: Clash.Poll? = null - - fun startTrafficPoll() { - handler.post { - if ( traffic != null ) - return@post - - traffic = clash.pollTraffic { - master.onTrafficEvent(it) - } - } - } - - fun stopTrafficPoll() { - handler.post { - traffic?.stop() - - traffic = null - } - } - - fun startBandwidthPoll() { - handler.post { - if ( bandwidth != null ) - return@post - - bandwidth = clash.pollBandwidth { - master.onBandwidthEvent(it) - } - } - } - - fun stopBandwidthPoll() { - handler.post { - bandwidth?.stop() - - bandwidth = null - } - } - - fun startLogsPoll() { - handler.post { - if ( logs != null ) - return@post - - logs = clash.pollLogs { - master.onLogEvent(it) - } - } - } - - fun stopLogPoll() { - handler.post { - logs?.stop() - - logs = null - } - } - - fun shutdown() { - stopTrafficPoll() - stopBandwidthPoll() - stopLogPoll() - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt deleted file mode 100644 index 782a0b7492..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashEventService.kt +++ /dev/null @@ -1,147 +0,0 @@ -package com.github.kr328.clash.service - -import com.github.kr328.clash.core.event.* -import java.util.concurrent.Executors - -class ClashEventService(private val master: Master) : IClashEventService.Stub() { - interface Master { - fun acquireEvent(event: Int) - fun releaseEvent(event: Int) - } - - companion object { - private val EVENT_SET = - setOf(Event.EVENT_LOG, Event.EVENT_TRAFFIC, Event.EVENT_BANDWIDTH) - } - - private data class EventObserverRecord( - val observer: IClashEventObserver, - val acquiredEvent: Set - ) - - private val observers = mutableMapOf() - private val executor = Executors.newSingleThreadExecutor() - - private var currentProcessEvent = ProcessEvent.STOPPED - - fun performProcessEvent(event: ProcessEvent) { - if (!executor.isShutdown) - executor.submit { - currentProcessEvent = event - - observers.values.forEach { - it.observer.onProcessEvent(event) - } - } - } - - fun performLogEvent(event: LogEvent) { - if (!executor.isShutdown) - executor.submit { - observers.values.forEach { - if (it.acquiredEvent.contains(Event.EVENT_LOG)) - it.observer.onLogEvent(event) - } - } - } - - fun performSpeedEvent(event: TrafficEvent) { - if (!executor.isShutdown) - executor.submit { - observers.values.forEach { - if (it.acquiredEvent.contains(Event.EVENT_TRAFFIC)) - it.observer.onTrafficEvent(event) - } - } - } - - fun performBandwidthEvent(event: BandwidthEvent) { - if (!executor.isShutdown) - executor.submit { - observers.values.forEach { - if (it.acquiredEvent.contains(Event.EVENT_BANDWIDTH)) - it.observer.onBandwidthEvent(event) - } - } - } - - fun performErrorEvent(event: ErrorEvent) { - if (!executor.isShutdown) - executor.submit { - observers.values.forEach { - it.observer.onErrorEvent(event) - } - } - } - - fun performProfileChangedEvent(event: ProfileChangedEvent) { - if (!executor.isShutdown) - executor.submit { - observers.values.forEach { - it.observer.onProfileChanged(event) - } - } - } - - fun performProfileReloadEvent(event: ProfileReloadEvent) { - if (!executor.isShutdown) - executor.submit { - observers.values.forEach { - it.observer.onProfileReloaded(event) - } - } - } - - override fun unregisterEventObserver(id: String?) { - if (!executor.isShutdown) - executor.submit { - require(id != null) - - observers.remove(id) - - recastEventRequirement() - } - } - - override fun registerEventObserver( - id: String?, - observer: IClashEventObserver?, - events: IntArray? - ) { - if (!executor.isShutdown) - executor.submit { - require(id != null && observer != null && events != null) - - val initial = !observers.containsKey(id) - - observers[id] = EventObserverRecord(observer, events.toSet()) - - observer.asBinder().linkToDeath({ - unregisterEventObserver(id) - }, 0) - - recastEventRequirement() - - if (initial) { - observer.onProcessEvent(currentProcessEvent) - } - } - } - - fun recastEventRequirement() { - if (!executor.isShutdown) - executor.submit { - val req = observers.values.flatMap { - it.acquiredEvent - }.toSet() - val rel = EVENT_SET - req - - req.forEach(master::acquireEvent) - rel.forEach(master::releaseEvent) - } - } - - fun shutdown() { - executor.shutdown() - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 0483c442b2..660586fc03 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -1,12 +1,15 @@ package com.github.kr328.clash.service import android.app.* -import android.content.ComponentName -import android.content.Intent +import android.content.* import android.os.Build import android.os.Handler +import android.os.PowerManager import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.event.EventStream +import com.github.kr328.clash.core.event.TrafficEvent import com.github.kr328.clash.core.utils.ByteFormatter class ClashNotification(private val context: Service) { @@ -24,7 +27,6 @@ class ClashNotification(private val context: Service) { .setSmallIcon(R.drawable.ic_notification_icon) .setOngoing(true) .setColor(context.getColor(R.color.colorAccentService)) - //.setColorized(true) .setOnlyAlertOnce(true) .setShowWhen(false) .setContentIntent( @@ -47,6 +49,17 @@ class ClashNotification(private val context: Service) { private var up = 0L private var down = 0L private var profile = "None" + private var traffic: EventStream? = null + private val observer = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when (intent?.action) { + Intent.ACTION_SCREEN_ON -> + enableUpdate() + Intent.ACTION_SCREEN_OFF -> + disableUpdate() + } + } + } init { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -59,18 +72,27 @@ class ClashNotification(private val context: Service) { ) ) } - } - fun show() { handler.post { showing = true update() } + + context.registerReceiver(observer, IntentFilter().apply { + addAction(Intent.ACTION_SCREEN_ON) + addAction(Intent.ACTION_SCREEN_OFF) + }) + + if (context.getSystemService(PowerManager::class.java)!!.isInteractive) { + enableUpdate() + } } - fun cancel() { + fun destroy() { handler.post { + disableUpdate() + if (showing) context.stopForeground(true) @@ -86,7 +108,15 @@ class ClashNotification(private val context: Service) { } } - fun setSpeed(up: Long, down: Long) { + fun setVpn(vpn: Boolean) { + handler.post { + this.vpn = vpn + + update() + } + } + + private fun setSpeed(up: Long, down: Long) { handler.post { this.up = up this.down = down @@ -95,11 +125,26 @@ class ClashNotification(private val context: Service) { } } - fun setVpn(vpn: Boolean) { + private fun enableUpdate() { handler.post { - this.vpn = vpn + if (traffic != null) + return@post + + traffic = Clash.openTrafficEvent().apply { + onEvent { + setSpeed(it.up, it.down) + } + } + } + } - update() + private fun disableUpdate() { + handler.post { + if (traffic == null) + return@post + + traffic?.close() + traffic = null } } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt deleted file mode 100644 index 31dd07ab49..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt +++ /dev/null @@ -1,72 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.Context -import com.github.kr328.clash.service.data.ClashDatabase -import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.service.data.ClashProfileProxyEntity - -class ClashProfileService(context: Context, private val master: Master) : - IClashProfileService.Stub() { - interface Master { - fun preformProfileChanged() - } - - private val profileDao by lazy { - ClashDatabase.getInstance(context).openClashProfileDao() - } - private val profileProxyDao by lazy { - ClashDatabase.getInstance(context).openClashProfileProxyDao() - } - - override fun removeProfile(id: Int) { - profileDao.removeProfile(id) - - master.preformProfileChanged() - } - - override fun addProfile(profile: ClashProfileEntity?) { - require(profile != null) - - profileDao.addProfile(profile) - - master.preformProfileChanged() - } - - override fun queryActiveProfile(): ClashProfileEntity? { - return profileDao.queryActiveProfile() - } - - override fun setActiveProfile(id: Int) { - profileDao.setActiveProfile(id) - - master.preformProfileChanged() - } - - override fun touchProfile(id: Int) { - profileDao.touchProfile(id) - - master.preformProfileChanged() - } - - override fun queryProfiles(): Array { - return profileDao.queryProfiles() - } - - fun queryProfileSelected(id: Int): Map { - return profileProxyDao.querySelectedForProfile(id).map { - it.proxy to it.selected - }.toMap() - } - - fun setCurrentProfileProxy(proxy: String, selected: String) { - val active = profileDao.queryActiveProfile() ?: return - - profileProxyDao.setSelectedForProfile(ClashProfileProxyEntity(active.id, proxy, selected)) - } - - fun removeCurrentProfileProxy(proxies: List) { - val active = profileDao.queryActiveProfile() ?: return - - profileProxyDao.removeSelectedForProfile(active.id, proxies) - } -} \ No newline at end of file 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 60b2073e04..d911e284c0 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 @@ -1,210 +1,91 @@ package com.github.kr328.clash.service import android.app.Service -import android.content.BroadcastReceiver +import android.content.ComponentName import android.content.Context import android.content.Intent -import android.content.IntentFilter +import android.content.ServiceConnection import android.os.Binder import android.os.IBinder -import android.os.IInterface import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.core.event.* -import com.github.kr328.clash.core.utils.Log -import java.util.concurrent.Executors -import kotlin.concurrent.thread -class ClashService : Service(), IClashEventObserver { - private val executor = Executors.newSingleThreadExecutor() - - private val instance: ClashServiceImpl by lazy { - ClashServiceImpl(this) +class ClashService : Service() { + companion object { + const val INTENT_EXTRA_START_TUN = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.start.tun" } - private val events: ClashEventService - get() = instance.eventService - private val clash: Clash - get() = instance.clash - - //private lateinit var puller: ClashEventPuller + private var stopReason: String? = null private lateinit var notification: ClashNotification + private val tunConnection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) {} + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + require(service != null) + + val tun = service.queryLocalInterface(TunService::class.java.name) as TunService + + tun.startTun().whenComplete { _, u -> + if (u != null) { + stopReason = u.message + stopSelf() + return@whenComplete + } - private val screenReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - when (intent?.action) { - Intent.ACTION_SCREEN_ON -> - instance.eventService.registerEventObserver( - ClashService::class.java.name, - this@ClashService, - intArrayOf(Event.EVENT_TRAFFIC) - ) - Intent.ACTION_SCREEN_OFF -> - instance.eventService.registerEventObserver( - ClashService::class.java.name, - this@ClashService, - intArrayOf() - ) + notification.setVpn(true) } } } - private fun onClashProcessChanged(event: ProcessEvent) { - instance.eventService.performProcessEvent(event) - } - - fun acquireEvent(event: Int) { - if ( instance.clash.getCurrentProcessStatus() == ProcessEvent.STOPPED ) - return - - when ( event ) { - Event.EVENT_BANDWIDTH -> - instance.eventPoll.startBandwidthPoll() - Event.EVENT_TRAFFIC -> - instance.eventPoll.startTrafficPoll() - Event.EVENT_LOG -> - instance.eventPoll.startLogsPoll() - } - } - - fun releaseEvent(event: Int) { - if ( instance.clash.getCurrentProcessStatus() == ProcessEvent.STOPPED ) - return - - when ( event ) { - Event.EVENT_BANDWIDTH -> - instance.eventPoll.stopBandwidthPoll() - Event.EVENT_TRAFFIC -> - instance.eventPoll.stopTrafficPoll() - Event.EVENT_LOG -> - instance.eventPoll.stopLogPoll() - } - } - override fun onCreate() { super.onCreate() - // Init instance - instance - notification = ClashNotification(this) - instance.eventService.registerEventObserver( - ClashService::class.java.name, - this@ClashService, - intArrayOf(Event.EVENT_TRAFFIC) - ) - - registerReceiver(screenReceiver, IntentFilter().apply { - addAction(Intent.ACTION_SCREEN_ON) - addAction(Intent.ACTION_SCREEN_OFF) - }) + Clash.initialize(this) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) - notification.setVpn(false) - notification.show() + Clash.start() + + sendBroadcast(Intent(Intents.INTENT_ACTION_CLASH_STARTED)) - instance.clash.start() + val startVpn = intent?.getBooleanExtra(INTENT_EXTRA_START_TUN, true) ?: true + + if (startVpn) + bindService( + Intent(this, TunService::class.java) + .setAction(Intents.INTENT_ACTION_BIND_TUN_SERVICE), + tunConnection, + Context.BIND_AUTO_CREATE + ) return START_NOT_STICKY } override fun onBind(intent: Intent?): IBinder? { - return instance + return Binder() } override fun onDestroy() { - instance.clash.stop() + runCatching { + unbindService(tunConnection) + } - instance.shutdown() + Clash.stop() - executor.shutdown() + notification.destroy() - unregisterReceiver(screenReceiver) + sendBroadcast( + Intent(Intents.INTENT_ACTION_CLASH_STOPPED) + .putExtra(Intents.INTENT_ACTION_CLASH_STOP_REASON, stopReason) + ) super.onDestroy() } - override fun onProfileChanged(event: ProfileChangedEvent?) { - reloadProfile() - } - - override fun onProcessEvent(event: ProcessEvent?) { - when (event!!) { - ProcessEvent.STARTED -> { - reloadProfile() - - notification.show() - - instance.eventService.recastEventRequirement() - } - ProcessEvent.STOPPED -> { - instance.eventService.performSpeedEvent(TrafficEvent(0, 0)) - instance.eventService.performBandwidthEvent(BandwidthEvent(0)) - - notification.cancel() - - stopSelf() - } - } - - sendBroadcast(Intent(Constants.CLASH_PROCESS_BROADCAST_ACTION).setPackage(packageName)) - } - private fun reloadProfile() { - executor.submit { - if ( clash.getCurrentProcessStatus() != ProcessEvent.STARTED) - return@submit - val active = instance.profileService.queryActiveProfile() - - if (active == null) { - events.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, "No active profile")) - clash.stop() - return@submit - } - - Log.i("Loading profile ${active.file}") - - try { - clash.loadProfile(active.file) - - instance.profileService.queryProfileSelected(active.id).mapNotNull { - if ( clash.setSelectedProxy(it.key, it.value) ) - null - else - it.key - }.toList().apply { - instance.profileService.removeCurrentProfileProxy(this) - } - - notification.setProfile(active.name) - - events.performProfileReloadEvent(ProfileReloadEvent()) - } catch (e: Exception) { - clash.stop() - events.performErrorEvent(ErrorEvent(ErrorEvent.Type.PROFILE_LOAD, e.message ?: e.toString())) - Log.w("Load profile failure", e) - } - } - } - - override fun onProfileReloaded(event: ProfileReloadEvent?) { - sendBroadcast(Intent(Constants.CLASH_RELOAD_BROADCAST_ACTION).setPackage(packageName)) - } - - override fun onTrafficEvent(event: TrafficEvent?) { - notification.setSpeed(event?.up ?: 0, event?.down ?: 0) - } - - override fun onBandwidthEvent(event: BandwidthEvent?) {} - override fun onLogEvent(event: LogEvent?) {} - override fun onErrorEvent(event: ErrorEvent?) {} - override fun asBinder(): IBinder = object : Binder() { - override fun queryLocalInterface(descriptor: String): IInterface? { - return this@ClashService - } } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt b/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt deleted file mode 100644 index b085a17f08..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashServiceImpl.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.github.kr328.clash.service - -import android.os.ParcelFileDescriptor -import com.github.kr328.clash.callback.IUrlTestCallback -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.core.event.ErrorEvent -import com.github.kr328.clash.core.event.ProcessEvent -import com.github.kr328.clash.core.model.CompressedProxyList -import com.github.kr328.clash.core.model.General -import com.github.kr328.clash.core.model.compress -import com.github.kr328.clash.core.utils.Log -import java.io.FileInputStream -import java.io.IOException -import java.util.concurrent.atomic.AtomicInteger - -class ClashServiceImpl(clashService: ClashService) : IClashService.Stub() { - private val bridge: ClashEventBridge = ClashEventBridge(clashService) - - val clash: Clash = Clash(clashService, bridge::onProcessChanged) - val profileService = ClashProfileService(clashService, bridge) - val settingService = ClashSettingService(clashService) - val eventPoll = ClashEventPoll(clash, bridge) - val eventService: ClashEventService - get() = bridge.eventService - - override fun setSelectProxy(proxy: String?, selected: String?) { - require(proxy != null && selected != null) - - try { - if ( clash.setSelectedProxy(proxy, selected) ) - profileService.setCurrentProfileProxy(proxy, selected) - else - eventService.performErrorEvent( - ErrorEvent(ErrorEvent.Type.SET_PROXY_SELECTED, "Unable to set $proxy -> $selected") - ) - } catch (e: IOException) { - Log.w("Set proxy failure", e) - - eventService.performErrorEvent( - ErrorEvent(ErrorEvent.Type.SET_PROXY_SELECTED, e.toString()) - ) - } - } - - override fun queryGeneral(): General { - return clash.queryGeneral() - } - - override fun queryAllProxies(): CompressedProxyList { - return try { - clash.queryProxyGroups().compress() - } - catch (e: Exception) { - Log.w("Query proxies", e) - - eventService.performErrorEvent(ErrorEvent(ErrorEvent.Type.QUERY_PROXY_FAILURE, e.message ?: "Unknown")) - - CompressedProxyList(emptyMap(), emptyList()) - } - } - - override fun startUrlTest(proxies: Array?, callback: IUrlTestCallback?) { - require(proxies != null && callback != null) - - val count = AtomicInteger(proxies.size) - - proxies.forEach { - clash.startHealthCheck(it) - } - } - - override fun checkProfileValid(pipe: ParcelFileDescriptor?): String? { - require(pipe != null) - - val data = FileInputStream(pipe.fileDescriptor).use { - String(it.readBytes()) - } - - pipe.close() - - return clash.checkProfileValid(data) - } - - override fun start() { - clash.start() - } - - override fun stop() { - clash.stop() - } - - override fun getEventService(): IClashEventService { - return eventService - } - - override fun getProfileService(): IClashProfileService { - return profileService - } - - override fun getSettingService(): IClashSettingService { - return settingService - } - - override fun getCurrentProcessStatus(): ProcessEvent { - return clash.getCurrentProcessStatus() - } - - fun shutdown() { - eventPoll.shutdown() - eventService.shutdown() - } -} \ No newline at end of file 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 deleted file mode 100644 index 22b656883c..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashSettingService.kt +++ /dev/null @@ -1,69 +0,0 @@ -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_DNS_HIJACKING_ENABLED = "key_dns_hijacking_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 setDnsHijackingEnabled(enabled: Boolean) { - preference.edit { - putBoolean(KEY_DNS_HIJACKING_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 isDnsHijackingEnabled(): Boolean { - return preference.getBoolean(KEY_DNS_HIJACKING_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/Intents.kt b/service/src/main/java/com/github/kr328/clash/service/Intents.kt new file mode 100644 index 0000000000..ceeeb8e40f --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/Intents.kt @@ -0,0 +1,9 @@ +package com.github.kr328.clash.service + +object Intents { + const val INTENT_ACTION_BIND_TUN_SERVICE = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.bind.tun" + const val INTENT_ACTION_CLASH_STARTED = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.started" + const val INTENT_ACTION_CLASH_STOPPED = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.stopped" + + const val INTENT_ACTION_CLASH_STOP_REASON = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason" +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/Settings.kt b/service/src/main/java/com/github/kr328/clash/service/Settings.kt new file mode 100644 index 0000000000..5549a7055e --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/Settings.kt @@ -0,0 +1,76 @@ +package com.github.kr328.clash.service + +class Settings(private val clashManager: IClashManager) { + companion object { + const val ACCESS_CONTROL_MODE_ALL = "access_control_mode_all" + const val ACCESS_CONTROL_MODE_BLACKLIST = "access_control_mode_blacklist" + const val ACCESS_CONTROL_MODE_WHITELIST = "access_control_mode_whitelist" + + val BYPASS_PRIVATE_NETWORK = BooleanSetting("bypass_private_network", true) + val ACCESS_CONTROL_MODE = StringSetting("access_control_mode", ACCESS_CONTROL_MODE_ALL) + val ACCESS_CONTROL_PACKAGES = PackageListSetting("access_control_packages", emptyList()) + val DNS_HIJACKING = BooleanSetting("dns_hijacking", true) + } + + fun put(key: String, value: String) { + clashManager.putSetting(key, value) + } + + fun get(key: String): String? { + return clashManager.getSetting(key) + } + + fun get(setting: Setting): T { + return setting.parseValue(get(setting.key)) + } + + fun put(setting: Setting, value: T) { + put(setting.key, setting.valueToString(value)) + } + + interface Setting { + val key: String + fun parseValue(value: String?): T + fun valueToString(value: T): String + } + + class BooleanSetting(override val key: String, private val def: Boolean): Setting { + override fun parseValue(value: String?): Boolean { + val v = value ?: return def + return v.toBoolean() + } + override fun valueToString(value: Boolean): String { + return value.toString() + } + } + + class IntSetting(override val key: String, private val def: Int): Setting { + override fun parseValue(value: String?): Int { + val v = value ?: return def + return v.toIntOrNull() ?: def + } + override fun valueToString(value: Int): String { + return value.toString() + } + } + + class StringSetting(override val key: String, val def: String): Setting { + override fun parseValue(value: String?): String { + return value ?: def + } + override fun valueToString(value: String): String { + return value + } + } + + class PackageListSetting(override val key: String, private val def: List): Setting> { + override fun parseValue(value: String?): List { + val v = value ?: return def + + return v.split(":") + } + override fun valueToString(value: List): String { + return value.joinToString(":") + } + } +} \ 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 f9c56b8107..62004bb129 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 @@ -1,17 +1,18 @@ package com.github.kr328.clash.service -import android.app.Service -import android.content.ComponentName -import android.content.Context import android.content.Intent -import android.content.ServiceConnection import android.net.VpnService -import android.os.* -import com.github.kr328.clash.core.event.* +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.os.IInterface +import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.net.DefaultNetworkObserver +import com.github.kr328.clash.service.util.DefaultThreadPool +import java.util.concurrent.CompletableFuture -class TunService : VpnService(), IClashEventObserver { +class TunService : VpnService(), IInterface { companion object { // from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/bg/VpnService.kt private const val VPN_MTU = 1500 @@ -21,54 +22,59 @@ class TunService : VpnService(), IClashEventObserver { private const val VLAN4_ANY = "0.0.0.0" } - private var start = true - private lateinit var fileDescriptor: ParcelFileDescriptor - private lateinit var clash: ClashServiceImpl private lateinit var defaultNetworkObserver: DefaultNetworkObserver - private lateinit var settings: ClashSettingService - private val connection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName?) { - stopSelf() - } - - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - val clash = IClashService.Stub.asInterface( - service - ) ?: throw NullPointerException() + private lateinit var settings: Settings + + // export to ClashService + fun startTun(): CompletableFuture { + val result = CompletableFuture() + + DefaultThreadPool.submit { + val fd = Builder() + .addAddress() + .addDnsServer(PRIVATE_VLAN_DNS) + .addBypassApplications() + .addBypassPrivateRoute() + .setMtu(VPN_MTU) + .setBlocking(false) + .setMeteredCompat(false) + .establish() + + if (fd == null) { + result.completeExceptionally(NullPointerException("Unable to establish VPN")) + return@submit + } - this@TunService.clash = (clash as ClashServiceImpl) + val dnsAddress = + if (settings.get(Settings.DNS_HIJACKING)) + "$VLAN4_ANY:53" + else + "$PRIVATE_VLAN_DNS:53" - start = true + Clash.startTunDevice(fd.fd, VPN_MTU, dnsAddress) - clash.eventService.registerEventObserver( - TunService::class.java.simpleName, - this@TunService, - intArrayOf() - ) + result.complete(Unit) } - } - override fun onCreate() { - super.onCreate() + return result + } - if (prepare(this) != null) { - stopSelf() - return + override fun onBind(intent: Intent?): IBinder? { + if (Intents.INTENT_ACTION_BIND_TUN_SERVICE == intent?.action) { + return object : Binder() { + override fun queryLocalInterface(descriptor: String): IInterface? { + return this@TunService + } + } } - settings = ClashSettingService(this) + return super.onBind(intent) + } - fileDescriptor = Builder() - .addAddress() - .addDnsServer(PRIVATE_VLAN_DNS) - .addBypassApplications() - .addBypassPrivateRoute() - .setMtu(VPN_MTU) - .setBlocking(false) - .setMeteredCompat(false) - .establish() ?: throw NullPointerException("Unable to establish VPN") + override fun onCreate() { + super.onCreate() - bindService(Intent(this, ClashService::class.java), connection, Context.BIND_AUTO_CREATE) + settings = Settings(ClashManager(this)) defaultNetworkObserver = DefaultNetworkObserver(this) { setUnderlyingNetworks(it?.run { arrayOf(it) }) @@ -77,62 +83,14 @@ class TunService : VpnService(), IClashEventObserver { defaultNetworkObserver.register() } - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - return Service.START_NOT_STICKY - } - override fun onDestroy() { super.onDestroy() - clash.stop() - - clash.eventService.unregisterEventObserver(TunService::class.java.simpleName) - - unbindService(connection) + Clash.stopTunDevice() defaultNetworkObserver.unregister() } - override fun onProcessEvent(event: ProcessEvent?) { - when (event) { - ProcessEvent.STOPPED -> { - val startNow = start - start = false - - if (startNow) - clash.start() - else - stopSelf() - - clash.clash.stopTunDevice() - - Log.i("STOPPED") - } - ProcessEvent.STARTED -> { - start = false - - if ( settings.isDnsHijackingEnabled ) { - clash.clash.startTunDevice( - fileDescriptor.fd, VPN_MTU, - VLAN4_ANY - ) - } - else { - clash.clash.startTunDevice( - fileDescriptor.fd, VPN_MTU, - PRIVATE_VLAN_DNS - ) - } - - fileDescriptor.close() - - Log.i("STARTED") - } - } - } - private fun Builder.setMeteredCompat(isMetered: Boolean): Builder { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) setMetered(isMetered) @@ -141,15 +99,12 @@ class TunService : VpnService(), IClashEventObserver { private fun Builder.addBypassPrivateRoute(): Builder { // IPv4 - if ( settings.isBypassPrivateNetwork ) { - Log.i("Bypass Private Network") - + if (settings.get(Settings.BYPASS_PRIVATE_NETWORK)) { resources.getStringArray(R.array.bypass_private_route).forEach { val address = it.split("/") addRoute(address[0], address[1].toInt()) } - } - else { + } else { addRoute("0.0.0.0", 0) } @@ -157,20 +112,20 @@ class TunService : VpnService(), IClashEventObserver { } private fun Builder.addBypassApplications(): Builder { - when ( settings.accessControlMode ) { - ClashSettingService.ACCESS_CONTROL_MODE_ALLOW_ALL -> { - for ( app in resources.getStringArray(R.array.default_disallow_application) ) { + when (settings.get(Settings.ACCESS_CONTROL_MODE)) { + Settings.ACCESS_CONTROL_MODE_ALL -> { + for (app in resources.getStringArray(R.array.default_disallow_application)) { runCatching { addDisallowedApplication(app) } } addDisallowedApplication(packageName) } - ClashSettingService.ACCESS_CONTROL_MODE_ALLOW -> { + Settings.ACCESS_CONTROL_MODE_WHITELIST -> { addAllowedApplication(packageName) - for ( app in settings.accessControlApps.toSet() - + for (app in settings.get(Settings.ACCESS_CONTROL_PACKAGES).toSet() - resources.getStringArray(R.array.default_disallow_application) - - setOf(packageName) ) { + setOf(packageName)) { runCatching { addAllowedApplication(app) }.onFailure { @@ -178,9 +133,9 @@ class TunService : VpnService(), IClashEventObserver { } } } - ClashSettingService.ACCESS_CONTROL_MODE_DISALLOW -> { - for ( app in settings.accessControlApps.toSet() + - resources.getStringArray(R.array.default_disallow_application) ) { + Settings.ACCESS_CONTROL_MODE_BLACKLIST -> { + for (app in settings.get(Settings.ACCESS_CONTROL_PACKAGES).toSet() + + resources.getStringArray(R.array.default_disallow_application)) { runCatching { addDisallowedApplication(app) }.onFailure { @@ -200,15 +155,11 @@ class TunService : VpnService(), IClashEventObserver { return this } - override fun onTrafficEvent(event: TrafficEvent?) {} - override fun onBandwidthEvent(event: BandwidthEvent?) {} - override fun onLogEvent(event: LogEvent?) {} - override fun onErrorEvent(event: ErrorEvent?) {} - override fun onProfileChanged(event: ProfileChangedEvent?) {} - override fun onProfileReloaded(event: ProfileReloadEvent?) {} - override fun asBinder(): IBinder = object : Binder() { - override fun queryLocalInterface(descriptor: String): IInterface? { - return this@TunService + override fun asBinder(): IBinder { + return object : Binder() { + override fun queryLocalInterface(descriptor: String): IInterface? { + return this@TunService + } } } } \ No newline at end of file From a29c95ceef93978d9a3c147acf2f4b0ce5c38f07 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 21 Jan 2020 20:17:31 +0800 Subject: [PATCH 071/358] service refactored --- .../com/github/kr328/clash/MainApplication.kt | 38 +-------------- .../kr328/clash/service/ClashManager.kt | 9 ++-- .../clash/service/ClashProfileManager.kt | 6 ++- .../kr328/clash/service/ClashService.kt | 46 ++++++++++++++----- .../github/kr328/clash/service/Constants.kt | 6 ++- .../com/github/kr328/clash/service/Intents.kt | 14 ++++-- .../github/kr328/clash/service/Settings.kt | 17 ++++--- .../kr328/clash/service/data/ClashDatabase.kt | 6 ++- .../service/data/ClashDatabaseMigrations.kt | 29 ++++++------ .../clash/service/data/ClashProfileEntity.kt | 2 +- .../service/data/ClashProfileProxyEntity.kt | 10 ++-- .../service/ipc/ParcelableCompletedFuture.kt | 11 ++--- .../clash/service/ipc/ParcelableResult.kt | 2 +- .../service/net/DefaultNetworkObserver.kt | 3 +- .../clash/service/util/BroadcastUtils.kt | 8 ++++ 15 files changed, 115 insertions(+), 92 deletions(-) create mode 100644 service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 8305328d05..7ff19a7088 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -39,12 +39,12 @@ class MainApplication : Application() { private fun ByteArray.toHexString(): String { return this.map { Integer.toHexString(it.toInt() and 0xff) - }.map { + }.joinToString(separator = "") { if (it.length < 2) "0$it" else it - }.joinToString(separator = "") + } } } @@ -68,40 +68,6 @@ class MainApplication : Application() { Crashlytics.setBool(CRASHLYTICS_SPLIT_APK_KEY, detectSplitArchive()) Crashlytics.setString(CRASHLYTICS_BASE_SIZE_KEY, getBaseApkSize()) Crashlytics.setUserIdentifier(userIdentifier) - - Log.handler = object : Log.LogHandler { - override fun info(message: String, throwable: Throwable?) { - android.util.Log.i(Constants.TAG, message, throwable) - } - - override fun warn(message: String, throwable: Throwable?) { - throwable?.also { - Crashlytics.logException(it) - } - - android.util.Log.w(Constants.TAG, message, throwable) - } - - override fun error(message: String, throwable: Throwable?) { - throwable?.also { - Crashlytics.logException(it) - } - - android.util.Log.e(Constants.TAG, message, throwable) - } - - override fun wtf(message: String, throwable: Throwable?) { - throwable?.also { - Crashlytics.logException(it) - } - - android.util.Log.wtf(Constants.TAG, message, throwable) - } - - override fun debug(message: String, throwable: Throwable?) { - android.util.Log.d(Constants.TAG, message, throwable) - } - } } private fun detectFromPlay(): Boolean { diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index 9fbd7cc94e..10d4b4bbc4 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -1,7 +1,6 @@ package com.github.kr328.clash.service import android.content.Context -import android.content.SharedPreferences import androidx.core.content.edit import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.model.General @@ -9,7 +8,7 @@ import com.github.kr328.clash.core.model.ProxyGroup import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture import com.github.kr328.clash.service.ipc.ParcelablePipe -class ClashManager(context: Context): IClashManager.Stub() { +class ClashManager(context: Context) : IClashManager.Stub() { private val settings = context.getSharedPreferences("service", Context.MODE_PRIVATE) override fun queryAllProxies(): Array { @@ -27,7 +26,7 @@ class ClashManager(context: Context): IClashManager.Stub() { } override fun openBandwidthEvent(): ParcelablePipe { - return object: ParcelablePipe() { + return object : ParcelablePipe() { val stream = Clash.openBandwidthEvent().apply { onEvent { send(it) @@ -45,7 +44,7 @@ class ClashManager(context: Context): IClashManager.Stub() { return ParcelableCompletedFuture().apply { Clash.startHealthCheck(group).whenComplete { _: Unit?, u: Throwable? -> - if ( u != null ) + if (u != null) this.completeExceptionally(u) else this.complete(null) @@ -54,7 +53,7 @@ class ClashManager(context: Context): IClashManager.Stub() { } override fun openLogEvent(): ParcelablePipe { - return object: ParcelablePipe() { + return object : ParcelablePipe() { val stream = Clash.openLogEvent().apply { onEvent { send(it) diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt index 495b92dcd6..be9d888876 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt @@ -17,8 +17,10 @@ class ClashProfileManager(private val context: Context, private val database: Cl override fun updateProfile(id: Int): ParcelableCompletedFuture { val entity = database.openClashProfileDao().queryProfileById(id) - require(entity != null && (entity.type == ClashProfileEntity.Type.URL || - entity.type == ClashProfileEntity.Type.FILE)) + require( + entity != null && (entity.type == ClashProfileEntity.Type.URL || + entity.type == ClashProfileEntity.Type.FILE) + ) val result = ParcelableCompletedFuture() 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 d911e284c0..6ce6d83fb1 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 @@ -1,13 +1,13 @@ package com.github.kr328.clash.service import android.app.Service -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection +import android.content.* import android.os.Binder import android.os.IBinder import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.util.DefaultThreadPool +import com.github.kr328.clash.service.util.sendBroadcastSelf class ClashService : Service() { companion object { @@ -25,16 +25,18 @@ class ClashService : Service() { val tun = service.queryLocalInterface(TunService::class.java.name) as TunService tun.startTun().whenComplete { _, u -> - if (u != null) { - stopReason = u.message - stopSelf() - return@whenComplete - } + if (u != null) + return@whenComplete stopSelf(u.message ?: "Start tun failure") notification.setVpn(true) } } } + private val profileObserver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + reloadProfile() + } + } override fun onCreate() { super.onCreate() @@ -49,7 +51,7 @@ class ClashService : Service() { Clash.start() - sendBroadcast(Intent(Intents.INTENT_ACTION_CLASH_STARTED)) + sendBroadcastSelf(Intent(Intents.INTENT_ACTION_CLASH_STARTED)) val startVpn = intent?.getBooleanExtra(INTENT_EXTRA_START_TUN, true) ?: true @@ -61,6 +63,10 @@ class ClashService : Service() { Context.BIND_AUTO_CREATE ) + reloadProfile() + + registerReceiver(profileObserver, IntentFilter(Intents.INTENT_ACTION_PROFILE_CHANGED)) + return START_NOT_STICKY } @@ -77,15 +83,33 @@ class ClashService : Service() { notification.destroy() - sendBroadcast( + sendBroadcastSelf( Intent(Intents.INTENT_ACTION_CLASH_STOPPED) .putExtra(Intents.INTENT_ACTION_CLASH_STOP_REASON, stopReason) ) + unregisterReceiver(profileObserver) + super.onDestroy() } private fun reloadProfile() { + DefaultThreadPool.submit { + val active = ClashDatabase.getInstance(this).openClashProfileDao().queryActiveProfile() + ?: return@submit stopSelf("Empty active profile") + + Clash.loadProfile( + filesDir.resolve(Constants.PROFILES_DIR).resolve(active.file), + filesDir.resolve(Constants.CLASH_DIR).resolve(active.base) + ).whenComplete { _, u -> + if (u != null) + return@whenComplete stopSelf(u.message ?: "Load profile failure") + } + } + } + private fun stopSelf(reason: String) { + stopReason = reason + stopSelf() } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/Constants.kt b/service/src/main/java/com/github/kr328/clash/service/Constants.kt index 8e6b4ae2c1..b47169d775 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Constants.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Constants.kt @@ -1,8 +1,10 @@ package com.github.kr328.clash.service object Constants { - const val CLASH_PROCESS_BROADCAST_ACTION = "com.github.kr328.clash.ClashService.ClashProcessEvent" - const val CLASH_RELOAD_BROADCAST_ACTION = "com.github.kr328.clash.ClashService.ProfileReloadEvent" + const val CLASH_PROCESS_BROADCAST_ACTION = + "com.github.kr328.clash.ClashService.ClashProcessEvent" + const val CLASH_RELOAD_BROADCAST_ACTION = + "com.github.kr328.clash.ClashService.ProfileReloadEvent" const val CLASH_DIR = "clash" const val PROFILES_DIR = "profiles" diff --git a/service/src/main/java/com/github/kr328/clash/service/Intents.kt b/service/src/main/java/com/github/kr328/clash/service/Intents.kt index ceeeb8e40f..8d3d0375fd 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Intents.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Intents.kt @@ -1,9 +1,15 @@ package com.github.kr328.clash.service object Intents { - const val INTENT_ACTION_BIND_TUN_SERVICE = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.bind.tun" - const val INTENT_ACTION_CLASH_STARTED = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.started" - const val INTENT_ACTION_CLASH_STOPPED = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.stopped" + const val INTENT_ACTION_BIND_TUN_SERVICE = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.bind.tun" + const val INTENT_ACTION_CLASH_STARTED = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.started" + const val INTENT_ACTION_CLASH_STOPPED = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.stopped" + const val INTENT_ACTION_PROFILE_CHANGED = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.changed" - const val INTENT_ACTION_CLASH_STOP_REASON = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason" + const val INTENT_ACTION_CLASH_STOP_REASON = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason" } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/Settings.kt b/service/src/main/java/com/github/kr328/clash/service/Settings.kt index 5549a7055e..3ff4d24151 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Settings.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Settings.kt @@ -20,11 +20,11 @@ class Settings(private val clashManager: IClashManager) { return clashManager.getSetting(key) } - fun get(setting: Setting): T { + fun get(setting: Setting): T { return setting.parseValue(get(setting.key)) } - fun put(setting: Setting, value: T) { + fun put(setting: Setting, value: T) { put(setting.key, setting.valueToString(value)) } @@ -34,41 +34,46 @@ class Settings(private val clashManager: IClashManager) { fun valueToString(value: T): String } - class BooleanSetting(override val key: String, private val def: Boolean): Setting { + class BooleanSetting(override val key: String, private val def: Boolean) : Setting { override fun parseValue(value: String?): Boolean { val v = value ?: return def return v.toBoolean() } + override fun valueToString(value: Boolean): String { return value.toString() } } - class IntSetting(override val key: String, private val def: Int): Setting { + class IntSetting(override val key: String, private val def: Int) : Setting { override fun parseValue(value: String?): Int { val v = value ?: return def return v.toIntOrNull() ?: def } + override fun valueToString(value: Int): String { return value.toString() } } - class StringSetting(override val key: String, val def: String): Setting { + class StringSetting(override val key: String, val def: String) : Setting { override fun parseValue(value: String?): String { return value ?: def } + override fun valueToString(value: String): String { return value } } - class PackageListSetting(override val key: String, private val def: List): Setting> { + class PackageListSetting(override val key: String, private val def: List) : + Setting> { override fun parseValue(value: String?): List { val v = value ?: return def return v.split(":") } + override fun valueToString(value: List): String { return value.joinToString(":") } diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt index 9eef229d39..3972461b89 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt @@ -5,7 +5,11 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase -@Database(version = 2, exportSchema = false, entities = [ClashProfileEntity::class, ClashProfileProxyEntity::class]) +@Database( + version = 2, + exportSchema = false, + entities = [ClashProfileEntity::class, ClashProfileProxyEntity::class] +) abstract class ClashDatabase : RoomDatabase() { abstract fun openClashProfileDao(): ClashProfileDao abstract fun openClashProfileProxyDao(): ClashProfileProxyDao diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt index 5a8ba2b712..f06b909bfa 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt @@ -14,22 +14,25 @@ object ClashDatabaseMigrations { database.execSQL("ALTER TABLE profiles RENAME TO _profiles") - database.execSQL("CREATE TABLE profiles(" + - "name TEXT NOT NULL, " + - "type INTEGER NOT NULL, " + - "uri TEXT NOT NULL, " + - "file TEXT NOT NULL, " + - "base TEXT NOT NULL" + - "active INTEGER NOT NULL, " + - "last_update INTEGER NOT NULL, " + - "id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)") + database.execSQL( + "CREATE TABLE profiles(" + + "name TEXT NOT NULL, " + + "type INTEGER NOT NULL, " + + "uri TEXT NOT NULL, " + + "file TEXT NOT NULL, " + + "base TEXT NOT NULL" + + "active INTEGER NOT NULL, " + + "last_update INTEGER NOT NULL, " + + "id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL)" + ) - val cursor = database.query("SELECT name, token, file, active, last_update FROM _profile") + val cursor = + database.query("SELECT name, token, file, active, last_update FROM _profile") val random = SecureRandom() val bases = mutableSetOf() cursor.moveToFirst() - while ( !cursor.isAfterLast ) { + while (!cursor.isAfterLast) { // old val name = cursor.getString(0) val token = cursor.getString(1) @@ -46,7 +49,7 @@ object ClashDatabaseMigrations { val uri = token.removePrefix("url|").removePrefix("file|") var base = random.nextLong().absoluteValue - while ( bases.contains(base) ) + while (bases.contains(base)) base = random.nextLong().absoluteValue database.insert("profiles", @@ -59,7 +62,7 @@ object ClashDatabaseMigrations { put("active", active) put("last_update", lastUpdate) put("base", base.toString()) - }) + }) } cursor.close() diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index 353996133b..3997feb005 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -41,7 +41,7 @@ data class ClashProfileEntity( @TypeConverter fun intToType(value: Int?): Type { - return when ( value ) { + return when (value) { Type.URL.id -> Type.URL Type.FILE.id -> Type.FILE Type.UNKNOWN.id -> Type.UNKNOWN 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 2b41ff0aa8..ca3002d6f9 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 @@ -2,13 +2,17 @@ package com.github.kr328.clash.service.data import androidx.room.* -@Entity(tableName = "profile_select_proxies", +@Entity( + tableName = "profile_select_proxies", indices = [Index("profile_id")], - foreignKeys = [ForeignKey(entity = ClashProfileEntity::class, + foreignKeys = [ForeignKey( + entity = ClashProfileEntity::class, childColumns = ["profile_id"], parentColumns = ["id"], onDelete = ForeignKey.CASCADE, - onUpdate = ForeignKey.CASCADE)]) + onUpdate = ForeignKey.CASCADE + )] +) data class ClashProfileProxyEntity( @ColumnInfo(name = "profile_id") val profileId: Int, @ColumnInfo(name = "proxy") val proxy: String, diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt index 56488c5742..7055326da5 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt @@ -7,7 +7,7 @@ import java.util.concurrent.CompletableFuture class ParcelableCompletedFuture private constructor(private val pipe: ParcelablePipe) : CompletableFuture(), Parcelable { - constructor(): this(ParcelablePipe()) + constructor() : this(ParcelablePipe()) constructor(parcel: Parcel) : this( parcel.readParcelable( ParcelableCompletedFuture::class.java.classLoader @@ -16,17 +16,16 @@ class ParcelableCompletedFuture private constructor(private val pipe: Parcelable pipe.onReceive { val result = (it ?: throw NullPointerException()) as ParcelableResult - if ( result.exception != null ) { + if (result.exception != null) { completeExceptionally(RemoteException(result.exception)) - } - else { + } else { complete(result.data) } } } override fun complete(value: Parcelable?): Boolean { - if ( super.complete(value) ) { + if (super.complete(value)) { pipe.send(ParcelableResult(value, null)) return true } @@ -34,7 +33,7 @@ class ParcelableCompletedFuture private constructor(private val pipe: Parcelable } override fun completeExceptionally(ex: Throwable?): Boolean { - if ( super.completeExceptionally(ex) ) { + if (super.completeExceptionally(ex)) { pipe.send(ParcelableResult(null, ex?.message ?: "Unknown")) return true } diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt index cd3b28f771..f73cc1f877 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt @@ -3,7 +3,7 @@ package com.github.kr328.clash.service.ipc import android.os.Parcel import android.os.Parcelable -data class ParcelableResult(val data: Parcelable?, val exception: String?): Parcelable { +data class ParcelableResult(val data: Parcelable?, val exception: String?) : Parcelable { constructor(parcel: Parcel) : this( parcel.readParcelable(ParcelableResult::class.java.classLoader), parcel.readString() diff --git a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkObserver.kt b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkObserver.kt index 3a7b544eb2..c3e5b4ab40 100644 --- a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkObserver.kt +++ b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkObserver.kt @@ -50,7 +50,8 @@ class DefaultNetworkObserver(val context: Context, val listener: (Network?) -> U return try { connectivity.allNetworks .flatMap { network -> - connectivity.getNetworkCapabilities(network)?.let { listOf(it to network) } ?: emptyList() + connectivity.getNetworkCapabilities(network)?.let { listOf(it to network) } + ?: emptyList() } .asSequence() .filterNot { diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt new file mode 100644 index 0000000000..a15f08f5d4 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt @@ -0,0 +1,8 @@ +package com.github.kr328.clash.service.util + +import android.content.Context +import android.content.Intent + +fun Context.sendBroadcastSelf(intent: Intent) { + this.sendBroadcast(intent.setPackage(this.packageName)) +} \ No newline at end of file From e9e59f6026868a74a305222c52a29cd00217644f Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 21 Jan 2020 20:23:02 +0800 Subject: [PATCH 072/358] service refactored --- service/src/main/AndroidManifest.xml | 10 ++++++++-- .../com/github/kr328/clash/service/IClashManager.aidl | 4 ++++ .../com/github/kr328/clash/service/ClashManager.kt | 7 ++++++- .../github/kr328/clash/service/ClashManagerService.kt | 11 +++++++++++ service/src/main/res/values/strings.xml | 4 ++++ 5 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index d4f2da91d8..5ca8d4afa8 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -7,16 +7,22 @@ + android:process=":background" /> + diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index 4f9f9fe5e3..29fab27802 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -1,9 +1,13 @@ package com.github.kr328.clash.service; +import com.github.kr328.clash.service.IClashProfileManager; import com.github.kr328.clash.service.ipc.IPCParcelables; import com.github.kr328.clash.core.model.Packet; interface IClashManager { + // Profile + IClashProfileManager getProfileManager(); + // Control boolean setSelectProxy(String proxy, String selected); ParcelableCompletedFuture startHealthCheck(String group); diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index 10d4b4bbc4..d861a14a8f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -5,12 +5,17 @@ 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.ProxyGroup +import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture import com.github.kr328.clash.service.ipc.ParcelablePipe -class ClashManager(context: Context) : IClashManager.Stub() { +class ClashManager(private val context: Context) : IClashManager.Stub() { private val settings = context.getSharedPreferences("service", Context.MODE_PRIVATE) + override fun getProfileManager(): IClashProfileManager { + return ClashProfileManager(context, ClashDatabase.getInstance(context)) + } + override fun queryAllProxies(): Array { return Clash.queryProxyGroups().toTypedArray() } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt new file mode 100644 index 0000000000..093928c086 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt @@ -0,0 +1,11 @@ +package com.github.kr328.clash.service + +import android.app.Service +import android.content.Intent +import android.os.IBinder + +class ClashManagerService : Service() { + override fun onBind(intent: Intent?): IBinder? { + return ClashManager(this) + } +} \ No newline at end of file diff --git a/service/src/main/res/values/strings.xml b/service/src/main/res/values/strings.xml index 165e200fe4..deac4f007b 100644 --- a/service/src/main/res/values/strings.xml +++ b/service/src/main/res/values/strings.xml @@ -12,4 +12,8 @@ "%1$s↑\t%2$s↓" + + Clash Core Service + Tun Device Service + Clash Manager Service From cca7ec81e19334a6cc2b83edf3f33be777501023 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 21 Jan 2020 20:27:05 +0800 Subject: [PATCH 073/358] service refactored --- .../kr328/clash/service/ClashProfileManager.kt | 15 +++++++++++++++ .../com/github/kr328/clash/service/Intents.kt | 2 ++ 2 files changed, 17 insertions(+) diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt index be9d888876..8e56bf1745 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt @@ -1,6 +1,7 @@ package com.github.kr328.clash.service import android.content.Context +import android.content.Intent import android.net.Uri import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.data.ClashDatabase @@ -8,6 +9,7 @@ import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture import com.github.kr328.clash.service.util.DefaultThreadPool import com.github.kr328.clash.service.util.FileUtils +import com.github.kr328.clash.service.util.sendBroadcastSelf class ClashProfileManager(private val context: Context, private val database: ClashDatabase) : IClashProfileManager.Stub() { @@ -29,6 +31,8 @@ class ClashProfileManager(private val context: Context, private val database: Cl result.complete(null) database.openClashProfileDao().touchProfile(id) + + sendChangedBroadcast() }, onFailure = { result.completeExceptionally(it) @@ -62,6 +66,8 @@ class ClashProfileManager(private val context: Context, private val database: Cl ) ) + sendChangedBroadcast() + result.complete(null) }, onFailure = { @@ -113,4 +119,13 @@ class ClashProfileManager(private val context: Context, private val database: Cl } } } + + private fun sendChangedBroadcast() { + val active = database.openClashProfileDao().queryActiveProfile() + + context.sendBroadcastSelf( + Intent(Intents.INTENT_ACTION_PROFILE_CHANGED) + .putExtra(Intents.INTENT_ACTION_PROFILE_ACTIVE_NAME, active?.name) + ) + } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/Intents.kt b/service/src/main/java/com/github/kr328/clash/service/Intents.kt index 8d3d0375fd..d89fe18124 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Intents.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Intents.kt @@ -12,4 +12,6 @@ object Intents { const val INTENT_ACTION_CLASH_STOP_REASON = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason" + const val INTENT_ACTION_PROFILE_ACTIVE_NAME = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.active.name" } \ No newline at end of file From 114456a1f21883a752374436ff6d1ff41839ee59 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Wed, 22 Jan 2020 13:23:27 +0800 Subject: [PATCH 074/358] [WIP] refactor UI --- app/build.gradle | 3 +- .../com/github/kr328/clash/BaseActivity.kt | 156 +++++------------- .../com/github/kr328/clash/MainApplication.kt | 4 +- .../github/kr328/clash/remote/Broadcasts.kt | 65 ++++++++ .../com/github/kr328/clash/remote/Calls.kt | 7 + .../com/github/kr328/clash/remote/Channels.kt | 33 ++++ .../github/kr328/clash/remote/ClashClient.kt | 95 +++++++++++ .../clash/service/ClashProfileManager.kt | 2 +- .../kr328/clash/service/ClashService.kt | 2 +- .../com/github/kr328/clash/service/Intents.kt | 4 +- 10 files changed, 253 insertions(+), 118 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt create mode 100644 app/src/main/java/com/github/kr328/clash/remote/Calls.kt create mode 100644 app/src/main/java/com/github/kr328/clash/remote/Channels.kt create mode 100644 app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt diff --git a/app/build.gradle b/app/build.gradle index 4b7af1d5d7..7b3c3e9981 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,7 +40,8 @@ dependencies { implementation project(":core") implementation project(":service") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation "androidx.lifecycle:lifecycle-livedata:2.1.0" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' + implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0" implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.preference:preference:1.1.0' diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 928eacb287..bcfc251824 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -1,103 +1,65 @@ package com.github.kr328.clash -import android.content.ComponentName +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent -import android.content.ServiceConnection -import android.os.Bundle -import android.os.IBinder import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar -import com.github.kr328.clash.core.event.* -import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.service.ClashService -import com.github.kr328.clash.service.IClashEventObserver -import com.github.kr328.clash.service.IClashService -import java.util.concurrent.LinkedBlockingQueue -import kotlin.concurrent.thread - -abstract class BaseActivity : AppCompatActivity(), IClashEventObserver { - companion object { - private var activityCount: Int = 0 - - private val paddingRequest = LinkedBlockingQueue<(IClashService) -> Unit>() - private var clashConnection: ClashConnection? = null - - class ClashConnection : ServiceConnection { - private var requestHandler: Thread? = null - private var clash: IClashService? = null - - override fun onServiceDisconnected(name: ComponentName?) { - synchronized(BaseActivity::class.java) { - Log.i("ClashService disconnected") +import com.github.kr328.clash.service.data.ClashProfileEntity +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch + +abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() { + class EmptyBroadcastReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) {} + } - requestHandler?.interrupt() - requestHandler = null - } + private val receiver = object : Broadcasts.Receiver { + override fun onStarted() { + launch { + onClashStarted() } + } - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - synchronized(BaseActivity::class) { - Log.i("ClashService connected") - - clash = IClashService.Stub.asInterface(service) - requestHandler = thread { - try { - while (!Thread.currentThread().isInterrupted) { - val block = paddingRequest.take() - - clash?.run(block) - } - } catch (e: InterruptedException) { - } - - Log.i("ClashConnect exited") - - synchronized(BaseActivity::class.java) { - clash = null - requestHandler = null - } - } - } + override fun onStopped(cause: String?) { + launch { + onClashStopped(cause) } } - } - protected fun runClash(block: (IClashService) -> Unit) { - paddingRequest.offer(block) + override fun onProfileChanged(active: ClashProfileEntity?) { + launch { + onClashProfileChanged(active) + } + } } - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) + val isClashRunning: Boolean + get() { + return EmptyBroadcastReceiver().peekService( + this, + Intent(this, ClashService::class.java) + ) != null + } - synchronized(BaseActivity::class.java) { - if (clashConnection == null) { - clashConnection = ClashConnection() + open suspend fun onClashStarted() {} + open suspend fun onClashStopped(reason: String?) {} + open suspend fun onClashProfileChanged(active: ClashProfileEntity?) {} - applicationContext.bindService( - Intent(this, ClashService::class.java), - clashConnection!!, - Context.BIND_AUTO_CREATE - ) - } + override fun onStart() { + super.onStart() - activityCount++ - } + Broadcasts.register(receiver) } - override fun onDestroy() { - synchronized(BaseActivity::class.java) { - if (--activityCount <= 0) { - if (clashConnection != null) { - applicationContext.unbindService(clashConnection!!) - clashConnection?.onServiceDisconnected(null) - - clashConnection = null - } - } - } + override fun onStop() { + super.onStop() - super.onDestroy() + Broadcasts.unregister(receiver) } override fun setSupportActionBar(toolbar: Toolbar?) { @@ -116,39 +78,9 @@ abstract class BaseActivity : AppCompatActivity(), IClashEventObserver { return true } - private val observerBinder = object : IClashEventObserver.Stub() { - override fun onLogEvent(event: LogEvent?) = - this@BaseActivity.onLogEvent(event) - - override fun onProcessEvent(event: ProcessEvent?) = - this@BaseActivity.onProcessEvent(event) - - override fun onErrorEvent(event: ErrorEvent?) = - this@BaseActivity.onErrorEvent(event) - - override fun onTrafficEvent(event: TrafficEvent?) = - this@BaseActivity.onTrafficEvent(event) - - override fun onBandwidthEvent(event: BandwidthEvent?) { - this@BaseActivity.onBandwidthEvent(event) - } - - override fun onProfileChanged(event: ProfileChangedEvent?) = - this@BaseActivity.onProfileChanged(event) - - override fun onProfileReloaded(event: ProfileReloadEvent?) { - this@BaseActivity.onProfileReloaded(event) - } - } + override fun onDestroy() { + cancel() - override fun onLogEvent(event: LogEvent?) {} - override fun onErrorEvent(event: ErrorEvent?) {} - override fun onProfileChanged(event: ProfileChangedEvent?) {} - override fun onProcessEvent(event: ProcessEvent?) {} - override fun onTrafficEvent(event: TrafficEvent?) {} - override fun onBandwidthEvent(event: BandwidthEvent?) {} - override fun onProfileReloaded(event: ProfileReloadEvent?) {} - override fun asBinder(): IBinder { - return observerBinder + super.onDestroy() } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 7ff19a7088..a8d49efd74 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -7,6 +7,7 @@ import com.crashlytics.android.Crashlytics import com.github.kr328.clash.core.Constants import com.github.kr328.clash.core.utils.ByteFormatter import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.remote.ClashClient import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric import java.io.File @@ -15,7 +16,6 @@ import java.security.MessageDigest import java.util.zip.ZipFile import kotlin.concurrent.thread - class MainApplication : Application() { companion object { const val KEY_PROXY_MODE = "key_proxy_mode" @@ -68,6 +68,8 @@ class MainApplication : Application() { Crashlytics.setBool(CRASHLYTICS_SPLIT_APK_KEY, detectSplitArchive()) Crashlytics.setString(CRASHLYTICS_BASE_SIZE_KEY, getBaseApkSize()) Crashlytics.setUserIdentifier(userIdentifier) + + ClashClient.init(this) } private fun detectFromPlay(): Boolean { diff --git a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt new file mode 100644 index 0000000000..c99997e651 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt @@ -0,0 +1,65 @@ +package com.github.kr328.clash.remote + +import android.app.Application +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import com.github.kr328.clash.service.Intents +import com.github.kr328.clash.service.data.ClashProfileEntity + +object Broadcasts { + interface Receiver { + fun onStarted() + fun onStopped(cause: String?) + fun onProfileChanged(active: ClashProfileEntity?) + } + + private val receivers = mutableListOf() + + private val broadcastReceiver = object: BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + when ( intent?.action ) { + Intents.INTENT_ACTION_CLASH_STARTED -> + receivers.forEach { + it.onStarted() + } + Intents.INTENT_ACTION_CLASH_STOPPED -> + receivers.forEach { + it.onStopped(intent.getStringExtra(Intents.INTENT_EXTRA_CLASH_STOP_REASON)) + } + Intents.INTENT_ACTION_PROFILE_CHANGED -> + receivers.forEach { + it.onProfileChanged(intent.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE)) + } + } + } + } + + fun register(receiver: Receiver) { + receivers.add(receiver) + } + + fun unregister(receiver: Receiver) { + receivers.remove(receiver) + } + + fun init(application: Application) { + ProcessLifecycleOwner.get().lifecycle.addObserver(object: DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + application.registerReceiver(broadcastReceiver, IntentFilter().apply { + addAction(Intents.INTENT_ACTION_PROFILE_CHANGED) + addAction(Intents.INTENT_ACTION_CLASH_STOPPED) + addAction(Intents.INTENT_ACTION_CLASH_STARTED) + }) + } + + override fun onStop(owner: LifecycleOwner) { + application.unregisterReceiver(broadcastReceiver) + } + }) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt new file mode 100644 index 0000000000..d55d776b00 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt @@ -0,0 +1,7 @@ +package com.github.kr328.clash.remote + +suspend fun withClash(block: suspend (ClashClient) -> Unit) { + val client = ClashClient.instance ?: return + + block(client) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Channels.kt b/app/src/main/java/com/github/kr328/clash/remote/Channels.kt new file mode 100644 index 0000000000..8b38289089 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/Channels.kt @@ -0,0 +1,33 @@ +package com.github.kr328.clash.remote + +import com.github.kr328.clash.core.event.BandwidthEvent +import com.github.kr328.clash.core.event.LogEvent +import com.github.kr328.clash.service.ipc.ParcelablePipe +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.channels.Channel + +class LogChannel(private val pipe: ParcelablePipe): Channel by Channel(Channel.CONFLATED) { + init { + pipe.onReceive { + offer(it as LogEvent) + } + } + + override fun cancel(cause: CancellationException?) { + pipe.close() + close(cause) + } +} + +class BandwidthChannel(private val pipe: ParcelablePipe): Channel by Channel(Channel.CONFLATED) { + init { + pipe.onReceive { + offer(it as BandwidthEvent) + } + } + + override fun cancel(cause: CancellationException?) { + pipe.close() + close(cause) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt new file mode 100644 index 0000000000..44496bcf91 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -0,0 +1,95 @@ +package com.github.kr328.clash.remote + +import android.app.Application +import android.content.* +import android.os.IBinder +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner +import com.github.kr328.clash.MainApplication +import com.github.kr328.clash.core.event.BandwidthEvent +import com.github.kr328.clash.core.event.LogEvent +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.model.ProxyGroup +import com.github.kr328.clash.service.ClashManagerService +import com.github.kr328.clash.service.IClashManager +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.withContext + +class ClashClient(val service: IClashManager) { + companion object { + var instance: ClashClient? = null + + private val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + instance = null + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + if (service != null) + instance = ClashClient(IClashManager.Stub.asInterface(service)) + } + } + + fun init(application: Application) { + ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + application.bindService( + Intent(application, ClashManagerService::class.java), + connection, + Context.BIND_AUTO_CREATE + ) + } + override fun onStop(owner: LifecycleOwner) { + application.unbindService(connection) + } + }) + } + } + + private val openedChannel: MutableList> = mutableListOf() + + suspend fun setSelectProxy(name: String, proxy: String): Boolean = withContext(Dispatchers.IO) { + return@withContext service.setSelectProxy(name, proxy) + } + + suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) { + CompletableDeferred().apply { + service.startHealthCheck(group).whenComplete { _, u -> + if (u != null) + completeExceptionally(u) + else + complete(Unit) + } + } + }.await() + + suspend fun queryAllProxyGroups(): Array = withContext(Dispatchers.IO) { + service.queryAllProxies() + } + + suspend fun queryGeneral(): General = withContext(Dispatchers.IO) { + service.queryGeneral() + } + + suspend fun openLogChannel(): ReceiveChannel = withContext(Dispatchers.IO) { + LogChannel(service.openLogEvent()).also { + openedChannel.add(it) + } + } + + suspend fun openBandwidthChannel(): ReceiveChannel = + withContext(Dispatchers.IO) { + BandwidthChannel(service.openBandwidthEvent()).also { + openedChannel.add(it) + } + } + + fun close() { + for (channel in openedChannel) + channel.cancel() + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt index 8e56bf1745..50d8454741 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt @@ -125,7 +125,7 @@ class ClashProfileManager(private val context: Context, private val database: Cl context.sendBroadcastSelf( Intent(Intents.INTENT_ACTION_PROFILE_CHANGED) - .putExtra(Intents.INTENT_ACTION_PROFILE_ACTIVE_NAME, active?.name) + .putExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE, active) ) } } \ No newline at end of file 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 6ce6d83fb1..682e981d63 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 @@ -85,7 +85,7 @@ class ClashService : Service() { sendBroadcastSelf( Intent(Intents.INTENT_ACTION_CLASH_STOPPED) - .putExtra(Intents.INTENT_ACTION_CLASH_STOP_REASON, stopReason) + .putExtra(Intents.INTENT_EXTRA_CLASH_STOP_REASON, stopReason) ) unregisterReceiver(profileObserver) diff --git a/service/src/main/java/com/github/kr328/clash/service/Intents.kt b/service/src/main/java/com/github/kr328/clash/service/Intents.kt index d89fe18124..551fe8f587 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Intents.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Intents.kt @@ -10,8 +10,8 @@ object Intents { const val INTENT_ACTION_PROFILE_CHANGED = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.changed" - const val INTENT_ACTION_CLASH_STOP_REASON = + const val INTENT_EXTRA_CLASH_STOP_REASON = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason" - const val INTENT_ACTION_PROFILE_ACTIVE_NAME = + const val INTENT_EXTRA_PROFILE_ACTIVE = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.active.name" } \ No newline at end of file From ee6458a60edc4de4e663f4f29cca12f2934e17cc Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 26 Jan 2020 14:47:59 +0800 Subject: [PATCH 075/358] [WIP] refactor UI --- app/build.gradle | 9 +- app/src/main/AndroidManifest.xml | 93 +---- app/src/main/ic_launcher-web.png | Bin 31807 -> 17440 bytes .../com/github/kr328/clash/BaseActivity.kt | 55 ++- .../kr328/clash/BootCompleteReceiver.kt | 15 - .../github/kr328/clash/ClashStartService.kt | 56 --- .../github/kr328/clash/ClashStatusActivity.kt | 3 - .../java/com/github/kr328/clash/Constants.kt | 6 - .../kr328/clash/CreateProfileActivity.kt | 97 ------ .../github/kr328/clash/FeedbackActivity.kt | 49 --- .../github/kr328/clash/ImportFileActivity.kt | 160 --------- .../github/kr328/clash/ImportUrlActivity.kt | 168 --------- .../com/github/kr328/clash/LogActivity.kt | 93 ----- .../com/github/kr328/clash/MainActivity.kt | 226 +++---------- .../github/kr328/clash/ProfilesActivity.kt | 204 ----------- .../com/github/kr328/clash/ProxyActivity.kt | 183 ---------- .../kr328/clash/SettingAccessActivity.kt | 282 ---------------- .../kr328/clash/SettingApplicationActivity.kt | 104 ------ .../github/kr328/clash/SettingMainActivity.kt | 22 -- .../kr328/clash/SettingProxyActivity.kt | 105 ------ .../com/github/kr328/clash/TileService.kt | 97 ------ .../github/kr328/clash/adapter/FormAdapter.kt | 162 --------- .../github/kr328/clash/adapter/LogAdapter.kt | 45 --- .../kr328/clash/adapter/ProfileAdapter.kt | 117 ------- .../kr328/clash/adapter/ProxyAdapter.kt | 167 --------- .../github/kr328/clash/model/ClashProfile.kt | 11 - .../github/kr328/clash/model/ClashProxy.kt | 9 - .../kr328/clash/model/ClashProxyGroup.kt | 9 - .../com/github/kr328/clash/model/ClashRule.kt | 106 ------ .../com/github/kr328/clash/model/ListProxy.kt | 21 -- .../com/github/kr328/clash/remote/Calls.kt | 16 +- .../com/github/kr328/clash/remote/Channels.kt | 43 +-- .../github/kr328/clash/remote/ClashClient.kt | 31 +- .../com/github/kr328/clash/utils/FileUtils.kt | 21 -- .../github/kr328/clash/utils/ServiceUtils.kt | 46 --- .../com/github/kr328/clash/view/FatItem.kt | 103 ------ .../kr328/clash/view/MarqueeTextView.kt | 21 -- .../github/kr328/clash/view/RadioFatItem.kt | 98 ------ app/src/main/res/drawable/ic_about.xml | 2 +- app/src/main/res/drawable/ic_boot.xml | 2 +- app/src/main/res/drawable/ic_cloud.xml | 2 +- app/src/main/res/drawable/ic_feedback.xml | 2 +- .../res/drawable/ic_launcher_foreground.xml | 10 + app/src/main/res/drawable/ic_logo.png | Bin 4001 -> 0 bytes app/src/main/res/drawable/ic_logo.xml | 4 + app/src/main/res/drawable/ic_logs.xml | 2 +- app/src/main/res/drawable/ic_profiles.xml | 2 +- app/src/main/res/drawable/ic_proxies.xml | 2 +- app/src/main/res/drawable/ic_settings.xml | 2 +- .../main/res/drawable/ic_settings_color.xml | 2 +- .../{ic_clash_started.xml => ic_started.xml} | 2 +- .../{ic_clash_stopped.xml => ic_stopped.xml} | 2 +- app/src/main/res/layout/activity_main.xml | 319 ++++++++++-------- .../res/layout/activity_main_clash_status.xml | 53 --- .../res/layout/activity_main_profiles.xml | 52 --- .../res/layout/activity_main_proxy_manage.xml | 52 --- app/src/main/res/layout/adapter_form_text.xml | 4 +- app/src/main/res/layout/adapter_log.xml | 2 +- .../main/res/layout/adapter_proxy_header.xml | 2 +- app/src/main/res/layout/dialog_about.xml | 2 +- .../res/mipmap-anydpi-v26/ic_launcher.xml | 4 +- .../mipmap-anydpi-v26/ic_launcher_round.xml | 4 +- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 2222 -> 1800 bytes .../mipmap-hdpi/ic_launcher_foreground.png | Bin 3901 -> 0 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 4290 -> 3657 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1472 -> 1225 bytes .../mipmap-mdpi/ic_launcher_foreground.png | Bin 2356 -> 0 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2672 -> 2320 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 3212 -> 2429 bytes .../mipmap-xhdpi/ic_launcher_foreground.png | Bin 5834 -> 0 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 6299 -> 5197 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 5406 -> 3760 bytes .../mipmap-xxhdpi/ic_launcher_foreground.png | Bin 10330 -> 0 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 10203 -> 8074 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 7955 -> 5240 bytes .../mipmap-xxxhdpi/ic_launcher_foreground.png | Bin 16573 -> 0 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 15094 -> 11658 bytes app/src/main/res/values-night/bools.xml | 4 + app/src/main/res/values-night/colors.xml | 7 + app/src/main/res/values/attrs.xml | 2 + app/src/main/res/values/bools.xml | 4 + app/src/main/res/values/colors.xml | 10 +- app/src/main/res/values/styles.xml | 24 +- build.gradle | 4 +- core/src/main/golang/bridge/statistics.go | 38 +-- .../java/com/github/kr328/clash/core/Clash.kt | 12 +- design/.gitignore | 1 + design/build.gradle | 34 ++ design/consumer-rules.pro | 0 design/proguard-rules.pro | 21 ++ design/src/main/AndroidManifest.xml | 2 + .../clash/design/view/ColorfulTextCard.kt | 58 ++++ .../kr328/clash/design/view/TextCard.kt | 58 ++++ .../res/layout/view_colorful_text_card.xml | 33 ++ design/src/main/res/layout/view_text_card.xml | 33 ++ design/src/main/res/values/attrs.xml | 19 ++ design/src/main/res/values/strings.xml | 3 + design/src/main/res/values/style.xml | 4 + .../kr328/clash/service/IClashManager.aidl | 8 +- .../clash/service/IClashProfileManager.aidl | 6 +- .../clash/service/ipc/IPCParcelables.aidl | 5 - .../clash/service/ipc/IStreamCallback.aidl | 9 + .../service/ipc/ParcelableContainer.aidl | 3 + .../kr328/clash/service/ClashManager.kt | 63 ++-- .../clash/service/ClashProfileManager.kt | 28 +- .../kr328/clash/service/data/ClashDatabase.kt | 4 +- .../service/data/ClashDatabaseMigrations.kt | 6 +- .../clash/service/data/ClashProfileEntity.kt | 23 +- .../service/ipc/ParcelableCompletedFuture.kt | 4 +- .../clash/service/ipc/ParcelableContainer.kt | 28 ++ .../kr328/clash/service/ipc/ParcelablePipe.kt | 2 +- settings.gradle | 2 +- 112 files changed, 782 insertions(+), 3363 deletions(-) delete mode 100644 app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/ClashStartService.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/ClashStatusActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/Constants.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/LogActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/ProxyActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/SettingMainActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/TileService.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/model/ClashProfile.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/model/ClashProxy.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/model/ClashProxyGroup.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/model/ClashRule.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/model/ListProxy.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/utils/FileUtils.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/view/FatItem.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt create mode 100644 app/src/main/res/drawable/ic_launcher_foreground.xml delete mode 100644 app/src/main/res/drawable/ic_logo.png create mode 100644 app/src/main/res/drawable/ic_logo.xml rename app/src/main/res/drawable/{ic_clash_started.xml => ic_started.xml} (88%) rename app/src/main/res/drawable/{ic_clash_stopped.xml => ic_stopped.xml} (91%) delete mode 100644 app/src/main/res/layout/activity_main_clash_status.xml delete mode 100644 app/src/main/res/layout/activity_main_profiles.xml delete mode 100644 app/src/main/res/layout/activity_main_proxy_manage.xml delete mode 100644 app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png delete mode 100644 app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png create mode 100644 app/src/main/res/values-night/bools.xml create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values/bools.xml create mode 100644 design/.gitignore create mode 100644 design/build.gradle create mode 100644 design/consumer-rules.pro create mode 100644 design/proguard-rules.pro create mode 100644 design/src/main/AndroidManifest.xml create mode 100644 design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt create mode 100644 design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt create mode 100644 design/src/main/res/layout/view_colorful_text_card.xml create mode 100644 design/src/main/res/layout/view_text_card.xml create mode 100644 design/src/main/res/values/attrs.xml create mode 100644 design/src/main/res/values/strings.xml create mode 100644 design/src/main/res/values/style.xml delete mode 100644 service/src/main/aidl/com/github/kr328/clash/service/ipc/IPCParcelables.aidl create mode 100644 service/src/main/aidl/com/github/kr328/clash/service/ipc/IStreamCallback.aidl create mode 100644 service/src/main/aidl/com/github/kr328/clash/service/ipc/ParcelableContainer.aidl create mode 100644 service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableContainer.kt diff --git a/app/build.gradle b/app/build.gradle index 7b3c3e9981..c826806db1 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -39,22 +39,23 @@ dependencies { implementation project(":core") implementation project(":service") + implementation project(":design") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' - implementation "androidx.lifecycle:lifecycle-extensions:2.1.0" - implementation "androidx.lifecycle:lifecycle-common-java8:2.1.0" + implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" + implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.core:core-ktx:1.2.0-rc01' - implementation 'androidx.fragment:fragment-ktx:1.2.0-rc05' + implementation 'androidx.fragment:fragment-ktx:1.2.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation "androidx.room:room-runtime:$room_version" implementation "androidx.preference:preference-ktx:1.1.0" implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" - implementation "com.google.android.material:material:1.2.0-alpha03" + implementation "com.google.android.material:material:1.2.0-alpha04" implementation 'com.google.firebase:firebase-analytics:17.2.2' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index efa0fbdba6..e212bef28b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -19,103 +19,12 @@ + android:configChanges="uiMode"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png index f2336a2ae57b2255007e937d71139d48ea2282cc..45ffbf65e3bc8f556600738e680013683681d66b 100644 GIT binary patch literal 17440 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 MYuqWkZ5r_Z0258$&;S4c literal 31807 zcmeFYRa}(a7eD$8EmDerG>A$`NJzsV(lWFNNJ)dzT{DP?po9tn(%mH;Lx?a)H_{-T zLk~5~8Q}f>Kj&PW+jDi+%`>xSKWlwg?6r1Hs0Kull$eef002^DrDs|IfP?*r0}v5l z9|vB;rvTtbp!`f$$7^B}PUuCgTaWZV8b9;%O+jxVeNp3+#vo0dOgIo;AeZds`wjOgMnPj4IL4*v3a=aTD1zK_NlA z445WPx&?W)Gla~AEEIi6gC|R|ST=kl3}{atLxT`OmVXxzk+b(=$IqOODDY&(h=~V> zpD-trAE~P+OXe-L?=EBou@H5}^QTsvxowOViRY8U=(~d=Ok%Y3Co-zZWhtVo!C>?x z`V6(!n$Qi{e?V~v||AfbsuyyJFl)t2Su{^F?hB#Bl2kufc=@&X3dc9cAyZyr;zYy+0}$D={I* z1+c!AbqE0sJL3LEAcnBvW_z?b&Z9H?jsyG=bwP!mYz-mzWji5%$3jGGaTkma2;-ER zw!ACRz3f30i9RFRf^w5@D_&`q3I>`{?qGweH{%K;)%@u|+${UY2iEWQXkRlj2fR90%r9E4 zbips5JOJ{|h178`VX#Dk=4?|U%T)2+TY`XN8sJpqicNGIu;ZAGAIMM1l3)c`+y}UP zu;maNLEa=lAJJ{lV@j4gR@nM@X|P8G012*zAXd@XtdmCofOYBKT_Aa+2M++=_um0u zGmHUXe7GMb=&&zc`l|!jVa9&EbPKi{mM*~nz@vI#PV9e*ND^Y(F9XS&5=h1Y-jf06 z1vf4~UAF1t0mm#@lVYV^*6S;iXgJ`*FqiDEnE@<#z?=-|(pcDzSb|~OFP}PASYbr} zHLzT^V834d@?ZP4*vlXOp922xZ212J54#E*762f*G7tzbz0wfN{(2Muz}^3MCYJ*K zGeN3=oUE?F1Z5qDKi*;1AIur4U5P>X2cG-vE#8n)fuwyJa~PP{)>~`ex<7pDO5BoL z9vO5i!hE<8gF}pb5;76~O6BG#GN(WUIk+$^G?gJ)JX|3p?kg6k&^@CxZ)81W{WeEy zqeW&g)O8nz6^(_ww3s}NMDS{i#I5O+jyiE^xZZ_lznT4fS2;yqJq5C6w&5+744!$pwZC3t-i}W=2bGuPV zlgMe<>n3BhGfd4ha^|eLFmWf39&UKuW{ZaeTQ|Cu8WjPsY*510OT3 z8~9`^#ivP3S(*QmuROq!W4m?Ks*^tdGJ9ccqeq-A4}ub_utakD#C=u*JB%(M@+#2H z`uXFwFY5M1#t4~#-2T75g9a+iSifGOFxZHhVJ$F=Nab>f}*e-G(bp+amC#zW?6)-dB*+WJ~rP^cvB9~8N^=uw-cXh$gnU=_M>rSX7OCg5>C1&u2K(V6qx?QuJlz; zAUv)-bS8o!M(o0IW?~Vqg6=$H)chHekLL4PPWAv_6IwZDQe8Xo)XDr;iVX*NT){0V zi|1bO8PT;XQ-^3A`)R^(q0_O8SrID&tj&q&B3B^z4$*c z4V`KPLy;oi(0*SoUl{QiW7Y_0O7%LtfGts{5BcpJaJWHhA_#T^Zzh~h(Y$f;`Pl8R zLW=Kv6ZYLu7i@H5)`C>T%&Z>gb&E3a;ijSqA0pK4D@(GbtR;O_ktv>4XBMJO8UNVz z-p^hj6?}qmF$qxpB*HYj9IXV*(uT$ZngKv# zmECtb$4n5KcJEL@pWbiWqM7dWWUiH?aERn|YsdSot=ZH!bKl1q5$F$>b>FNpLn2rA z4xbg0N6BSA+zwA^iXm2v_$lh|kYp+kx&v?w{j(Q$KBC*^zKXD?Pv%tz0x4s2?Lus+ z*?A>!VIsWeR!O&nd}gfbT#>BX?qvPFLDp+Sd`&$t@P19B^nf}ZG+#xCK~Cna_{THi zD@R2cZ9dq#!87RvlXiF#IKDawImN^>>9Ho@*a7te~XEl!(upz_0EhX z0i%>ZJ_?&Pu?aLSw>;KC13QkQ6?{tvKa!iFfS#ciyPv+W7a>t=cfv-P)m&d}4Lt3s zL8fJ7o<-W6pY!N6wAjqZChKzg5W+6Ysp_M@I1vk8sn5qeK2q#SrX>N&DRZ%D#jaY6!0%iobJ zx7K$aJ|Sm2H=OeLCbmdjDwiGdhf~O{olmEE*oi)|Cdk&5K(QqZhXcVk;?{t}Sid%d z>-Xqky)Q-vDOxNKB_6)}ivU*2+MA9;+Z<~gNj%E%H2f^H1V7N1>`FU|!KGuvVYA=& zwENzcjyO+-9|~q;XxV^xANaCH{;Vr6I^f`5BVAm0T>y9J1exLU;9ntSDe%-W6EMZm zn4+J~+}Xh=Ww(D*?o+PUG4Yiwx#roIY3p$uSVoy?wmCmjlg*;ix{PU)0n|UoH4AOFolexE9kw7wkGNZY0$wf*BPa zWk0o2obx0Q7yu@tc1#;xbDu;JbM>+|?7L0Wp`xqqKTcMSvi6r$Ny}69=qF5xtXxBP zVWCR{9DQR=JR=9(bzZ`tK5Qn(MhNQ{;CO}VZTJO_m0~kKF!X|p_RYt`zX6E3Gi0?L zc~;yP)wRu~gyT0(QCR_pWe%wL1B19VF{coi1-Mys+0m<4Iwn`-YrwcS!+#vdix7Ev z6}#0kxqcfZq@;ze! zR(Vf(x-Ff)mBcc`rfJKZf0Wx=XSY7(nUX@~q`+i2`Q%m3r|%w{W$!abrTK+%Rh@jO z+LV&#%vT)CR`6BM>d6wYEihPknwwKuP7jcDf00Y6x4z-M+{;i);QP7ViVWD z%BcFuUplIXHd7xBIWn~eKTwYog<~` zgQ4L_PY^%i^BRQ&_O+Gs(22dXx~Ne7q%=1UTn(>;3bzFi*U%<60pRoj=OyGRHSnmF z*lNYp?M?|N)KPec@N{2Nnq{FaA8LohZ0)2)m)JquU8(QGekNB3bWKDK?2mo$jCO6( z^(7*Wuls%*IfEz%U5yKmBK#5vA@?imxIdhkA~Pp`n54e3nibc7$7&Jxn8##y*Z{lu z;lTMpz;Fk|YsJ=Wr%%;O`r4++w18*s2n@+-5>xXWFctq8*n7K;;lTyz z9nnm;5KaC!ENaJQH>Yb+{Fy!@>ti=ZPBtT{@-U2H%mP}7i^!x}1>H&Yd2sTSYp*o{ z0C6;77W)TEGC)Y%N*kXE@uyzT?Z#7OdX0x}189K7ainypLJBhH5;2;SYp*>Bis-v5clV9(D`t+Yvoe$fWUSmk=g7tzehMC58C9OOx zNL+ctlPM>~lOaQRGIFq!C&adGxwXP=NYWbr2mUyNo{paV7(f*w!8xO4BxbN?Ko5N4MJ`u&6!4_D~d;i=sshuel`{<+oN?Ffw zrYwm%_vp7SB3*p+{qM6*Y0paJ%}wn@WcD<>&3v7|fpN>K7`<(Obmg%7rhuJ@8>=u9 zB{hPS{+3^j`QBNX2!4>EpuO8MuXF$PT{xrkd*YG1&q_i_`KLaRE ze;mT|*mY=I#ul@C(`{Sj9S*za>hhNF#64+prib>o2=(X0-1Y>5J>$RZzUt8gwlMB} zarig{sS6w_Wb1p68K41--FcJgx8!V(Kc}oY5Xrk^^B0z)6&pcO*@reaUxNx*&}1c{cIYWvg+8sF*oIp& z_Jy8|xG$eyAQlYW+HH!?SU zc@fcHPpY8tMOHPfSbJa7Qv=F%|~~s!K}@ z4k_Cn!lyGZy6!==Z%kas8Z$@f0fzquA`|Kdvf5o56GA^W}mSOW+m=pyPJ2&SN*| zSv*Ko0Lh3{e?<7p>BZN1=$aDj;BU=0R+wlD)LQoiH1RhHZuQU&sYdDy=~LPnennZsrxu zhyF6LbMwQslS_17`T?Ot@6~=kl;I<8Za^vkN_r_IRJjwe4|`_|{Ru%x_A8*UcN~Xu zf*V!LX)ZkoI=a`~fD^;VXO066UQyEx0Vf*nO^>4KfzYn(pH{MDc=lxv30v>H0F-8z z^?+2=`;-dH#NB}ZszV$fd~oPA;E>6uT3=_6K!0IPD@}JdvL!H;APDpfwR6 zaJYR@SW6s#$P0~W9QK(*x$XUOZ%DZU0}y8qlOy~Nryu&T6*f@-48c)7WU_V*vub^H zCQ+^PKX3R5AP(%#rMaz%559F#Q3U9vrKtUU%^bAHnNKSyfA(BpM*W%-zNUx+yJnxW z>e`43T(AuW7!SUfgDW43q8TJ4Hfk51Lus})Ji+KUa>GZ6b^C>v3k}6!-@saENNzW>?%yf*4EZb@j6)UR5${!>STEqmIV@< zq=1iK7;Y%c&Vg@0x=RlM(q3ScTEU_+6y8+IIjQ_O#N{03hwQGC6 z48Z((VHs0EZ-ul-?{i+omWi8|Xb1t(%awA8k|BGF81>~u0Tj!Q{hXqRMPG{a^l*-f z!V1Q&?WIr75tvST!TKs)Uv8dZXXEd(iq0SdA(|O6AI9(iTFkVb4@a}}PZo2|m$Zrx zTwnrcr3@(v`{OB1DGmjCCafPjL~UQ3SfbpC2{+AuKhAkLF|Fu2+LTs&!ZH`Wj?NwW z80^M*K|P~+4IAJ7R%%QF`2hXMsjU>BtK1tAp8hVTvYE%7qSDnaI6%u;L+NzBKf0#$ z--x@!w4N0sPOZ5oH`B}<+_?IB@>M<5_ z0A70wrpJAh{pPPRhClhf?+M6ulVRyPNiG}ZQ&WxBe zr8Z(ucij>PAS2T0WVp@mm_$z5A)X*DrU6Oxg4B?O2n3^eO!lxT z1Y#5(iF2J-I?5lwVwgrk%k}d7y2_|mN*s!AfsBQkD`K1l1ZwdhOL#(w+uU5{EGzCV zOgV@sIHaVDH*Tztni=QZbi$)wvd(AW$qaT=?~=TQaZQID5o;MAS_S9~nii^YU>t@L z-{D~_cNn~vN`pA##eWf<(XXd8LAb(J4|tNqnIlouSFeJNSDo$XE*BhOW3)ClZv^Q~ z9PpZFU!q*dffhV`X{kTe!-E&T_UU zLJD@a|EpwRBlQ|N6S6pj$Hfy(VDc4+=-)7vRY}UNqp^)j6}ZjufpBybc6$6j@gsls zcXdj-EV2T(wzbI>`6*rj6g|?bOaJb|8IoH+?I9*ULkSH6#?5yl22|7i0+YsyXI>`C z5aR6VZ7M%*4M^=8drWzy3IGN{KTmpL>&bV3?F3-Ae>ux?TI21x!Hllk(oPmF|3( z@fg{Igw4h#4}=^2o`?igH;axeOV^NWFe4h_74lyp?9c<6PRzcImNuT->Y1=@bm5oM zla+$-a(bUulsvy3e^M<;xb9|>6Vbcf0J+Eh>zP=4W4gU7F+HS{ug;_OB4S+|)m^uW zam)&!CP%7cj~-f|U*iMt)9X=8TBygLNTdp%*8+e$r`Zj5t__kchODX2?~NlO*e5At zofnUW9-de(ppyv&At3P4Mb|U(%x)lX;Gj;EHjS;dDdN zGtzqXB>*hqqi^eG;(>$nWC+jF=SyJcKuvkbnU6ndAQNAdFGciE3A{hCU7ppW2=)5G z0;)RA?tWzDTEYLcU)0ibysve#oPj+_<_1I~@M@eH98g^k;Xi`9l6g?Gw)g)KCi4ll zRB+34_Mfhd7Z}{;o-zN={QW1~B5PagWyFBnX=#r{TyCbOcE2KAu_M=?+BZ*kV?tANq~GkSz>yH(0`VL zy|T?2{MeX(kgJ280O~>TQtK0kJlU#y$CZWd^)J+f`*pmFnD)Q1C>kgk0`BYb{{GA7 zH__SIhUF>LGfgY}!{B;R2tO5e*BeV(@-4#Dy;${aW^oM8`5Z2Z7bwAVK-5;e>X`I z8eTkGr#j{^lg5_AI}oOJmWRn)x-+nA$fZ@#bd+{i)DhL0Gu8A<_)K$YbfKb>mAYh7 zjxfR`wQIM!axFcF&p3zi3^G`K^NHrSzwtWx1N_!NHgeuNSE8QD`6-5$B9$Y5(t;Fh9uACHEu?>UD0YVz_X<=;-i4@Z zaMsGL|@M?@x#&mIF#qEC)&BI{5G%tnRDl#ngHPmt@_sc zZD~k72H&p-Q>O?nKckO1^Fw3V(C1<9q)YXJj7_;rWe5!RX(Dgmj=jy(J7@G?2GD-{ z*ZYw_DKK)fkpU#yz;aujC;(pJ(tWa-m?Djlm+MbX6?^h(;%AF+S_5TxHNF`OuIgr+ zZ?}8Y-COBt_~4LeUwW%6p1yZ72-;1YzL!ueyXqB=&Adp8ujIujTKD)nU5^({6(#>^OlkqKkbsGEXw1cTL zY?qdCLFzg6F6bwu=N|ZQ?edhhld$$fd4VyRjD3{-AgYNYf>`icB(V5~crH5TV8VL( zJ}C7QHvQ}T@d>Jw4{dlku10DGt^TLS^(w>hyZCV8!@~=bl&TL7vvB4eDLvo$^-?uB z?>FH%%EO~+S%4n!2|o$^x-Ih?d(d`CEbQ}pM?otC1xtsdT(~$j{QYeLG7=QlcO)D_ zvHP4-&g#6-i&1jJ9$xDJEFbqymk;EH|TRmqK~mVazNRrODr-P>8WGmI3dT0YHn zJr5AxJSClO49bID@dv(+h8ucv=AYg1S<-&dI-xIc&-87_^wF~sJZV0e&2+0Wb^RLs zGdFC}z;Qlkd51Upw?@yBL!*q)of(}`;0#S3j9&5FiM)AWTdvv7B;#t_r1Tu1SxktffFwt=IIvFj>Gf zG^}$Y@e3uw*5g%So2>-{A78l??>)4Rz0W8ulK{VoOI)Xf z?j_e<-jithz5ISXx%ah2Md)FHp9zbjHDj&e>wv3dPbegCTdx}wonsiyYdzT~;`v-)K7zXNjR zkni;)Vz)}`-+vtJI&zo}Jm4pS2=efRe}6l*^4hPd{OurfLGd!*Y`jmTAw%@(wMSE5 zu?x#ObaxBul)l!J#cO88!<&o#*LV?n`&+|Sc>V_HRjqRZHg(>9+AHFwb5Am&cxFf1 zd;1FxmFst;696P-0x(^56E1o=K5n}~SFUO0Upn9`^uV}^(RSrZT(C~+Q<|(>TTugX zRzmsRHehRQf-2j%X4^=0RCQI*GH0*&zybq|-EH&0GJn9sZ%&T7fueG?+5=Hmub7@j zoiotmkIs)l@MKcY0T{6vQ|HVq+5redxB^~K#yZR9QFj;ncWe_RFjuok> zt`#2ZJ*#_0_1=hbdmX(R?lFtF9&d?yj{4^v2FoacP{)SZ$s}zE^C+`YSHMXUlMLk0 zv_l@v1ip7jSO1at#ri|xMr39LEi2f>QKH{aw{BoF%dkRSZUOyY{V;vwH&mRVll}sE zKydKw=tKK3!>v;F-|HRDFOwL=2H(Zh4NWagO8r@W5qoDFJ@VS;aHy9TXbzP-{B_Io zb_XZWv6+yElj5E+>Ri4$d~ytHT_I=6>fcj4Tm6=s_7#;$SMN=KEq2{{IgYEHYPq_3+EBv5^`d~BG^Ogt?b}HFXveaZ4qCgvZ-3%Y8D({uy6^45 z1Ehf5Nv4c|)ZZ+H_LuGjfYhmdE%u!L_q8%Z2{HA~iA|Dt$Iyjvw#`icxLm8r-HHH& z*;|rX65$QKhJydfOSYoyvNfjw)(5{stSt1TX?^Fr)>1k>K%9!xjpJK>>N2 z+AEcM@}z;=xI=F~;0<&1EFDi)_$^51kDbuRu7sP7KCqu&Py~z*K;SIu*0sNRs$Pie zOnCoCPSMf#J^zUT0zjN!pm^McZej_ACy2R_T?E#J{G@p%OJgIrdeNsv^#%d3y5Yhf z71;r}7oCTb6Jsj@FX5l)P>K1$69Rj|;@%=YV`O z%LdG{y?N{F(v&f7A7=ro%%*2XXRE7h*nJU)PQ;V3N53j*xnv@*h@RhXg3E5)lfZw< ziJvNeP;Q-W3)8+apJ1I`3IZ1))_+j1_usnQI=>_eMQc8z9IeAM`|#yYmJ?P&sjmSF z;m*g!iILxCa~FnBIz6+`Rn)cB1hlg*s(%R11&HgL?)|E*ujSL%4A@GBPACU~_OUaP zvkMN@xv+T&?=R1LXG>^RpO|y8TuFd=P1IuU!}W3^)8-FU>p&ifkBDR?0VmBBQGkDE zqb-j3@-8Sg){?19FiBzW4~Km))IcdCSriC%cm6YIclMdw&Bt1Tw-Zs)5-NQjP6C3z zP7V>G9NHu+bY6%rwM`zVwlz4hsa*drZ*p0ReZBy8#T59rmVlqI+QT(1iQSJS zeAsyqamymeGx}JK#60wG1n*I`1QY>u^7@jCE6rp?+(vcWFm0>xBlAE+)haT9VCg?o z$cMkm?IvH3`j3w-d5;R@qcKBweSxSC!07E?>6yHm-#tHf`Fn$#CrpG8QxDHx)iE@n zw~7s)utJ0)wzEkBUo~xdd-pZR@6&)_;+2QXTlHRJ*U@cZRK^o-a2TH^l)e=QG`|N8 z9B_|m6ImuID+c0zb5>!z|BY_?RS;!JuWDknREU~_AGnX@ArQi}&?C4r#y0Ss5RgV7 z$G9yghXNN?c8jlJc06*=8$L~9H=u(El9bXJ6-&BTrLTo}V0v8@i~%sq7z-{SU`c)l zXz(L|6t@sCy*v!X1r-`M8S}-yRqPVkY_I=s{Fg(L>lH?t?%Li21}Wwy9zUgkYBDEV z0H*&ENB)lv@i<;{>RhN37N0V=ia}E@uIY#HT^&q{Ac-jf4!UVy=Anh%lw~4poVJwV zy^V~-%C9rH(TRm@h#q91)^a;#y74DU;Yh4>yXlSCG!yFE(bH86GVRB}+=DDg^_B=e zQvAx7ux^7&Z<(0StLAp18Nt}yp^q?acJr0QY0+75ui1)H(t&XjP8cQs9&y#wq8r{G z!~I?}mOP7<_&%xY-E`7cc??&**Fm%FgwTCM%>jv1c$?uTM%8*YH;170^ciV0-H z7gIrX`*zHLrILX{2?xS2$A^4tzpEtQe73;_Un{`)_x%E7(KjK#5|_7qu%oSMSiQcASt!AnXA(jNyg9h-_!6A%iI@mkv9MrycK6Bmzhk{8 z`-#$yCY7-mP56b-7DCMp*}_H&FkvsSwk{%sT>nrnIXctfiSqCv1H=LaUufk_H#Rg8 z?lHVsIaCeC6agB!TI@d#(BcjI*9z@z{9WU|GqPpawp;28sS*Qyy@ewZAv%2D#qXrc zGjojFeIQZtEJ$T5#`prDlR;V6c?fE{ULg8jrOC1L+E7GA#Xb8*^Cs0LoN%aM>GjCs!PF_;ksE8I&n*}9>cPr#`vZ{mEr9qSB4rRqS>R9KVX}R$Dn>Zpu}(R z`vhrX7OD3<_HNuIMWu%~F{KTX{u4-SoZYp?0SlQM=n7fqviz7eFTXkcli@?_6s~Ou z8)a>jSd(K%#`9Lt!DNfJL5VSXwHv#KZQGT+Q1N=m*r0LObE08`!>Hj$s{c1*^x2U= zRBM6hTcvTs#AM{hcyQCE=u^)-E14I|**G`Wcb{xaK27jo)CT4sP1nRkOIxJEere(} zNr%!U*18E6e1?>0K56Myewy4d-}Nl&d@fIQnNs{rAl_ZuBvKcylT9kTDS*9aQx}1{ zycLZdaCWQSYjv}$$V#8PbgCKqjtM8Tg$#j%1LrniK#l-(cqH${y5VKfxi#6So5k%; zFP7rLEUvaILdhAtyXEcPjrqIQC1n1GI)BJaa)~d6c3ICFU+3&~^l14kbW`%d{h5pk zc6AbF`#N7FD|S(s^yJ(pPMkbJF__EU-7~2rfGlvvgb9e-NNk+ZznB%rOT_^nzG$_@ z3#=@BawN1u80d$w>Y#!en84@dA0 ze?9;1_*ZqKnmN7FkbLm#p|8zUVoIKEA15{oHL8>};qC_sx7oLAS#&?66SB>#deZvU zy`Bo4jYj3t7I649KjNCTAcAX_mH+GC>qLit&ky1Ad&AHuu+cVXqP6DZZ?j+A`afiD zXT4H%(o5_<gR%oN3mGo`>XPq-sSkJZATt+t zw<`MtX!3{U$4dyQ@@9ptAE;OxxlEKAoLUI-zvn!$IoWu1B-G^LlF zC-(HxxEBDI8id7X*RC0pn{zwG+KZcvGnl~vm;N@N7Y!>GE=T`}sBQuRx^qJwLSBQf zdB$%eWw7F4orwq;T)&uK8Hxj{mWAP2;#ORCe)?v5a!;Og#jlmu^2iZ>B@18qO}Y~P zeN*10_mBHKvIuR3^z+mET>=B|d~-ZgKfBYtoE&_&5M8EPXwU75QB^F@nchvuNp~&3 zeJqMf&?WD2>claia+^F%$(C>2xwtIIx!v9`)RWKU&mxkth}8%NZ)PQ;A=R4BlM&=T>W$-6@Op zl(zYeji2g#F%D`=>8C@?IeZyc%Q%hr{XIG$uflYM%%{mi-O8Nv8##qI1+ZjQi#_JU zizeDoXxf$td~)eBuw`$(7EM9CkRuOO(L@^?pwvh2^n`&0C6R+5`8|NA#amx^Q0Nf{ zDKXT!D6GLWxYoK{_yEVby6s0vok^<*VF}WkJhGRpqN|PnE#6rdaR9-M8yV?=a|%-! zXyq25&%Rcf;rdm0JCl&^k)n0%2-7OuHMwBE2fq97WPkavo|b?^Ck;0|{^DCzp}@q| z*zGJY0IKNulZ-X3fn2HkP`*FZfPrfc$*DD2$9>`nYiEfdlH?3CMw8{g5U9DQ!+`K# zF=f8(@Nd>ELmS`m1x_-hb{pI4tcNy4P#5l88c%+RlPBkq2BtAyB2AU7C!Sj|V(6UPUObR!;Bz_t z#Q|15m&j@uLwztM1#H!iIN_I4#Ujl8x0sCV`*KE*TK zXSZ<1HFy>!`|kD6u4Hb+B#t6nj(y7R0e#T^M1!^fIT1(pG^xusQya3dd2f&g-XshH z-^2xMq_RHGXcv4g+L#AvW_$2c6}m2cug?WMkKm&q~2 z_szP(0;GKY?|yRbs{g^ssHkACc)h^nQ~%mGi}g)AKH$nRLZ>B+3pj4Hb5^>|6&i?3 zHg1*~_~;zHcrte1w<+Pzd8JQ-+{>JBz4{C%x7ba(2s{b zhC%U{-oU1zMjTQ5-G@GMwm|SLz(FT9>w$o2Pou+wvdX@>?*^Gloo1a(8ClIY#HU4~ zeoju)NLY8%=kNZb#lvy`*#|R;B!OJ8kpZh4m9p`GM(Sql9c)UwzK)?WGf*UfvN@r2X-A~GJ?yia<)%d$LJ9zsZwS-)b-2+i|Zivt`E0d$Y zm49ig4$j3lk(%6jXUvKAHd(I6JaO^phW9emc#GUaVH0g?Y5TDy8Ag|Cq&B2+k2z|V zd~uB-*tv=QUj?(ee+wQ+GNjqqS>sV7TlK8^@~d8;9W5;C`gL!`#UNnc8U7csJ@Naj z%E{Zr;O{4TkkVLJmVs6)S;pGmKR0b?F4=N8u5Pw@*-f7r`YbJcKk79R5=uRjC@~Xp zTuKc0GRf)vsIx<-~%}`c6XOXy$I}5)(z1n)31q&g&=}vVo_n|)x}BDB)>RPsy%Z|wfCnB4sjMGpXC&m@ z`a@Z9RotTM51-kt9x+N~9Qd6M-DDQB#Qw7b@BKnvPkgv&iJslq+{Tz4R2n=k<|u-2 zUMhTbVSLo^GeYNxRupdSFfX_Ka@1$|SEIDR?F$mFczvX*&W6&y-kU7NKb$sKf1%~@ z*Pgej1w29WaDME{!l=ZYHuxXhzGajY zyY#&K>~A0>)siMgmpbm*lJzuO{X&Ov{Wp6zA5st0$Bi9YOm@l8L#opQRXWjt#v|uP z6<5o>=mK}-1>28CCtRw6i7(?U24=9B=a%2tn_ZaF2_&Otk~56WTw~$N^oZ3XI*aX3 z-L)1+OR`^s#+izf1Haij=|sMq_m{oXtXG2W-_UCugcGp@how%bYiAENpg>|)Np-A@r; z+P2N}R38WEaS5fgBO~gB02CwIR!I)8LveI$CsN2#L?e$b^B&Bl`k&)NEw|Y}27N2F zDEhPE_nc=_Fst+D_S=$gs>#aZy%P24o?Bduc3TnayGEww=mTRcz6>(~J@>aOr)v|G zkJ@q9#&0g~e*#8O)hlIt3*VeF!@rdgeQ6+bW2-OS_TMIz4clcfaCqW2 zk4`J*z1ET81o&HFwShgyP{{F!$MY9eUt&p6#C_NcP2;}>a?Zkvdx{k|+)U!_om|Ni zr&>*?`6@5g5+(Qy+SYdyKrGNT>N_GHOcPhzYn^ZvRo|yFMU%?R!|UDn0QBn}Zu1}2 zF)mXOI;cKkby0NLe={*vZ-6W(!f;f-iwW^*@6&eErt6$hkkQGrF?%+{70k@a4`Yt; zxQU~a4_Zt)=}DJ(Qw!ZY|8V9REKqJ9b_@*;+8m{m9(V*lp$$)Yc$1_<5&!4&LH!xt zhy!0rt@o|>{{Q&n+%=qgPwcbL-skKb zZ{mVEreAd!V!!Jwe~^S&UC#!f_c5&sLqE7z3Rxo67~2^GZ}sm_S;VCKs!dF6&!8;-nUcu_>59Q2J1)ibz>(4E)T=^3rH zI~8b3JZT@VX69I`pxaz)C)vbw(=|EO75`5G`9y@J9a}B3g0PK!xGF*skU=$A~U{y4xCr z0NM8Cd45T+t&`owmk*xDQzpZGo;yS0+b>+h3J)#Mq8f(aB3?Wiwwr5hYSJ>@vsgPf zYBCGKoniCtm?8_Sm{$uESbAAjF{kr@)Tr{F-R)GMT{GAz!2139?fZEj`|(PglJnl# z6Vo4O${6IOx7KWM2jCbhwg8@Cp^O1YGt8j|+&15`uO0Do;4=d)v1nw?&(1j0I3}OLtiYPI4 zP;B#M%&W;^C9o$3zHy@~UU}p5GgWqT-N~g*=WHXrU#E3eYK>NtN^6DLL_ibm6D^wQ zs`K=LD4mu$vFwGlM5W0-I^+yv7!L=X>c$u!dH4(F@_5bMcg-@{EP&8k_)&|$6`c&P z1Yfg9&Av9du*O=5^SXbo!eY@7it3-5y|?ig7R0ByCH=P0Z&bE)nT3mUXyFKD^PF+H z(T7Is^)Z^~X_%l=aCl5kVRgK?rt0jU)_;F2*~^0@%b#+KRJ*n3EB*D5MO6}<*& zDn>ZAf3r*AVQVcYDtOrEm5y@pgmOaTKj2&*HTx6&p(S(FDg6Y{Q)le!TKc#tM>%$-H;1BhK91_-tk}#>9Iwc7LpUhq7$SHJhqKNnD6mE+ z=#OLg?2D=)0v-+uncas_LVxhzgl)7?8__0DzUdnhB3XL`o*iZcm@*LnjmL7ZAnjv! z=a4AR_11W=wDrJWP|FOu;a0WArMh=!*S4(Mu>`37{GqEX*roWU*gFxEwiKxM9hZde z`rrB>#Bc`F9!Ql-kGW+r!ir?r!^SZwf8 zyoSDfU4uMH_&m!^I<`?3yn=$W!9O<s=1UUVBTQsLCpRniLOgR>#9vc$pTRYZ| zc7^{o>|N@wE^`D3(!NVQMltsBDD*8U{8oLEl+2?oO3Wd;ADOx_P<^EM-2XZNFc0+| zjTk){zcNWm2>Ew(wfYQ)AjiC@FZUj^n>JPqed>?d6Es)$LNZM5zQpJrD!2_-*XmpC zk!FM3M9)v;p>*&7yDu3}dmUb=Y67@@dh%37Kd7HbuIWu0<pW`M1Q=h;vgVbpcNvlY+5WfE&wqJ26#j%G3NQpz=?Qjfzl>bsWP7m#(d zCTKGdnX5t+AvePEm1fuP?_>1#vW+I6tHK1$iLX6E!$nK~so) z+`yjzX^*`fB-FB|LbrzbQb&+?IPB}6m&4+@F4K}ClnT?)?wV6PgRi5#*!KPd z&hgAYAkJ%Ve6xFnV|&Y^z&mJA{RuzW=~qKonEUUjqOjx#e*6Ms-mn#6!NWL=VWm0S zAR0QH>vPQk;kEot9E6B`Joyy=K}zwDP_{EK0kTf2GC}%_@9If%v6(oWHzJ+QYO>;f zf_*D0v`9l*>&H!_O%>ON|Jf)qa-SpX_l4EgXwFtdys1jNus_tE-arPX2L3cF5!;z^b9-fqRXvUACv5_2hY*TEpEGj{)H0AQ z^H1>_)z=VPQJ9el6VC=_2F#A)y%4&6nG{v90UA0-h4`YZ&42A17VgBi4fSz#uR(CK zM_aotU-9R4kgq!`HQ5p0OR#{Bbl_0I1Wb^|hHCH;mD}9tMk08O$7Pa>Qht)N5;0?VRGE(*%b^42V~8G|LH} zj5{05ds@qIbNdy`xYu`#vuWDnjL_fhQQw!P_V%@iUi+^jx#5My!#kHX$kw-V`d6pt zOplIlLkb|Kx!Z$(qT&g@QS(?EBXZxI^*Q!rOiX9)GEQtp^H^9iB^ZEK=@E z*sYK_S<+`~Ea-_x5WKt7YWaKaR%VZy`u)pU8X@G6!qX8O=RskyVVx2J?zT&J9YS%gkOE*k{ah{|zWM*l2ln*<@RP50?i z;zJ`Q!vNc1t-_L}T1Pm89r=IAnRHYCYKu+(?ftSB=sA^h{Mr&qG|}Pj_03#JkX!m`+J;Nqhha9+nE5z7p!=!} ze3`SL|=~` zCT_^?$g8U5ZQYx4;2@ntg9-^sO*b@|5X}|@Ms<-Iu*@DGwUIt*8wxWB5;t8iJ3~-o zR~Y9v-gJtT!FjVF>!OExjVZNulLEDock5=V*}2EOE#ZkRuAgt(@qV_b+Qi^)#!D;e#MWA#G^`O+EKAD?kaR!?{4l;z7?5$o2Ij-hjY_M1O@caj zL4xG-IDO~5>Bjk+^t$j%efl* zJ%Di)&L^Gs@=~w%?Oxs6oMRUQtb-+t2pYcFY%b@fhp9ER*(+R;yS?@kKJ~Gw?%y+O z_S!nwo9RX{{Uk%(%(6^FjTqk;hh}_PB-mQ=IyIr<^TI3qVlHeBcEzZ^dB9*>`fg2t zfQ1`?{z;#&CN3oE4vXB2H6JsLDK5oP*B&Su#s2#AKp!<9qsC zM6Mmp-Rdv2ox$>*nqx;I5&R@VffBeS;$b3Y1i41_h%i&DRSPlxeWB0dQ7hF|@4JF! z$k5Tfk?9DJDv@5F#?~=?5;WmFn=@nf4@0tPb>c%SnjqG$ z37m?<9=Y2i9Y1F|WRB*%Z-SDR%BF_SsNWW#H_<0HKJq?p#mweQRhl`l7`{IG)*~iMr;-Apf?l`F#BTVpPR(Nw&y?gZ9z!f6!?GfN0TZ0pZ`c_b@k6 zCd7E`fXZj`&%i-{UEs-Fk18h*QwF>}is&UXv9h&k^MYX9bF3&tO<#iQcfi%e#eQF*Pm>dZD}4^*2aT9fC0f;uZc?y6Wc^K7~a?s?4I1fnMzSJ zzK!}_39A38bW4?}!kZ&4T`^3{aerh)t;$=7w6}a;N z?)|0QbPi>IvAfc{wNofS&(#!5*eDO3Sc?uaa513rHn z$-LN{2aJ_*J4##cB3ZP*0vb@zKEX{TbqYLvQ{=%wMem{Q&C^?-6|QT?vi%QpyzN=o zYO8X<;{{d!7ZCEicg!r;B4a|&9Mk=DV{fytj%O;?{0UB@EL(o<#Tdk*#Bt^w%~k0l ztpe^V>zuv3R`J|NFz-{;+{B+*uIBq|b7~3Qdw;7+P}#?A6gvY8oa0epQS zE91!09Q0O>&%|D=5F(O$8nR-=(BJ0O8ozpN;eHj<_ZY!xLZ^MTgsDN)@1>}Gvc0F% zlQdz(Qb1TT^PMHD(e;57GYE{hG7b-S{9i;lZ!n?xD^H(42IM>W%vTD}w?{`l-P`Oa zNQ!IxHvdUUr^Uh%$YsX* zahiSY(2`m*@Dm%lc?33P7^dgZ?fR^RSIL|3c+Y+J&G!T@%*EBy*;T+ZQ>nDINws|{`oGrH>1worms$mH4=Lb8YWHlBfzMdt8E3>#0($W zsR4EHwrY9fXdWWPGmKin1R;>smo7uImysmSl5tz$cCy zd`@*22MJTNN6j>s*zu>80DzbvQ^ch7)3cXSo_0_VM+s4oX+~S`_#B4w(aNKr03?rg zbw#&U4E(KaSnjX*(xbc7g;GrC4(FH;en0qj&LL_fTc)gD_l7e0t$yc_R#bT>pNX(~_ z1ZI9#9;Ya`H4YUH?+C_(zchbBew3Lz&^F~U`Gy|uJ5@h^q?qQHI`l1=E~pN*{{Gm~ z-zexz+(wm&aEU)HQEP43GONh3yrzNp2+Crhod|A@(A8QM~@^y)er zcTf@Ea2y8kPT>;4`$Kdr1|DneZ?U@k>6nU}y;?nkMLJhoocjs^SYZYi?BxJNRNDFAc@_{4F^!$iM zTf0ZMj`gdZe3$4b1k+`hI~t(!`+Li;PdRCm}@|tpXUK1VGWD z_Iyi-Yb^DpD}qizb9Uh)Nh|gWr4MeB>UQP|y@~8Q^V5a%Au-*Cr<3-RIWaj+lnolMX2v7^fq08q?FDX z#%xcUC@VXx-7RhQ0n1XW{)Vsbep}fTxR_ezeazzHiQX|B;#wUM7A7fQ#jSjvDY?$c z8NTQh*8k)!e!p3`_!5`#47)M1$vUSZYMdekE-h{mz0vH74AT9yt>jOf^V32K1gMnh z1^e=aePrpfA|W4Laend1*eMZbJUT4kT3nwqQ+d)({nga&NZUYa@^roeecyyb4jS)O zb#nwhkAGTG09q>_2bnXS%L9>2ku$h4Yo`zccV6U9lzgjvjjTZQljD4PdFD)7Vu6U> zq*ewVHs5|$Ddjh}ZJCaL9XagiIU4@Qy#j3FV>BsNwnEpEg zmG{JI+h4ncZ+1D1f(}y63Ss0wpJ;E z9r4~{WAs%;30s6%uPLlNaRZE}*E-(jQ}ph`!`gsbA}2kc#@s;T~_ zs@DH4kDGmka+fxmj-;2i_+^7R+|_^CJVd#HQF?Iiw!-PSJX1(3-LZtr7xE^(4qqp?i`ibE{}^J z?ZR{PU(7(hM3*6_Obe>}Aa--?oEUL)5X~jdi zWpgED(+asRh|NJxl2XbyPQOF_S{Q=nR9skFDNJXrsefgHVQ{S^zYWuUJ%;7jSht+> zZcxX@b(+P?cN^AEHtQi;9xeZ@!aL5+*#9N(&8R}?~-HXenDaYnc(om z;16LEzMP&LwH>6YiN=l}x*oYZXbtm}_jzc_8j^|3D=*^EXy!y0;^R#EzrsSU+heF3!FmNoZ z#(KQ{#RRYY47@$4(1q)E+e}j>U-z6e(0|&l!9vGGIIX6wcwWw_{?F^$*l9++{lu3` zoy}EN#TcZ$plj}Ujl6^X%C%3QKw+$7ZQ+#@w&n&BZl`guI7V0-)K5Rxcuq+t-?AVjB8i!LXXP#29l(#>ChXv2NT1Ee zn8G=(t5JisTbYdit|8B}T3wSZuGGFzM22%jOQ)&xdm2^V?k>HjUZUio z>Kc=%_FC9tsm=J3PUWeT!FewCJ6fLpeB@ z$YXuMw8v(2A;DH}~DWEp6 znJu+{qy2*s1!AK)*=RhT+;;Szbfl)5Wqd(ia$Iq;ZK4RPP;_xygoU=-ZP&dOse(gq zbCEGSy7NW~;>;BHjxL>t$f;pc8*TR<)-%O(Gi<}^(wqDpbA|8dP!E*M8kOIR-q>OE=H2F{XplVP_%}K zvojW}p!M*OU&iH7A#8q_qC&O#oIz3azXh1Z3JFjOCNJX7C&R}SJFSBs>hn3BiXU&M zWH$@%4twm1egBTkdVt3xXv6QGEas*x(32=0o9OPV>%H#7E@Wh%7T*A}**-dpR->)C z7E4&&Pl11rW3H7~>uY-!t$a6s{?DjA38< zyMtRxf0)^33Z8SiDE{+RWu-%Re#$&H;mffZ%5F9?K1QOU`q+X5SM1}E!Q#YBMmXc& z%&cDfG}QV))%UD~Mc=HUUUL?wsqg90D+IXZu1wU~hb-^<7Pv33cbtpxIbiRx-cC7s zxTE&=E03=&%y4|?2R<6{KLP^yEvY1M@Aj}%_bgh5&u+nIRg10J$NW*z-5I)Gycp8_ z(`v>u$qk9E5qVFr&Wf)p7y%4gT!n1tFfT>k7=Vd$26%Li5OXv@r`Ew{7=1cr!NKvBhWpq`hx~X^GEvx`3zAN69z*IyzhSH`{+<#sOd} zbx~EdfC-YBSJ&D-=EyL?CmfX3K#Feq6N(sEM9{3Ws&}+ZL6;5>j)=>}y^q_xFA4Jl zyNpdk2VnZPa#RE7(g8uor#0tZeuVk7ByLpY2j=?NOF#7M+uz^Z+DptzWIBgt@iAdPG-Q#tQs3Q=Z69Yj2R1@GCHwQ)9aJ`nbc90wA`_Og4xJ-!~l4rj0 z7?UOT5rS>RmgFJ&FgNG>^JEX?*L?rzg7Dac(It?N10=SS?MXeJM+5TP0_J27p3SFa zb897L|9Qcx#k2hI;TI{UI0X}LR@t+9(|sDdq4)a2R0f)7yL}voS4dkg@vrM8=apZr zQ#DVPSD0!+Z!7??I(BC+-oUPny^T=TbC&0RSSGIy<3dB{HD|nvCO3}@!Dz-FSG6=z z9l6l=m>G!-Q)2e>TR55(e!6s-DN^PT;-QzwnmT^Xah>5nv=8wL|6Qq;6Fk0iGPJcb z$>_(p2)XM8+=qCDg@x)A#x-s%Ui*TYMhZf3f6^0`mF<-7H5k6GW?hgjp)nD)K;74L zGKyCEpg1?&1VW!W;yj)H4t(Zf=5V%lpRdvxcR&FD+|!uTQi_7xQ|PYkd-Xj^40U|` zYCL8AKuumQ|1<9)SOgx4SO_L}G7T+e5(}-cneN-i|Gos=X-MB4$z>>bK0!X^GHLzb zM}$n-uKie;kZd%<0jX34cYIzX(HGG`7#J>_n!oo(buyEqnHGC?B~B_#>)pn^`<}af z=JgL2nU>;6DsMnrB2`bN69QSimUAwTG9tA978$%#QYfpbfdbNJ<4uFGQP2{KwP;uf z--Z4aLf=8jCE58U)>3RPMdb8^JI(;1b`$%xRSeOA&VwU-Ju9?dLx;bG0%zZ3({6am z_FrDZ69;_y8=T1aXqBLMv-sOO5wIjri&u?L4j1QmNlfhJTlTS@3Vp`g|R)`A`NTUYULS!GzHjuCz4<4?vLH zvVRaAlg+Z)JYI^bpX)TUtGw%rwZsv>B(Hn^lMkrM!W8j2{ECgeT^^D%Z)#+k z@8NiFC9D{&z*ejBQDUjbg536k3;M4UI(0g~*n)MsaLrHns)39evvqm>*(K7+#(z}Y z-5bmPhS+M#Z*HDe`&_+keO>-^(4>|ges{qI1JPn&@W)EsK2B%%!s_%abRbML_1Rx4 zP@RY9FE1Oh8;N=_EXnSKRI1^96b9XJHM() zZ*6Pp9lEBSxq}E*vvSDWavpC?`y38D-}P#iWZCuUIcofQL~n5`{07lsGT(Nw@fT@Z z1IZRP3qKfUzcCEE%{(}#;J0-7+z(lWBOak3RI-h8vHcb6h>u8<(jdq^`-$?37D`kd zfpX?{-~*vm5_BjScbYT|11Gk(dU)!0EipwOiYSAbE(k^8TjVvBAXCsY-)n-JRCW*> z8KXI1pBN?lsA`cIN{c-<0@=82G)4|Q(RdmjjQm4orX$_ZAZ2m`mq#??Mrf>qdmc{9iDo7BEX>b`7oYNdQdXTVweBEz3x+I8iTl?U)>~CsaLZeH~p? zON%Wvnbg9p$r|as4cNhiP9efcyW?vm#qA|<;_^={hIlZBKwh{*C8nLy|Y1~k% zq|u#T;JZ2*2>%RYkChlV{oHyGR!>@|+wAjK5VphN;z`435>?)|2&p}xdLbQlb1JTs zDf9{{z$ITaWXQ-cWWw%2!Ik=JeaWQCFSLCALVfn?(pfzyHmx(;?dFRQEa!@MycuYh+(2gZqY%?gdOEK1$SaeL<@NH9b`m9&{dd1QUto#D8x%^PEBhL`Alks=Hxduiq z*d-Mvd!`H5GaU5iH-D{5U2MWdWXsRtu()Q`JWU;~ZHb$N75UQRk$W4BtCBgXbHp~9 z{R5m%4iX2uzJK;z(oruQ*!Zc%GxpABV6d~$;Vg&~+L@#N$*omuWopZL2|^cM-v+s* zJvp{J*z8}jEyzjC&q)vRZaTXi8J_#yrH<4KI#R*0Kib_{ui0l-ZynP$ti23vv5Gk4 z*z>14++jM*x_WgDM;Qd2pP6sX`M>u>2pakkQMxc6B6vrPhx06#5@E>2`VzX^qG z_C6Q(6p|&P728${%Z?fm*jwTz`kft!V_h#D`STeQy{Kx(OqF8}8@+$a*mU@`Q+fx4 zUs-kUemGhRrx=kba{Sr%)86A7z_tNFS{*khNTlWLox^fkdi_m0cC-^VAb(oDW3ft? z+_VDC*o_R|rhci2pHLrvGS~txfP_(vl3t<9nFLMpi`()#O8e^oTfG%soptX$amHps zcAu+s$LCWnZ4-`p@|Vy?f$$iscVs1eZ%rJIY-ZODw@<}@CroSs$ncvmaGbQ=mo<>#!aNiv5<*Qfz*>{rjxKwgrzTPGE?_aB}$+Brj zT&WIn=${xMX*6=8*$3hOpW?M2yL~W2(0>@P8!sAUB1PArMPoHb*Qm1i#on|&e5Z3V zR=Y+mcZ4os-=#oP2J=p#x+tK$U}F!VM_~-vLT(m8GYV2ocO4pCL#Dr(EM1{WGz+F2 zR1Y2Rp=(fLn$K2SJDpI5n`)$hfM3>{LtO#61~oQHITC?LC~%(_&2BXRSJu^imSo_Z zmGQ36H#HC^6ao%2)2lFilK50Lp@-hT91UILJ67%&CZL|$8D3*MY;ntr(|ivKzAiPf zi~9RJaft!|=b|1)%#i)<$InW(Iv`07zz+kA86E+;-LVWP6$OQ``fOQHE(N$}-0rNy zPEI@#_n;y^`!|jiPe{>S#nClfF>{kv9?CXK0m@0NhfE&NW4BgmCwCVj2US69^Zi{j zLDZ9{PlN0oUV6O(`Y!p4?xVYUVJ$ZqW9BC0Rp&WqX=%;3`kzO5qfhMD9?ZM0VgNkX z(`r@UZg)l{vp`G4rCY9wM?qYrNs)kYI1o@zmdoJx9QAm??xJnQM%F;~mlP1yF*#Fh z-7g_NmY0EUg15Ym1F+6*+PGygZ-+uW_aHw%k}5_}aD##3p%Fn&jkA^ff>%F^)1CmR zFtEXX;Ma}f^Dw-2C+xs?vZq0}S3hQI?Gx=-q>RMow(%msG-H59tr1E@ctR;Z=rC5| zg5}Yp6@5d|8)|o>_dwEtBgr1Bj1%b4Y#&1wP;*@L2zZ{a#}e>UaK4WF0^bR^%ZwL; z!(Ip4!%B^^Bj>K*^)5N(Ce3;_Lz!$C*aR;}Kd(&3%-jt z>Cpa0Lh(D4pvuU7FmV4Tz6rnMLBX75iBWi%SR|2PFI<+gf{J0 zHY0mt=r}5j>YWRJbXHR7-cl#bV@qM?0-2Z%qcEYoYv3kjh)y<)V5{lf;h$qN*WH=t zB6kC+y>Jvy<9QQum*%i|3Kym)SAj~c769DQa{*_}s5zWmuB^E*Q5LmKAvx}*eLUP4 zTx=H18FBz{b*jAF=?bd7fPl&3mWp7et$xwtbIa2osfEi?8+B+TXbt%Nz!UcwoH!?{ zc8wm5X-at7hkF!#OeDGL1^EzfpU zd!7lwtze&9?SZEq0jOwpbK^_%>vdwmFIFw{Ds8evDj(!dS0 zf!0OoVAcl8fr9c}*ZV;xW0SuZA|7xs3k%PRu1|yM1TS^V^a3@&2nQD%FRpzUP?5lW z613i}QmCMT3E z8W{Ch^TbPR%FQ6wYv+q<&n}pm(2w|hG&%?ahiJeK`a&V}Ety97>nd!MCeJMihlvuEJ>1Py*)N5Kz8Q+O9J#!?-lc*W z&qzz$wh8TAXyw37byPWm^UJiyfX>BhVe(Kv)@_H+392eYMnB$>Fl+4o55?#HR32aq-hg8Q4_gWQQy&^a^U zpaUmc)4^6M759YoQ(f9g_qf;7J(A7A5)<^+=U~D)$cUS6kA7Cb7Cjf8kC&5Kj|JCN zQ=`k+7o{uJpfNB*X7u6Vq01&Eebff&FSzq9&IU3x3w4%=>a;=ON_PpT5YYL!aqe`&%=NS7=YNRD<92 zqcxG&`pF40k_7WmeIx_vVJ~D`{(fNdL30gq8%fD9byhEUn;eyQ8VlS{D1@@&HT;SP z7Sa5!&#Q+*<`w~c%zP_G70{W45#`!rRKMB$0iJ!Tf7){~FEe$+hLl%9-(3S1FAaXr zQjmCJyd^E612q_QY_5%<*!S6|lEVplkHvhmdA6Iga>>_-%D=If&hzkR$ntA5*=w{A z)nHAs+#ZP#MQof!C=Y1~>0^vwk_oq;AS{k3{L5RJzHCl^WWDJqmwfjVLHsQ5qGsRh zV>6(c1zq-(a+UiYHAz@v=R`sv$M~As)tZqcL<-+Q)E)fVw~OA$(Etn1btPRU2{xbr zihQDSDFz^fFDG8^@df4 zOznd*19_CUV>d(XHHT0Qa!lH(Yn7$L;9xNgSl|P zbCRS^)!bEbR*2be_J3m*6m`5kE6@33_MW+2f{tX85H|&DH%98aB4b>zdko2pmTTFQ zyAOSKtA2pa|K?@*pZm93Ls2Mfn_FgObPH0nW{zci5DvkVa))>GiO=jFF)Ft} zJiO7GdMlaIseWjy#HdJ$n}}wFi_Yfyi!ovh90!wdbe?w}s*Rs0wYJ-~ezV8$0_@%x z-I&z189kT)Q)XE?cn}6AnQB{(sr0Ubbx^0idOvX1g)jLl$2bA^y%)AIo9o)$%H#NC za;@ACsNdAS_-*}3Dv97dp7e7?f=*U&)Twf6mS4rs!XDf#L`ZzdF|NjK;3avA1#X2k zJetVLf`dekN9dX|afy^5Z$Xmt6K%ZkCQqp3C5im+A7ncXD!o3>z*)I^F=7gXtfH4( l2KUi>lK-C-fBV)@XxU displayMetrics.heightPixels) { + val padding = (displayMetrics.widthPixels - displayMetrics.heightPixels) / 2 + + setPadding(padding, 0, padding, 0) + } + } + + LayoutInflater.from(this).inflate(layoutResID, base, true) + + super.setContentView(base) + } + override fun onStart() { super.onStart() Broadcasts.register(receiver) + + clashRunning = EmptyBroadcastReceiver().peekService( + this, + Intent(this, ClashService::class.java) + ) != null } override fun onStop() { @@ -83,4 +114,10 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() super.onDestroy() } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + + recreate() + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt b/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt deleted file mode 100644 index 798c66d103..0000000000 --- a/app/src/main/java/com/github/kr328/clash/BootCompleteReceiver.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.github.kr328.clash - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import com.github.kr328.clash.utils.ServiceUtils - -class BootCompleteReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (Intent.ACTION_BOOT_COMPLETED != intent?.action || context == null) - return - - ServiceUtils.startStarterService(context) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ClashStartService.kt b/app/src/main/java/com/github/kr328/clash/ClashStartService.kt deleted file mode 100644 index 1a2325a33b..0000000000 --- a/app/src/main/java/com/github/kr328/clash/ClashStartService.kt +++ /dev/null @@ -1,56 +0,0 @@ -package com.github.kr328.clash - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.Service -import android.content.Intent -import android.os.Binder -import android.os.Build -import android.os.IBinder -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import com.github.kr328.clash.utils.ServiceUtils -import kotlin.concurrent.thread - -class ClashStartService : Service() { - companion object { - private const val NOTIFICATION_CHANNEL_ID = "clash_start_service_notification_channel" - private const val NOTIFICATION_ID = 142 - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - NotificationManagerCompat.from(this).apply { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - createNotificationChannel( - NotificationChannel( - NOTIFICATION_CHANNEL_ID, - getString(R.string.clash_start_service_notification_channel), - NotificationManager.IMPORTANCE_DEFAULT - ) - ) - } - } - - val notification = NotificationCompat - .Builder(this, NOTIFICATION_CHANNEL_ID) - .setContentTitle(getString(R.string.clash_start_service_notification)) - .setSmallIcon(R.drawable.ic_notification_icon) - .build() - - startForeground(NOTIFICATION_ID, notification) - - thread { - Thread.sleep(1000) - - ServiceUtils.startProxyService(this) - - stopSelf() - } - - return START_NOT_STICKY - } - - override fun onBind(intent: Intent?): IBinder? { - return Binder() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ClashStatusActivity.kt b/app/src/main/java/com/github/kr328/clash/ClashStatusActivity.kt deleted file mode 100644 index 1c37b51ae0..0000000000 --- a/app/src/main/java/com/github/kr328/clash/ClashStatusActivity.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.kr328.clash - -class ClashStatusActivity : BaseActivity() \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/Constants.kt b/app/src/main/java/com/github/kr328/clash/Constants.kt deleted file mode 100644 index cd98ccc8cc..0000000000 --- a/app/src/main/java/com/github/kr328/clash/Constants.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.kr328.clash - -object Constants { - const val CLASH_DIR = "clash" - const val PROFILES_DIR = "profiles" -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt deleted file mode 100644 index a29e5a618e..0000000000 --- a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.github.kr328.clash - -import android.app.Activity -import android.content.Context -import android.content.Intent -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter -import com.github.kr328.clash.view.FatItem -import kotlinx.android.synthetic.main.activity_new_profile.* - -class CreateProfileActivity : BaseActivity() { - companion object { - val NEW_PROFILE_SOURCE = listOf( - AdapterData( - R.drawable.ic_new_profile_file, - R.string.clash_new_profile_file_title, - R.string.clash_new_profile_file_summary - ), - AdapterData( - R.drawable.ic_new_profile_url, - R.string.clash_new_profile_url_title, - R.string.clash_new_profile_url_summary - ) - ) - private const val IMPORT_REQUEST_CODE = 1024 - } - - class Adapter(private val context: Context) : BaseAdapter() { - override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { - return ((convertView ?: FatItem(context)) as FatItem).apply { - val current = NEW_PROFILE_SOURCE[position] - - isClickable = false - - icon = context.getDrawable(current.icon) - title = context.getString(current.title) - summary = context.getString(current.summary) - } - } - - override fun getItem(position: Int): Any { - return NEW_PROFILE_SOURCE[position] - } - - override fun getItemId(position: Int): Long { - return position.toLong() - } - - override fun getCount(): Int { - return NEW_PROFILE_SOURCE.size - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == IMPORT_REQUEST_CODE && resultCode == Activity.RESULT_OK) { - finish() - return - } - - super.onActivityResult(requestCode, resultCode, data) - } - - data class AdapterData(val icon: Int, val title: Int, val summary: Int) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_new_profile) - - setSupportActionBar(activity_new_profile_toolbar) - - with(activity_new_profile_list) { - adapter = Adapter(this@CreateProfileActivity) - setOnItemClickListener { _, _, index, _ -> - when (index) { - 0 -> { - startActivityForResult( - Intent( - this@CreateProfileActivity, - ImportFileActivity::class.java - ), IMPORT_REQUEST_CODE - ) - } - 1 -> { - startActivityForResult( - Intent( - this@CreateProfileActivity, - ImportUrlActivity::class.java - ), IMPORT_REQUEST_CODE - ) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt b/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt deleted file mode 100644 index 4b0119276c..0000000000 --- a/app/src/main/java/com/github/kr328/clash/FeedbackActivity.kt +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.kr328.clash - -import android.content.ClipData -import android.content.ClipboardManager -import android.os.Bundle -import android.widget.Toast -import androidx.annotation.Keep -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import kotlinx.android.synthetic.main.activity_feedback.* - -class FeedbackActivity : BaseActivity() { - companion object { - const val KEY_FEEDBACK_ID = "feedback_id" - } - - @Keep - class Fragment : PreferenceFragmentCompat() { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.feedback, rootKey) - - findPreference(KEY_FEEDBACK_ID)?.apply { - summary = MainApplication.userIdentifier - - onPreferenceClickListener = Preference.OnPreferenceClickListener { p -> - val data = ClipData.newPlainText("userIdentifier", summary) - - requireContext().getSystemService(ClipboardManager::class.java) - ?.setPrimaryClip(data) - - Toast.makeText( - requireContext(), - R.string.feedback_feedback_id_copied, - Toast.LENGTH_LONG - ).show() - - true - } - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_feedback) - - setSupportActionBar(activity_feedback_toolbar) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt deleted file mode 100644 index 9948e40d92..0000000000 --- a/app/src/main/java/com/github/kr328/clash/ImportFileActivity.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.github.kr328.clash - -import android.app.Activity -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.os.ParcelFileDescriptor -import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager -import com.charleskorn.kaml.YamlException -import com.github.kr328.clash.adapter.FormAdapter -import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.utils.FileUtils -import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.activity_import_file.* -import java.io.FileOutputStream -import kotlin.concurrent.thread - -class ImportFileActivity : BaseActivity() { - private val elements: List = listOf( - FormAdapter.TextType( - R.drawable.ic_about, - R.string.clash_profile_name, - R.string.clash_profile_name_hint - ), - FormAdapter.FilePickerType( - R.drawable.ic_new_profile_file, - R.string.clash_profile_file, - R.string.clash_profile_file_hint - ) - ) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_import_file) - - setSupportActionBar(activity_import_file_toolbar) - - activity_import_file_form.also { - it.layoutManager = LinearLayoutManager(this) - it.adapter = FormAdapter(this, elements) - } - - activity_import_file_save.setOnClickListener { - activity_import_file_save.visibility = View.GONE - activity_import_file_saving.visibility = View.VISIBLE - - checkAndInsert() - } - - setResult(Activity.RESULT_CANCELED) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (data != null && (activity_import_file_form.adapter as FormAdapter).onActivityResult( - requestCode, - resultCode, - data - ) - ) - return - - super.onActivityResult(requestCode, resultCode, data) - } - - private fun checkAndInsert() { - - val name = elements[0] as FormAdapter.TextType - val file = elements[1] as FormAdapter.FilePickerType - - if (name.content.isEmpty()) { - activity_import_file_save.visibility = View.VISIBLE - activity_import_file_saving.visibility = View.GONE - Snackbar.make( - activity_import_file_root, - R.string.clash_import_file_empty_name, - Snackbar.LENGTH_LONG - ).show() - return - } - - if (file.content == null || file.content == Uri.EMPTY) { - activity_import_file_save.visibility = View.VISIBLE - activity_import_file_saving.visibility = View.GONE - Snackbar.make( - activity_import_file_root, - R.string.clash_import_file_empty_path, - Snackbar.LENGTH_LONG - ).show() - return - } - - val data = - contentResolver.openInputStream(file.content!!)?.use { - it.readBytes().toString(Charsets.UTF_8) - } ?: throw NullPointerException("Unable to open config file") - - - runClash { - try { - val pipe = ParcelFileDescriptor.createPipe() - - thread { - FileOutputStream(pipe[1].fileDescriptor).use { - it.write(data.toByteArray()) - } - - pipe[1].close() - } - - val error = it.checkProfileValid(pipe[0]) - - pipe[0].close() - - if (error != null) - throw Exception(error) - - val cache = - FileUtils.generateRandomFile(filesDir.resolve(Constants.PROFILES_DIR), ".yaml") - - FileOutputStream(cache).use { - it.write(data.toByteArray()) - } - - it.profileService.addProfile( - ClashProfileEntity( - name = name.content, - token = ClashProfileEntity.fileToken(file.content!!.toString()), - file = cache.absolutePath, - active = false, - lastUpdate = System.currentTimeMillis() - ) - ) - - runOnUiThread { - setResult(Activity.RESULT_OK) - - finish() - } - } catch (e: Exception) { - runOnUiThread { - Snackbar.make( - activity_import_file_root, - getString( - R.string.clash_profile_invalid, - e.message?.replace(YamlException::class.java.name + ":", "") - ?: "Unknown" - ), - Snackbar.LENGTH_LONG - ).show() - } - } - - runOnUiThread { - activity_import_file_save.visibility = View.VISIBLE - activity_import_file_saving.visibility = View.GONE - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt b/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt deleted file mode 100644 index 517429c565..0000000000 --- a/app/src/main/java/com/github/kr328/clash/ImportUrlActivity.kt +++ /dev/null @@ -1,168 +0,0 @@ -package com.github.kr328.clash - -import android.app.Activity -import android.content.Intent -import android.os.Bundle -import android.os.ParcelFileDescriptor -import android.view.View -import androidx.recyclerview.widget.LinearLayoutManager -import com.charleskorn.kaml.YamlException -import com.github.kr328.clash.adapter.FormAdapter -import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.utils.FileUtils -import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.activity_import_url.* -import java.io.FileOutputStream -import java.net.URL -import kotlin.concurrent.thread - -class ImportUrlActivity : BaseActivity() { - companion object { - const val DEFAULT_TIMEOUT = 30 * 1000 - } - - private val elements = listOf( - FormAdapter.TextType( - R.drawable.ic_about, - R.string.clash_profile_name, - R.string.clash_profile_name_hint - ), - FormAdapter.TextType( - R.drawable.ic_link, - R.string.clash_profile_url, - R.string.clash_profile_url_hint - ) - ) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_import_url) - - setSupportActionBar(activity_import_url_toolbar) - - activity_import_url_form.also { - it.layoutManager = LinearLayoutManager(this) - it.adapter = FormAdapter(this, elements) - } - - activity_import_url_save.setOnClickListener { - checkAndInsert() - } - - if (intent.action == Intent.ACTION_VIEW - && intent.data?.scheme == "clash" - && intent.data?.host == "install-config" - ) { - (elements[1] as FormAdapter.TextType).content = - intent.data?.getQueryParameter("url") ?: "" - } - - setResult(Activity.RESULT_CANCELED) - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (data != null && (activity_import_url_form.adapter as FormAdapter) - .onActivityResult(requestCode, resultCode, data) - ) - return - - super.onActivityResult(requestCode, resultCode, data) - } - - private fun checkAndInsert() { - try { - val name = elements[0] as FormAdapter.TextType - val url = elements[1] as FormAdapter.TextType - - if (name.content.isBlank()) { - return - } - - if (url.content.isBlank()) { - return - } - - activity_import_url_save.visibility = View.GONE - activity_import_url_saving.visibility = View.VISIBLE - - runClash { - thread { - try { - val connection = URL(url.content).openConnection() - - val data = with(connection) { - connectTimeout = DEFAULT_TIMEOUT - connect() - - getInputStream().bufferedReader().use { - it.readText() - } - } - - val pipe = ParcelFileDescriptor.createPipe() - - thread { - FileOutputStream(pipe[1].fileDescriptor).use { - it.write(data.toByteArray()) - } - - pipe[1].close() - } - - val error = it.checkProfileValid(pipe[0]) - - pipe[0].close() - - if (error != null) - throw Exception(error) - - val cache = - FileUtils.generateRandomFile( - filesDir.resolve(Constants.PROFILES_DIR), - ".yaml" - ) - - FileOutputStream(cache).use { outputStream -> - outputStream.write(data.toByteArray()) - } - - runClash { clash -> - clash.profileService.addProfile( - ClashProfileEntity( - name.content, - ClashProfileEntity.urlToken(url.content), - cache.absolutePath, - false, - System.currentTimeMillis() - ) - ) - } - - runOnUiThread { - setResult(Activity.RESULT_OK) - - finish() - } - } catch (e: Exception) { - runOnUiThread { - Snackbar.make( - activity_import_url_root, - getString( - R.string.clash_profile_invalid, - e.message?.replace(YamlException::class.java.name + ":", "") - ?: "Unknown" - ), - Snackbar.LENGTH_LONG - ).show() - - activity_import_url_save.visibility = View.VISIBLE - activity_import_url_saving.visibility = View.GONE - } - } - } - } - } catch (e: Exception) { - - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/LogActivity.kt b/app/src/main/java/com/github/kr328/clash/LogActivity.kt deleted file mode 100644 index 45036f8b2d..0000000000 --- a/app/src/main/java/com/github/kr328/clash/LogActivity.kt +++ /dev/null @@ -1,93 +0,0 @@ -package com.github.kr328.clash - -import android.os.Bundle -import android.os.Handler -import androidx.collection.CircularArray -import androidx.recyclerview.widget.DividerItemDecoration -import androidx.recyclerview.widget.LinearLayoutManager -import com.github.kr328.clash.adapter.LogAdapter -import com.github.kr328.clash.core.event.Event -import com.github.kr328.clash.core.event.LogEvent -import kotlinx.android.synthetic.main.activity_logs.* - - -class LogActivity : BaseActivity() { - companion object { - const val MAX_EVENT_COUNT = 100 - } - - private var syncLog = false - private val buffer = CircularArray(MAX_EVENT_COUNT) - private val handler = Handler() - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_logs) - - setSupportActionBar(activity_logs_toolbar) - - activity_logs_list.also { - val dividerItemDecoration = DividerItemDecoration( - this, - DividerItemDecoration.VERTICAL - ) - it.addItemDecoration(dividerItemDecoration) - - it.adapter = LogAdapter(this, buffer) - it.layoutManager = LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false) - } - } - - override fun onStart() { - super.onStart() - - syncLog = true - - activity_logs_list.adapter?.notifyDataSetChanged() - - runClash { - it.eventService.registerEventObserver( - LogActivity::class.java.name, - this, - intArrayOf(Event.EVENT_LOG) - ) - } - } - - override fun onStop() { - super.onStop() - - syncLog = false - } - - override fun onDestroy() { - super.onDestroy() - - runClash { - it.eventService.unregisterEventObserver(LogActivity::class.java.name) - } - } - - override fun onLogEvent(event: LogEvent?) { - handler.post { - buffer.addFirst(event) - - if (syncLog) { - activity_logs_list.adapter!!.notifyItemInserted(0) - if (activity_logs_list.computeVerticalScrollOffset() < 30) - activity_logs_list.scrollToPosition(0) - } - - if (buffer.size() >= MAX_EVENT_COUNT) { - buffer.removeFromEnd(buffer.size() - MAX_EVENT_COUNT - 1) - - if (syncLog) { - activity_logs_list.adapter?.notifyItemRangeRemoved( - MAX_EVENT_COUNT - 1, - buffer.size() - MAX_EVENT_COUNT - 1 - ) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 28e8cb9e90..2a096fe9e0 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -1,216 +1,80 @@ package com.github.kr328.clash -import android.app.Activity -import android.app.AlertDialog -import android.content.Intent -import android.content.pm.PackageInfo import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import com.github.kr328.clash.core.event.* -import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.utils.ByteFormatter -import com.github.kr328.clash.service.TunService -import com.github.kr328.clash.utils.ServiceUtils -import com.google.android.material.snackbar.Snackbar +import com.github.kr328.clash.remote.withClash import kotlinx.android.synthetic.main.activity_main.* -import kotlinx.android.synthetic.main.activity_main_clash_status.* -import kotlinx.android.synthetic.main.activity_main_profiles.* -import kotlinx.android.synthetic.main.activity_main_proxy_manage.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch class MainActivity : BaseActivity() { - companion object { - const val VPN_REQUEST_CODE = 233 - } - - private var lastEvent: ProcessEvent? = null + private var bandwidthJob: Job? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - activity_main_clash_proxies.setOnClickListener { - startActivity(Intent(this, ProxyActivity::class.java)) - } - - activity_main_clash_profiles.setOnClickListener { - startActivity(Intent(this, ProfilesActivity::class.java)) - } - - activity_main_clash_settings.setOnClickListener { - startActivity(Intent(this, SettingMainActivity::class.java)) - } - - activity_main_clash_logs.setOnClickListener { - startActivity(Intent(this, LogActivity::class.java)) - } - - activity_main_clash_feedback.setOnClickListener { - startActivity(Intent(this, FeedbackActivity::class.java)) - } - - activity_main_clash_about.setOnClickListener { - showAboutDialog() - } - activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_stopped) - activity_main_clash_status_title.text = getString(R.string.clash_status_stopped) - activity_main_clash_status_summary.text = getString(R.string.clash_status_tap_to_start) - activity_main_clash_proxies.visibility = View.GONE - activity_main_clash_logs.visibility = View.GONE - - activity_main_clash_status.setOnClickListener { - runClash { - when (it.currentProcessStatus) { - ProcessEvent.STARTED -> { - it.stop() - } - else -> runOnUiThread { - ServiceUtils.startProxyService(this)?.also { - startActivityForResult(it, VPN_REQUEST_CODE) - } - } - } - } - } - } - - override fun shouldDisplayHomeAsUpEnabled(): Boolean { - return false + setContentView(R.layout.activity_main) } - override fun onProcessEvent(event: ProcessEvent?) { - runOnUiThread { - if (event == lastEvent) - return@runOnUiThread - - lastEvent = event + override fun onStart() { + super.onStart() - when (event) { - ProcessEvent.STARTED -> { - activity_main_clash_status.setCardBackgroundColor(getColor(R.color.colorAccent)) - activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_started) - activity_main_clash_status_title.text = getString(R.string.clash_status_started) - activity_main_clash_status_summary.text = - getString( - R.string.clash_status_forwarded_traffic, - ByteFormatter.byteToString(0) - ) - activity_main_clash_proxies.visibility = View.VISIBLE - activity_main_clash_logs.visibility = View.VISIBLE - } - else -> { - activity_main_clash_status.setCardBackgroundColor(getColor(R.color.gray)) - activity_main_clash_status_icon.setImageResource(R.drawable.ic_clash_stopped) - activity_main_clash_status_title.text = getString(R.string.clash_status_stopped) - activity_main_clash_status_summary.text = - getString(R.string.clash_status_tap_to_start) - activity_main_clash_proxies.visibility = View.GONE - activity_main_clash_logs.visibility = View.GONE - } + launch { + if (clashRunning) { + status.icon = getDrawable(R.drawable.ic_started) + status.title = getText(R.string.clash_status_started) + status.summary = getString( + R.string.clash_status_forwarded_traffic, + ByteFormatter.byteToString(0L) + ) } - } - } - override fun onProfileReloaded(event: ProfileReloadEvent?) { - runClash { - val general = it.queryGeneral() - - runOnUiThread { - when (general.mode) { - General.Mode.DIRECT -> - activity_main_clash_proxies_summary.text = - getText(R.string.clash_proxy_manage_summary_direct) - General.Mode.GLOBAL -> - activity_main_clash_proxies_summary.text = - getText(R.string.clash_proxy_manage_summary_global) - General.Mode.RULE -> - activity_main_clash_proxies_summary.text = - getText(R.string.clash_proxy_manage_summary_rule) - } - } + startBandwidthPolling() } } - override fun onBandwidthEvent(event: BandwidthEvent?) { - runOnUiThread { - if (lastEvent == ProcessEvent.STARTED) { - activity_main_clash_status_summary.text = - getString( - R.string.clash_status_forwarded_traffic, - ByteFormatter.byteToString(event?.total ?: 0) - ) - } - } - } + override fun onStop() { + super.onStop() - override fun onProfileChanged(event: ProfileChangedEvent?) { - loadActiveProfile() + stopBandwidthPolling() } - override fun onStart() { - super.onStart() - - runClash { - it.eventService.registerEventObserver( - MainActivity::class.java.simpleName, - this, - intArrayOf(Event.EVENT_BANDWIDTH) - ) - } + override suspend fun onClashStarted() { + super.onClashStarted() - onProfileReloaded(ProfileReloadEvent()) - onProfileChanged(ProfileChangedEvent()) + startBandwidthPolling() } - override fun onStop() { - super.onStop() + override suspend fun onClashStopped(reason: String?) { + super.onClashStopped(reason) - runClash { - it.eventService.unregisterEventObserver(MainActivity::class.java.simpleName) - } + stopBandwidthPolling() } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - when (requestCode) { - VPN_REQUEST_CODE -> { - if (resultCode == Activity.RESULT_OK) - startService(Intent(this, TunService::class.java)) - } - } - - super.onActivityResult(requestCode, resultCode, data) - } + private fun startBandwidthPolling() { + if ( bandwidthJob != null ) + return - private fun loadActiveProfile() { - runClash { - val profile = it.profileService.queryActiveProfile() - - runOnUiThread { - if (profile != null) { - activity_main_clash_profiles_summary.text = - getString(R.string.clash_profiles_summary_selected, profile.name) - } else { - activity_main_clash_profiles_summary.text = - getString(R.string.clash_profiles_summary_unselected) + launch { + withClash { + bandwidthJob = launch { + while (clashRunning) { + val bandwidth = queryBandwidth() + status.summary = getString( + R.string.clash_status_forwarded_traffic, + ByteFormatter.byteToString(bandwidth) + ) + delay(1000) + } + bandwidthJob = null } } } } - private fun showAboutDialog() { - val view = LayoutInflater.from(this) - .inflate(R.layout.dialog_about, window.decorView as ViewGroup?, false) - - view.findViewById(android.R.id.summary).text = - packageManager.getPackageInfo(packageName, 0).let(PackageInfo::versionName) - - AlertDialog.Builder(this).setView(view).show() - } - - override fun onErrorEvent(event: ErrorEvent?) { - Snackbar.make(activity_main_root, event?.message ?: "Unknown", Snackbar.LENGTH_LONG).show() + private fun stopBandwidthPolling() { + bandwidthJob?.cancel() + bandwidthJob = null } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt deleted file mode 100644 index 224a07bc95..0000000000 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.github.kr328.clash - -import android.app.AlertDialog -import android.app.Dialog -import android.content.Intent -import android.os.Bundle -import android.os.ParcelFileDescriptor -import android.view.View -import android.widget.PopupMenu -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.LinearLayoutManager -import com.github.kr328.clash.adapter.ProfileAdapter -import com.github.kr328.clash.core.event.ErrorEvent -import com.github.kr328.clash.core.event.ProfileChangedEvent -import com.github.kr328.clash.service.data.ClashProfileEntity -import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.activity_profiles.* -import java.io.File -import java.io.FileOutputStream -import java.net.URL -import kotlin.concurrent.thread - -class ProfilesActivity : BaseActivity() { - private var dialog: Dialog? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_profiles) - - setSupportActionBar(activity_profiles_toolbar) - - activity_profiles_main_list.layoutManager = LinearLayoutManager(this) - activity_profiles_main_list.adapter = ProfileAdapter( - this, - this::onProfileClick, - this::onOperateClick, - this::onProfileLongClick - ) { - startActivity(Intent(this, CreateProfileActivity::class.java)) - } - } - - override fun onStart() { - super.onStart() - - runClash { - it.eventService.registerEventObserver( - ProfilesActivity::class.java.simpleName, - this, - intArrayOf() - ) - } - - reloadList() - } - - override fun onStop() { - super.onStop() - - runClash { - it.eventService.unregisterEventObserver(ProfilesActivity::class.java.simpleName) - } - } - - override fun onProfileChanged(event: ProfileChangedEvent?) { - reloadList() - } - - private fun reloadList() { - runClash { - refreshList(it.profileService.queryProfiles()) - } - } - - private fun refreshList(newData: Array) { - val adapter = activity_profiles_main_list.adapter as ProfileAdapter - val oldData = adapter.profiles - - val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - oldData[oldItemPosition].id == newData[newItemPosition].id - - override fun getOldListSize(): Int = oldData.size - - override fun getNewListSize(): Int = newData.size - - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - oldData[oldItemPosition] == newData[newItemPosition] - }) - - runOnUiThread { - adapter.profiles = newData - result.dispatchUpdatesTo(adapter) - } - } - - private fun onProfileClick(profile: ClashProfileEntity) { - runClash { - it.profileService.setActiveProfile(profile.id) - } - } - - private fun onOperateClick(profile: ClashProfileEntity) { - when { - ClashProfileEntity.isUrlToken(profile.token) -> { - dialog?.dismiss() - - dialog = AlertDialog.Builder(this) - .setTitle(R.string.clash_profile_updating) - .setView(R.layout.dialog_profile_updating) - .setCancelable(false) - .show() - - updateProfile(profile) - } - ClashProfileEntity.isFileToken(profile.token) -> { - Snackbar.make( - activity_profiles_root, - R.string.not_implemented, - Snackbar.LENGTH_LONG - ).show() - } - } - } - - private fun onProfileLongClick(parent: View, profile: ClashProfileEntity) { - PopupMenu(this, parent).apply { - setOnMenuItemClickListener { removeProfile(profile).run { true } } - inflate(R.menu.menu_profile_popup) - show() - } - } - - private fun removeProfile(profile: ClashProfileEntity) { - runClash { - it.profileService.removeProfile(profile.id) - - File(profile.file).delete() - } - } - - private fun updateProfile(profile: ClashProfileEntity) { - val url = ClashProfileEntity.getUrl(profile.token) - - runClash { - thread { - try { - val connection = URL(url).openConnection() - - val data = with(connection) { - connectTimeout = ImportUrlActivity.DEFAULT_TIMEOUT - connect() - - getInputStream().bufferedReader().use { - it.readText() - } - } - - val pipe = ParcelFileDescriptor.createPipe() - - thread { - FileOutputStream(pipe[1].fileDescriptor).use { - it.write(data.toByteArray()) - } - pipe[1].close() - } - - val error = it.checkProfileValid(pipe[0]) - - pipe[0].close() - - if (error != null) - throw Exception(error) - - FileOutputStream(profile.file).use { outputStream -> - outputStream.write(data.toByteArray()) - } - - runClash { clash -> - clash.profileService.touchProfile(profile.id) - } - } catch (e: Exception) { - runOnUiThread { - Snackbar.make( - activity_profiles_root, - getString(R.string.clash_profile_invalid, e.toString()), - Snackbar.LENGTH_LONG - ).show() - } - } - - runOnUiThread { - if (dialog?.isShowing == true) - dialog?.dismiss() - } - } - } - } - - override fun onErrorEvent(event: ErrorEvent?) { - Snackbar.make(activity_profiles_root, event?.message ?: "Unknown", Snackbar.LENGTH_LONG) - .show() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt deleted file mode 100644 index ff9fcab20a..0000000000 --- a/app/src/main/java/com/github/kr328/clash/ProxyActivity.kt +++ /dev/null @@ -1,183 +0,0 @@ -package com.github.kr328.clash - -import android.os.Bundle -import com.github.kr328.clash.adapter.ProxyAdapter -import com.github.kr328.clash.callback.IUrlTestCallback -import com.github.kr328.clash.core.event.ErrorEvent -import com.github.kr328.clash.core.model.General -import com.github.kr328.clash.core.model.Proxy -import com.github.kr328.clash.model.ListProxy -import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.activity_proxies.* - -class ProxyActivity : BaseActivity() { - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_proxies) - - setSupportActionBar(activity_proxies_toolbar) - - activity_proxies_list.also { - it.adapter = ProxyAdapter(this, this::setProxySelected, this::urlTest) - it.layoutManager = (it.adapter!! as ProxyAdapter).getLayoutManager() - } - - activity_proxies_swipe.setOnRefreshListener { - refreshList() - } - - refreshList() - } - - private fun setProxySelected(name: String, selected: String) { - runClash { - it.setSelectProxy(name, selected) - } - } - - private fun urlTest(position: Int, size: Int) { - val adapter = (activity_proxies_list.adapter as ProxyAdapter) - - runClash { - val proxies = adapter.elements.subList(position, position + size) - .filterIsInstance() - .mapIndexed { index, proxy -> proxy.name to IndexedValue(index, proxy) } - .toMap() - - for ((_, p) in proxies) { - p.value.delay = -1 - } - - it.startUrlTest(proxies.keys.toTypedArray(), object : IUrlTestCallback.Stub() { - override fun onResult(proxy: String?, delay: Long) { - if (proxy == null) { - (adapter.elements[position] as ListProxy.ListProxyHeader).urlTest = false - - runOnUiThread { - adapter.notifyItemRangeChanged(position, size) - } - - return - } - - val p = proxies[proxy] ?: return - - p.value.delay = delay - - runOnUiThread { - adapter.notifyItemChanged(position + p.index + 1) - } - } - }) - } - } - - private fun refreshList() { - if (!activity_proxies_swipe.isRefreshing) - activity_proxies_swipe.isRefreshing = true - - runClash { clash -> - val proxies = clash.queryAllProxies().uncompress().map { - it.name to it - }.toMap() - val general = clash.queryGeneral() - val order = (proxies["GLOBAL"]?.all ?: emptyList()) - .mapIndexed { index, name -> - name to index - }.toMap() - - val listData = proxies - .map { it.value } - .asSequence() - .filter { - when (it.type) { - Proxy.Type.URL_TEST -> true - Proxy.Type.FALLBACK -> true - Proxy.Type.LOAD_BALANCE -> true - Proxy.Type.SELECT -> true - else -> false - } - } - .filter { - when (general.mode) { - General.Mode.GLOBAL -> it.name == "GLOBAL" - General.Mode.RULE -> it.name != "GLOBAL" - else -> true - } - } - .sortedWith(compareBy( - { - it.type != Proxy.Type.SELECT - }, { - order[it.name] ?: Int.MAX_VALUE - }) - ) - .flatMap { - val header = - ListProxy.ListProxyHeader(it.name, it.type, it.now, 0) - - sequenceOf(header) + - it.all - .mapNotNull { name -> proxies[name] } - .map { item -> - ListProxy.ListProxyItem( - item.name, - item.type.toString(), - item.delay, - header - ) - } - .asSequence() - } - .mapIndexed { index, listProxy -> - if (listProxy is ListProxy.ListProxyItem) { - if (listProxy.name == listProxy.header.now) - listProxy.header.nowIndex = index - } - listProxy - } - .toList() - - val listDataOldChanged = (activity_proxies_list.adapter!! as ProxyAdapter) - .elements - .filterIsInstance(ListProxy.ListProxyHeader::class.java) - .map { it.nowIndex } - - val listDataChanged = listData - .filterIsInstance() - .map { it.nowIndex } - - val changed = (if (listDataOldChanged.size != listDataChanged.size) - (0..listData.size).toList() - else { - listDataChanged.mapIndexed { index, i -> - if (i == listDataOldChanged[index]) - emptyList() - else - listOf(listDataOldChanged[index], i) - }.flatten() + listData.withIndex().filter { - it.value is ListProxy.ListProxyHeader - }.map { - it.index - } - }).toSet() - - runOnUiThread { - activity_proxies_swipe.isRefreshing = false - - (activity_proxies_list.adapter!! as ProxyAdapter).apply { - elements = listData - - changed.forEach { - notifyItemChanged(it) - } - } - } - } - } - - override fun onErrorEvent(event: ErrorEvent?) { - Snackbar.make(activity_proxies_root, event?.message ?: "Unknown", Snackbar.LENGTH_LONG) - .show() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt deleted file mode 100644 index ae0523d943..0000000000 --- a/app/src/main/java/com/github/kr328/clash/SettingAccessActivity.kt +++ /dev/null @@ -1,282 +0,0 @@ -package com.github.kr328.clash - -import android.content.Context -import android.graphics.drawable.Drawable -import android.os.Bundle -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 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) - - setSupportActionBar(activity_setting_access_toolbar) - - activity_setting_access_allow_all.setOnClickListener { - updateSelectedMode(0) - } - - activity_setting_access_allow.setOnClickListener { - updateSelectedMode(1) - } - - activity_setting_access_disallow.setOnClickListener { - updateSelectedMode(2) - } - - 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 -> - updateSelectedMode(0) - ClashSettingService.ACCESS_CONTROL_MODE_ALLOW -> - updateSelectedMode(1) - ClashSettingService.ACCESS_CONTROL_MODE_DISALLOW -> - updateSelectedMode(2) - } - } - - val exclude = resources.getStringArray(R.array.default_disallow_application).toSet() - val selected = settings.accessControlApps.toMutableSet() - - val applications = packageManager.getInstalledApplications(0) - .filterNot { - exclude.contains(it.packageName) - } - .map { app -> - AppInfo( - app.packageName, - app.loadLabel(packageManager).toString(), - app.loadIcon(packageManager) - ) - } - .sortedWith( - compareBy( - { app -> !selected.contains(app.packageName) }, - { app -> app.name }) - ) - - 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 updateSelectedMode(mode: Int) { - when (mode) { - 0 -> { - activity_setting_access_allow_all.isChecked = true - activity_setting_access_allow.isChecked = false - activity_setting_access_disallow.isChecked = false - - showList = false - } - 1 -> { - activity_setting_access_allow_all.isChecked = false - activity_setting_access_allow.isChecked = true - activity_setting_access_disallow.isChecked = false - - showList = true - } - 2 -> { - activity_setting_access_allow_all.isChecked = false - activity_setting_access_allow.isChecked = false - activity_setting_access_disallow.isChecked = true - - showList = true - } - } - - updateListStatus() - } - - 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/SettingApplicationActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt deleted file mode 100644 index 7ca62e03ee..0000000000 --- a/app/src/main/java/com/github/kr328/clash/SettingApplicationActivity.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.github.kr328.clash - -import android.content.ComponentName -import android.content.Context -import android.content.pm.PackageManager -import android.os.Bundle -import androidx.annotation.Keep -import androidx.preference.CheckBoxPreference -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import com.github.kr328.clash.core.utils.Log -import kotlinx.android.synthetic.main.activity_setting_application.* -import kotlin.concurrent.thread - -class SettingApplicationActivity : BaseActivity() { - companion object { - const val KEY_START_ON_BOOT = "key_application_start_on_boot" - } - - @Keep - class Fragment : PreferenceFragmentCompat(), Preference.OnPreferenceChangeListener { - private var startOnBootStatus = false - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.setting_application, rootKey) - - findPreference(KEY_START_ON_BOOT)?.also { - it.isChecked = startOnBootStatus - it.onPreferenceChangeListener = this - } - } - - override fun onAttach(context: Context) { - super.onAttach(context) - - val status = context.packageManager - .getComponentEnabledSetting( - ComponentName.createRelative( - requireContext(), - BootCompleteReceiver::class.java.name - ) - ) - - startOnBootStatus = status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED - - findPreference(KEY_START_ON_BOOT)?.also { - it.isChecked = startOnBootStatus - it.onPreferenceChangeListener = this - } - } - - override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { - if (!isAdded) - return false - - when (preference?.key) { - KEY_START_ON_BOOT -> { - val enabled = newValue as Boolean? ?: false - - thread { - try { - setBootCompleteReceiverEnabled(enabled) - } catch (e: Exception) { - Log.w("Set boot complete failure", e) - } - } - } - } - - return true - } - - private fun setBootCompleteReceiverEnabled(enabled: Boolean) { - if (enabled) { - requireActivity().packageManager - .setComponentEnabledSetting( - ComponentName.createRelative( - requireActivity(), - BootCompleteReceiver::class.java.name - ), - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, - PackageManager.DONT_KILL_APP - ) - } else { - requireActivity().packageManager - .setComponentEnabledSetting( - ComponentName.createRelative( - requireActivity(), - BootCompleteReceiver::class.java.name - ), - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, - PackageManager.DONT_KILL_APP - ) - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_setting_application) - - setSupportActionBar(activity_setting_application_toolbar) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/SettingMainActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingMainActivity.kt deleted file mode 100644 index d8b1ae58ff..0000000000 --- a/app/src/main/java/com/github/kr328/clash/SettingMainActivity.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.kr328.clash - -import android.os.Bundle -import androidx.annotation.Keep -import androidx.preference.PreferenceFragmentCompat -import kotlinx.android.synthetic.main.activity_setting_main.* - -class SettingMainActivity : BaseActivity() { - @Keep - class Fragment : PreferenceFragmentCompat() { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.setting_main, rootKey) - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_setting_main) - - setSupportActionBar(activity_setting_main_toolbar) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt deleted file mode 100644 index 563fecb5bb..0000000000 --- a/app/src/main/java/com/github/kr328/clash/SettingProxyActivity.kt +++ /dev/null @@ -1,105 +0,0 @@ -package com.github.kr328.clash - -import android.content.Context -import android.os.Bundle -import android.view.View -import androidx.annotation.Keep -import androidx.core.content.edit -import androidx.preference.CheckBoxPreference -import androidx.preference.PreferenceFragmentCompat -import kotlinx.android.synthetic.main.activity_setting_proxy.* - -class SettingProxyActivity : BaseActivity() { - companion object { - private const val KEY_BYPASS_PRIVATE_NETWORK = "key_vpn_setting_bypass_private_network" - private const val KEY_IPV6_SUPPORT = "key_vpn_setting_ipv6_support" - private const val KEY_DNS_HIJACKING = "key_dns_hijacking" - } - - @Keep - class Fragment : PreferenceFragmentCompat() { - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.setting_proxy, rootKey) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - (requireActivity() as SettingProxyActivity).runClash { - val settings = it.settingService - - val ipv6 = false - val privateNetwork = settings.isBypassPrivateNetwork - val dnsHijacking = settings.isDnsHijackingEnabled - - requireActivity().runOnUiThread { - findPreference(KEY_IPV6_SUPPORT)?.isChecked = ipv6 - findPreference(KEY_BYPASS_PRIVATE_NETWORK)?.isChecked = - privateNetwork - findPreference(KEY_DNS_HIJACKING)?.isChecked = dnsHijacking - } - } - } - - override fun onStop() { - super.onStop() - - (requireActivity() as SettingProxyActivity).runClash { - val settings = it.settingService - - settings.isIPv6Enabled = - findPreference(KEY_IPV6_SUPPORT)?.isChecked ?: false - settings.isBypassPrivateNetwork = - findPreference(KEY_BYPASS_PRIVATE_NETWORK)?.isChecked - ?: true - settings.isDnsHijackingEnabled = - findPreference(KEY_DNS_HIJACKING)?.isChecked ?: true - } - } - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_setting_proxy) - - setSupportActionBar(activity_setting_proxy_toolbar) - - activity_setting_proxy_vpn_mode.setOnClickListener { - activity_setting_proxy_vpn_mode.isChecked = true - activity_setting_proxy_proxy_only_mode.isChecked = false - - activity_setting_proxy_divider.visibility = View.VISIBLE - activity_setting_proxy_content.visibility = View.VISIBLE - } - - activity_setting_proxy_proxy_only_mode.setOnClickListener { - activity_setting_proxy_vpn_mode.isChecked = false - activity_setting_proxy_proxy_only_mode.isChecked = true - - activity_setting_proxy_divider.visibility = View.GONE - activity_setting_proxy_content.visibility = View.GONE - } - - getSharedPreferences("application", Context.MODE_PRIVATE).apply { - when (getString(MainApplication.KEY_PROXY_MODE, MainApplication.PROXY_MODE_VPN)) { - MainApplication.PROXY_MODE_VPN -> - activity_setting_proxy_vpn_mode.performClick() - MainApplication.PROXY_MODE_PROXY_ONLY -> - activity_setting_proxy_proxy_only_mode.performClick() - } - } - } - - override fun onStop() { - super.onStop() - - getSharedPreferences("application", Context.MODE_PRIVATE).edit { - when { - activity_setting_proxy_vpn_mode.isChecked -> - putString(MainApplication.KEY_PROXY_MODE, MainApplication.PROXY_MODE_VPN) - activity_setting_proxy_proxy_only_mode.isChecked -> - putString(MainApplication.KEY_PROXY_MODE, MainApplication.PROXY_MODE_PROXY_ONLY) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/TileService.kt b/app/src/main/java/com/github/kr328/clash/TileService.kt deleted file mode 100644 index 4fb81921d1..0000000000 --- a/app/src/main/java/com/github/kr328/clash/TileService.kt +++ /dev/null @@ -1,97 +0,0 @@ -package com.github.kr328.clash - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.service.quicksettings.Tile -import android.service.quicksettings.TileService -import com.github.kr328.clash.core.event.ProcessEvent -import com.github.kr328.clash.service.ClashService -import com.github.kr328.clash.service.Constants -import com.github.kr328.clash.service.IClashService -import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.utils.ServiceUtils - -class TileService : TileService() { - override fun onClick() { - val tile = qsTile - - when (tile.state) { - Tile.STATE_INACTIVE -> { - ServiceUtils.startStarterService(this) - } - Tile.STATE_ACTIVE -> { - val binder = - clashStatusReceiver.peekService(this, Intent(this, ClashService::class.java)) - - runCatching { - val clash = IClashService.Stub.asInterface(binder) - - clash?.stop() - } - } - } - } - - override fun onStartListening() { - refreshStatus() - } - - private fun refreshStatus() { - if (qsTile == null) - return - - val current = getCurrentStatus() - - when (current.first) { - ProcessEvent.STARTED -> { - qsTile.state = Tile.STATE_ACTIVE - qsTile.label = current.second?.name ?: getString(R.string.launch_name) - } - ProcessEvent.STOPPED -> { - qsTile.state = Tile.STATE_INACTIVE - qsTile.label = getString(R.string.launch_name) - } - } - - qsTile.updateTile() - } - - private val clashStatusReceiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - refreshStatus() - } - } - - override fun onCreate() { - super.onCreate() - - registerReceiver( - clashStatusReceiver, - IntentFilter().apply { - addAction(Constants.CLASH_PROCESS_BROADCAST_ACTION) - addAction(Constants.CLASH_RELOAD_BROADCAST_ACTION) - } - ) - } - - override fun onDestroy() { - super.onDestroy() - - unregisterReceiver(clashStatusReceiver) - } - - private fun getCurrentStatus(): Pair { - val service = - IClashService.Stub.asInterface( - clashStatusReceiver - .peekService(this, Intent(this, ClashService::class.java)) - ) - - return runCatching { - (service?.currentProcessStatus - ?: ProcessEvent.STOPPED) to service?.profileService?.queryActiveProfile() - }.getOrNull() ?: ProcessEvent.STOPPED to null - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt deleted file mode 100644 index 50fc57b244..0000000000 --- a/app/src/main/java/com/github/kr328/clash/adapter/FormAdapter.kt +++ /dev/null @@ -1,162 +0,0 @@ -package com.github.kr328.clash.adapter - -import android.app.Activity -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.net.Uri -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.EditText -import android.widget.ImageView -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import androidx.recyclerview.widget.RecyclerView -import com.github.kr328.clash.R -import com.google.android.material.dialog.MaterialAlertDialogBuilder - -class FormAdapter( - private val activity: Activity, - private val elements: List -) : RecyclerView.Adapter() { - companion object { - const val BASE_REQUEST_CODE = 14654 - } - - interface Type { - val content: Any? - } - - data class TextType( - val icon: Int, - val title: Int, - val hint: Int, - override var content: String = "" - ) : Type - - data class FilePickerType( - val icon: Int, - val title: Int, - val hint: Int, - override var content: Uri? = Uri.EMPTY - ) : Type - - class TextHolder(view: View) : RecyclerView.ViewHolder(view) { - val icon: ImageView = view.findViewById(R.id.adapter_form_text_icon) - val title: TextView = view.findViewById(R.id.adapter_form_text_title) - val text: TextView = view.findViewById(R.id.adapter_form_text_text) - val clickable: View = view.findViewById(R.id.adapter_form_text_clickable) - } - - fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Boolean { - val position = requestCode - BASE_REQUEST_CODE - - if (position in 0..elements.size) { - val element = elements[position] - - if (element is FilePickerType) { - if (resultCode == RESULT_OK) { - element.content = data.data - notifyItemChanged(position) - } - - return true - } - } - - return false - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (val current = elements[position]) { - is TextType -> { - val castedHolder = holder as TextHolder - - castedHolder.icon.setImageResource(current.icon) - castedHolder.title.text = activity.getText(current.title) - castedHolder.text.hint = activity.getText(current.hint) - castedHolder.text.text = current.content - castedHolder.clickable.setOnClickListener { - showTextEditDialog( - castedHolder.text.text.toString(), - castedHolder.title.text.toString(), - castedHolder.text.hint.toString() - ) { - current.content = it - notifyItemChanged(position) - } - } - } - is FilePickerType -> { - val castedHolder = holder as TextHolder - - castedHolder.icon.setImageResource(current.icon) - castedHolder.title.text = activity.getText(current.title) - castedHolder.text.hint = activity.getText(current.hint) - castedHolder.text.text = current.content?.pathSegments?.lastOrNull() ?: "" - castedHolder.clickable.setOnClickListener { - activity.startActivityForResult( - Intent(Intent.ACTION_GET_CONTENT) - .setType("*/*"), - BASE_REQUEST_CODE + position - ) - } - } - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - TextType::class.java.hashCode(), FilePickerType::class.java.hashCode() -> - TextHolder( - LayoutInflater.from(activity).inflate( - R.layout.adapter_form_text, - parent, - false - ) - ) - else -> throw IllegalArgumentException() - } - } - - override fun getItemCount(): Int { - return elements.size - } - - override fun getItemViewType(position: Int): Int { - return elements[position].javaClass.hashCode() - } - - private fun showTextEditDialog( - initial: String, - title: String, - hint: String, - callback: (String) -> Unit - ) { - MaterialAlertDialogBuilder(activity) - .setTitle(title) - .setView(R.layout.dialog_text_edit) - .setPositiveButton(R.string.ok) { _, _ -> } - .setNegativeButton(R.string.cancel) { _, _ -> } - .create() - .apply { - setOnShowListener { - val data = findViewById(R.id.dialog_text_edit).also { - it?.hint = hint - it?.setText(initial) - } - getButton(AlertDialog.BUTTON_POSITIVE)?.apply { - setTextColor(context.getColor(R.color.colorAccent)) - setOnClickListener { - callback(data?.text?.toString() ?: "") - dismiss() - } - } - getButton(AlertDialog.BUTTON_NEGATIVE)?.apply { - setTextColor(context.getColor(R.color.colorAccent)) - } - } - } - .show() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt deleted file mode 100644 index f5fbd1ab8c..0000000000 --- a/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.github.kr328.clash.adapter - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.collection.CircularArray -import androidx.recyclerview.widget.RecyclerView -import com.github.kr328.clash.R -import com.github.kr328.clash.core.event.LogEvent -import java.text.SimpleDateFormat -import java.util.* - -class LogAdapter( - private val content: Context, - private val buffer: CircularArray -) : RecyclerView.Adapter() { - private val formatter = SimpleDateFormat("HH:mm:ss", Locale.getDefault()) - - class Holder(view: View) : RecyclerView.ViewHolder(view) { - val type: TextView = view.findViewById(R.id.adapter_log_type) - val time: TextView = view.findViewById(R.id.adapter_log_time) - val content: TextView = view.findViewById(R.id.adapter_log_content) - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { - return Holder( - LayoutInflater.from(content) - .inflate(R.layout.adapter_log, parent, false) - ) - } - - override fun getItemCount(): Int { - return buffer.size() - } - - override fun onBindViewHolder(holder: Holder, position: Int) { - val current = buffer.get(position) - - holder.type.text = current.level.toString() - holder.time.text = formatter.format(Date(current.time)) - holder.content.text = current.message - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt deleted file mode 100644 index e4b0fbcf0c..0000000000 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt +++ /dev/null @@ -1,117 +0,0 @@ -package com.github.kr328.clash.adapter - -import android.content.Context -import android.view.View -import android.view.ViewGroup -import androidx.recyclerview.widget.RecyclerView -import com.github.kr328.clash.R -import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.view.FatItem -import com.github.kr328.clash.view.RadioFatItem -import java.text.SimpleDateFormat -import java.util.* - -class ProfileAdapter( - private val context: Context, - private val onClick: (ClashProfileEntity) -> Unit, - private val onOperateClick: (ClashProfileEntity) -> Unit, - private val onLongClicked: (View, ClashProfileEntity) -> Unit, - private val onNewProfile: () -> Unit -) : - RecyclerView.Adapter() { - var profiles: Array = emptyArray() - - class ProfileViewHolder(val view: RadioFatItem) : RecyclerView.ViewHolder(view) - class NewProfileHolder(val view: FatItem) : RecyclerView.ViewHolder(view) - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - if (viewType == 0) { - return NewProfileHolder(FatItem(context).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - }) - } - - return ProfileViewHolder( - RadioFatItem(context).apply { - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - ) - } - - override fun getItemCount(): Int { - return profiles.size + 1 - } - - override fun onBindViewHolder(raw: RecyclerView.ViewHolder, position: Int) { - if (position == profiles.size) { - val holder = raw as NewProfileHolder - - holder.view.icon = context.getDrawable(R.drawable.ic_new_profile) - holder.view.title = context.getString(R.string.clash_new_profile) - - holder.view.setOnClickListener { - onNewProfile() - } - - return - } - - val current = profiles[position] - val holder = raw as ProfileViewHolder - - holder.view.title = current.name - holder.view.isChecked = current.active - holder.view.setOnClickListener { - onClick(current) - } - holder.view.setOnOperationOnClickListener(View.OnClickListener { - onOperateClick(current) - }) - holder.view.setOnLongClickListener { - onLongClicked(it, current).run { true } - } - - val profileUpdateDate = GregorianCalendar().apply { - timeInMillis = current.lastUpdate - } - val now = Calendar.getInstance() - - val formatter = if (profileUpdateDate.get(Calendar.YEAR) == now.get(Calendar.YEAR) && - profileUpdateDate.get(Calendar.MONTH) == now.get(Calendar.MONTH) && - profileUpdateDate.get(Calendar.DAY_OF_MONTH) == now.get(Calendar.DAY_OF_MONTH) - ) - SimpleDateFormat("HH:mm:ss", Locale.getDefault()) - else - SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - - when { - ClashProfileEntity.isFileToken(current.token) -> { - holder.view.operation = context.getDrawable(R.drawable.ic_edit) - holder.view.summary = context.getString( - R.string.clash_profile_item_summary_file, - formatter.format(profileUpdateDate.time) - ) - } - ClashProfileEntity.isUrlToken(current.token) -> { - holder.view.operation = context.getDrawable(R.drawable.ic_sync) - holder.view.summary = context.getString( - R.string.clash_profile_item_summary_url, - formatter.format(profileUpdateDate.time) - ) - } - } - } - - override fun getItemViewType(position: Int): Int { - return if (position == profiles.size) - 0 - else - 1 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt deleted file mode 100644 index 3bdcd05ea5..0000000000 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt +++ /dev/null @@ -1,167 +0,0 @@ -package com.github.kr328.clash.adapter - -import android.content.Context -import android.graphics.Color -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.RecyclerView -import com.github.kr328.clash.R -import com.github.kr328.clash.core.model.Proxy -import com.github.kr328.clash.model.ListProxy -import com.google.android.material.card.MaterialCardView - -class ProxyAdapter( - private val context: Context, - private val onSelect: (String, String) -> Unit, - private val onUrlTest: (Int, Int) -> Unit -) : - RecyclerView.Adapter() { - var elements: List = emptyList() - var clickable: Boolean = false - - class HeaderHolder(view: View) : RecyclerView.ViewHolder(view) { - val name: TextView = view.findViewById(R.id.adapter_proxy_header_name) - val test: View = view.findViewById(R.id.adapter_proxy_header_url_test) - } - - class ItemHolder(view: View) : RecyclerView.ViewHolder(view) { - val name: TextView = view.findViewById(R.id.adapter_proxy_item_name) - val type: TextView = view.findViewById(R.id.adapter_proxy_item_type) - val delay: TextView = view.findViewById(R.id.adapter_proxy_item_delay) - val card: MaterialCardView = view.findViewById(R.id.adapter_proxy_item_card) - } - - override fun getItemViewType(position: Int): Int { - return when (elements[position]) { - is ListProxy.ListProxyItem -> 1 - is ListProxy.ListProxyHeader -> 2 - else -> throw IllegalArgumentException("Invalid type") - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - return when (viewType) { - 1 -> ItemHolder( - LayoutInflater.from(context).inflate( - R.layout.adapter_proxy_item, - parent, - false - ) - ) - 2 -> HeaderHolder( - LayoutInflater.from(context).inflate( - R.layout.adapter_proxy_header, - parent, - false - ) - ) - else -> throw IllegalArgumentException("Invalid type") - } - } - - override fun getItemCount(): Int { - return elements.size - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - when (val current = elements[position]) { - is ListProxy.ListProxyItem -> bindItemView(holder as ItemHolder, current, position) - is ListProxy.ListProxyHeader -> bindHeaderView(holder as HeaderHolder, current) - } - } - - fun getLayoutManager(): GridLayoutManager { - return GridLayoutManager(context, 2).apply { - spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { - override fun getSpanSize(position: Int): Int { - return when (elements[position]) { - is ListProxy.ListProxyHeader -> 2 - is ListProxy.ListProxyItem -> 1 - else -> throw IllegalArgumentException("Invalid type") - } - } - } - } - } - - private fun bindItemView(holder: ItemHolder, current: ListProxy.ListProxyItem, position: Int) { - if (position == current.header.nowIndex) { - holder.card.setCardBackgroundColor(context.getColor(R.color.colorAccent)) - holder.name.setTextColor(Color.WHITE) - holder.type.setTextColor(Color.WHITE - 0x22222222) - holder.delay.setTextColor(Color.WHITE - 0x11111111) - } else { - holder.card.setCardBackgroundColor(Color.WHITE) - holder.name.setTextColor(Color.BLACK) - holder.type.setTextColor(Color.LTGRAY) - holder.delay.setTextColor(Color.DKGRAY) - } - - if (current.header.type == Proxy.Type.SELECT) { - holder.card.isFocusable = true - holder.card.isClickable = true - holder.card.setOnClickListener { - val element = (elements[position] as ListProxy.ListProxyItem) - - val old = element.header.nowIndex - element.header.nowIndex = position - - notifyItemChanged(old) - notifyItemChanged(position) - - onSelect(element.header.name, element.name) - } - } else { - holder.card.setOnClickListener(null) - holder.card.isFocusable = false - holder.card.isClickable = false - } - - holder.name.text = current.name - holder.type.text = current.type - holder.delay.text = - when { - current.header.urlTest && current.delay < 0 -> "..." - current.delay < 0 -> "N/A" - current.delay > 0 -> { - current.delay.toString() - } - else -> { - if (current.header.type != Proxy.Type.SELECT) - "N/A" - else - "" - } - } - } - - private fun bindHeaderView(holder: HeaderHolder, current: ListProxy.ListProxyHeader) { - holder.name.text = current.name - holder.test.visibility = - if (current.type == Proxy.Type.SELECT) View.VISIBLE else View.GONE - holder.test.setOnClickListener { - if (current.urlTest) - return@setOnClickListener - - val indexed = elements.withIndex() - .filter { it.value is ListProxy.ListProxyHeader } - .filterIsInstance>() - - val headerIndex = indexed.indexOfFirst { - it.value === current - }.takeIf { it >= 0 } ?: return@setOnClickListener - val header = indexed[headerIndex] - - indexed[headerIndex].value.urlTest = true - - val position = header.index - val size = (indexed.getOrNull(headerIndex + 1)?.index ?: elements.size) - header.index - - notifyItemRangeChanged(position, size) - onUrlTest(position, size) - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/model/ClashProfile.kt b/app/src/main/java/com/github/kr328/clash/model/ClashProfile.kt deleted file mode 100644 index 3aee724366..0000000000 --- a/app/src/main/java/com/github/kr328/clash/model/ClashProfile.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.kr328.clash.model - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ClashProfile( - @SerialName("Proxy") val proxies: List, - @SerialName("Proxy Group") val proxyGroups: List, - @SerialName("Rule") val rules: List -) \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/model/ClashProxy.kt b/app/src/main/java/com/github/kr328/clash/model/ClashProxy.kt deleted file mode 100644 index 7ab781bcb3..0000000000 --- a/app/src/main/java/com/github/kr328/clash/model/ClashProxy.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.kr328.clash.model - -import kotlinx.serialization.Serializable - -@Serializable -data class ClashProxy( - private val type: String, - private val name: String -) \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/model/ClashProxyGroup.kt b/app/src/main/java/com/github/kr328/clash/model/ClashProxyGroup.kt deleted file mode 100644 index 75bcd37c5b..0000000000 --- a/app/src/main/java/com/github/kr328/clash/model/ClashProxyGroup.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.kr328.clash.model - -import kotlinx.serialization.Serializable - -@Serializable -data class ClashProxyGroup( - private val type: String, - private val name: String -) \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt b/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt deleted file mode 100644 index 3b6665d39c..0000000000 --- a/app/src/main/java/com/github/kr328/clash/model/ClashRule.kt +++ /dev/null @@ -1,106 +0,0 @@ -package com.github.kr328.clash.model - -import com.charleskorn.kaml.YamlException -import kotlinx.serialization.* -import kotlinx.serialization.internal.StringDescriptor - -@Serializable(ClashRule.Serializer::class) -data class ClashRule( - val matcher: Matcher, - val pattern: String, - val target: String, - val extras: List -) { - enum class Matcher { - DOMAIN_SUFFIX, - DOMAIN_KEYWORD, - DOMAIN, - IP_CIDR, - IP_CIDR6, - SRC_IP_CIDR, - GEOIP, - DST_PORT, - SRC_PORT, - MATCH; - - override fun toString(): String { - return when (this) { - DOMAIN_SUFFIX -> "DOMAIN-SUFFIX" - DOMAIN_KEYWORD -> "DOMAIN-KEYWORD" - DOMAIN -> "DOMAIN" - IP_CIDR -> "IP-CIDR" - IP_CIDR6 -> "IP-CIDR6" - SRC_IP_CIDR -> "SRC-IP-CIDR" - GEOIP -> "GEOIP" - DST_PORT -> "DST-PORT" - SRC_PORT -> "SRC-PORT" - MATCH -> "MATCH" - } - } - - companion object { - fun fromString(s: String): Matcher { - return when (s) { - "DOMAIN-SUFFIX" -> DOMAIN_SUFFIX - "DOMAIN-KEYWORD" -> DOMAIN_KEYWORD - "DOMAIN" -> DOMAIN - "IP-CIDR" -> IP_CIDR - "IP-CIDR6" -> IP_CIDR6 - "SRC-IP-CIDR" -> SRC_IP_CIDR - "GEOIP" -> GEOIP - "DST-PORT" -> DST_PORT - "SRC-PORT" -> SRC_PORT - "MATCH" -> MATCH - // Deprecated - "SOURCE-IP-CIDR" -> SRC_IP_CIDR - "FINAL" -> MATCH - else -> throw YamlException("Invalid matcher $s", 0, 0) - } - } - } - } - - class Serializer : KSerializer { - override val descriptor: SerialDescriptor - get() = StringDescriptor.withName("rule") - - override fun deserialize(decoder: Decoder): ClashRule { - val rule = decoder.decodeString().split(",") - - return when (rule.size) { - 0, 1 -> { - throw YamlException("Invalid rule $rule", 0, 0) - } - 2 -> { - if (Matcher.fromString(rule[0]) != Matcher.MATCH) - throw YamlException("Invalid rule $rule", 0, 0) - - ClashRule(Matcher.MATCH, "", rule[1], emptyList()) - } - 3 -> { - ClashRule(Matcher.fromString(rule[0]), rule[1], rule[2], emptyList()) - } - else -> { - ClashRule( - Matcher.fromString(rule[0]), - rule[1], - rule[2], - rule.subList(3, rule.size) - ) - } - } - } - - override fun serialize(encoder: Encoder, obj: ClashRule) { - if (obj.matcher == Matcher.MATCH) { - encoder.encodeString("${obj.matcher},${obj.target}") - } else { - encoder.encodeString( - "${obj.matcher},${obj.pattern},${obj.target},${obj.extras.joinToString( - "," - )}" - ) - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt b/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt deleted file mode 100644 index 116dd5b282..0000000000 --- a/app/src/main/java/com/github/kr328/clash/model/ListProxy.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.kr328.clash.model - -import com.github.kr328.clash.core.model.Proxy - -interface ListProxy { - data class ListProxyHeader( - val name: String, - val type: Proxy.Type, - val now: String, - var nowIndex: Int, - var urlTest: Boolean = false - ) : - ListProxy - - data class ListProxyItem( - val name: String, - val type: String, - var delay: Long, - val header: ListProxyHeader - ) : ListProxy -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt index d55d776b00..1217896524 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt @@ -1,7 +1,19 @@ package com.github.kr328.clash.remote -suspend fun withClash(block: suspend (ClashClient) -> Unit) { +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.async +import kotlinx.coroutines.launch + +suspend fun withClash(block: suspend ClashClient.() -> T): T? { + val client = ClashClient.instance ?: return null + + return client.block() +} + +fun CoroutineScope.launchClash(block: suspend ClashClient.() -> Unit) { val client = ClashClient.instance ?: return - block(client) + launch { + client.block() + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Channels.kt b/app/src/main/java/com/github/kr328/clash/remote/Channels.kt index 8b38289089..fea3f0818e 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Channels.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Channels.kt @@ -1,33 +1,34 @@ package com.github.kr328.clash.remote +import android.os.RemoteException import com.github.kr328.clash.core.event.BandwidthEvent import com.github.kr328.clash.core.event.LogEvent +import com.github.kr328.clash.service.ipc.IStreamCallback +import com.github.kr328.clash.service.ipc.ParcelableContainer import com.github.kr328.clash.service.ipc.ParcelablePipe import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.Channel +import java.lang.Exception -class LogChannel(private val pipe: ParcelablePipe): Channel by Channel(Channel.CONFLATED) { - init { - pipe.onReceive { - offer(it as LogEvent) - } - } +class LogChannel: Channel by Channel(Channel.CONFLATED) { + fun createCallback(): IStreamCallback { + return object: IStreamCallback.Stub() { + override fun complete() { + close() + } - override fun cancel(cause: CancellationException?) { - pipe.close() - close(cause) - } -} + override fun completeExceptionally(reason: String?) { + close(RemoteException(reason)) + } -class BandwidthChannel(private val pipe: ParcelablePipe): Channel by Channel(Channel.CONFLATED) { - init { - pipe.onReceive { - offer(it as BandwidthEvent) + override fun send(data: ParcelableContainer?) { + try { + offer(data!!.data!! as LogEvent) + } + catch (e: Exception) { + throw RemoteException(e.message) + } + } } } - - override fun cancel(cause: CancellationException?) { - pipe.close() - close(cause) - } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt index 44496bcf91..c70a118392 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -3,6 +3,7 @@ package com.github.kr328.clash.remote import android.app.Application import android.content.* import android.os.IBinder +import android.os.RemoteException import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner @@ -13,6 +14,8 @@ import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.ProxyGroup import com.github.kr328.clash.service.ClashManagerService import com.github.kr328.clash.service.IClashManager +import com.github.kr328.clash.service.ipc.IStreamCallback +import com.github.kr328.clash.service.ipc.ParcelableContainer import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel @@ -25,6 +28,7 @@ class ClashClient(val service: IClashManager) { private val connection = object : ServiceConnection { override fun onServiceDisconnected(name: ComponentName?) { + instance?.close() instance = null } @@ -58,12 +62,15 @@ class ClashClient(val service: IClashManager) { suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) { CompletableDeferred().apply { - service.startHealthCheck(group).whenComplete { _, u -> - if (u != null) - completeExceptionally(u) - else - complete(Unit) - } + service.startHealthCheck(group, object: IStreamCallback.Default() { + override fun complete() { + this@apply.complete(Unit) + } + + override fun completeExceptionally(reason: String?) { + this@apply.completeExceptionally(RemoteException(reason)) + } + }) } }.await() @@ -76,16 +83,14 @@ class ClashClient(val service: IClashManager) { } suspend fun openLogChannel(): ReceiveChannel = withContext(Dispatchers.IO) { - LogChannel(service.openLogEvent()).also { - openedChannel.add(it) + LogChannel().apply { + service.openLogEvent(createCallback()) } } - suspend fun openBandwidthChannel(): ReceiveChannel = - withContext(Dispatchers.IO) { - BandwidthChannel(service.openBandwidthEvent()).also { - openedChannel.add(it) - } + suspend fun queryBandwidth(): Long = + withContext(Dispatchers.IO){ + service.queryBandwidth() } fun close() { diff --git a/app/src/main/java/com/github/kr328/clash/utils/FileUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/FileUtils.kt deleted file mode 100644 index 3585b113d1..0000000000 --- a/app/src/main/java/com/github/kr328/clash/utils/FileUtils.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.kr328.clash.utils - -import java.io.File -import java.security.SecureRandom -import kotlin.math.absoluteValue - -object FileUtils { - private val random = SecureRandom() - - fun generateRandomFile(dir: File, suffix: String = ""): File { - dir.mkdirs() - - var file: File - - do { - file = dir.resolve(random.nextLong().absoluteValue.toString() + suffix) - } while (file.exists()) - - return file - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt deleted file mode 100644 index fdfb403fb5..0000000000 --- a/app/src/main/java/com/github/kr328/clash/utils/ServiceUtils.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.kr328.clash.utils - -import android.content.Context -import android.content.Intent -import android.net.VpnService -import android.os.Build -import com.github.kr328.clash.ClashStartService -import com.github.kr328.clash.MainApplication -import com.github.kr328.clash.service.ClashService -import com.github.kr328.clash.service.TunService - -object ServiceUtils { - fun startProxyService(context: Context): Intent? { - context.getSharedPreferences("application", Context.MODE_PRIVATE).apply { - when (getString( - MainApplication.KEY_PROXY_MODE, - MainApplication.PROXY_MODE_VPN - )) { - MainApplication.PROXY_MODE_VPN -> { - val prepare = VpnService.prepare(context) - - if (prepare != null) - return prepare - - context.startService(Intent(context, TunService::class.java)) - } - MainApplication.PROXY_MODE_PROXY_ONLY -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - context.startForegroundService(Intent(context, ClashService::class.java)) - } else { - context.startService(Intent(context, ClashService::class.java)) - } - } - } - - return null - } - } - - fun startStarterService(context: Context) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - context.startForegroundService(Intent(context, ClashStartService::class.java)) - else - context.startService(Intent(context, ClashStartService::class.java)) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/view/FatItem.kt b/app/src/main/java/com/github/kr328/clash/view/FatItem.kt deleted file mode 100644 index 55127eefe1..0000000000 --- a/app/src/main/java/com/github/kr328/clash/view/FatItem.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.github.kr328.clash.view - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.FrameLayout -import android.widget.TextView -import com.github.kr328.clash.R - -class FatItem @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyleAttr: Int = 0, - defStyleRes: Int = 0 -) : - FrameLayout(context, attributeSet, defStyleAttr, defStyleRes) { - private val titleView: TextView - private val summaryView: TextView - private val operationView: View - private val iconView: View - private val clickable: View - private val operationClickable: View - - var icon: Drawable? - get() = iconView.background - set(value) { - iconView.background = value - } - var title: CharSequence? - get() = titleView.text - set(value) { - titleView.text = value - } - var summary: CharSequence? - get() = summaryView.text - set(value) { - summaryView.text = value - if (value?.isNotBlank() != true) - summaryView.visibility = View.GONE - else - summaryView.visibility = View.VISIBLE - } - var operation: Drawable? - get() = operationView.background - set(value) { - operationView.background = value - if (value == null) - operationClickable.visibility = View.GONE - else - operationClickable.visibility = View.VISIBLE - } - - override fun setOnClickListener(l: OnClickListener?) { - clickable.setOnClickListener(l) - } - - override fun setOnLongClickListener(l: OnLongClickListener?) { - clickable.setOnLongClickListener(l) - } - - fun setOperationOnClickListener(l: OnClickListener?) { - operationClickable.setOnClickListener(l) - } - - fun setOperationOnLongClickListener(l: OnLongClickListener?) { - operationClickable.setOnLongClickListener(l) - } - - override fun setClickable(clickable: Boolean) { - this.clickable.isFocusable = clickable - this.clickable.isClickable = clickable - this.operationClickable.isFocusable = clickable - this.operationClickable.isClickable = clickable - } - - override fun isClickable(): Boolean { - return this.clickable.isClickable - } - - init { - with(LayoutInflater.from(context).inflate(R.layout.view_fat_item, this, true)) { - titleView = findViewById(R.id.view_fat_item_title) - summaryView = findViewById(R.id.view_fat_item_summary) - operationView = findViewById(R.id.view_fat_item_operation) - iconView = findViewById(R.id.view_fat_item_icon) - clickable = findViewById(R.id.view_fat_item_clickable) - operationClickable = findViewById(R.id.view_fat_item_operation_clickable) - } - - context.theme.obtainStyledAttributes(attributeSet, R.styleable.FatItem, 0, 0).apply { - try { - icon = getDrawable(R.styleable.FatItem_icon) - title = getString(R.styleable.FatItem_title) - summary = getString(R.styleable.FatItem_summary) - operation = getDrawable(R.styleable.FatItem_operation) - } finally { - recycle() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt b/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt deleted file mode 100644 index 3a09e2a954..0000000000 --- a/app/src/main/java/com/github/kr328/clash/view/MarqueeTextView.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.kr328.clash.view - -import android.content.Context -import android.text.TextUtils -import android.util.AttributeSet -import android.widget.TextView - -class MarqueeTextView @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyleAttr: Int = 0, - defStyleRes: Int = 0 -) : TextView(context, attributeSet, defStyleAttr, defStyleRes) { - init { - ellipsize = TextUtils.TruncateAt.MARQUEE - } - - override fun isFocused(): Boolean { - return true - } -} \ 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 deleted file mode 100644 index 798a4676a1..0000000000 --- a/app/src/main/java/com/github/kr328/clash/view/RadioFatItem.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.github.kr328.clash.view - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.FrameLayout -import android.widget.TextView -import com.github.kr328.clash.R -import com.google.android.material.radiobutton.MaterialRadioButton - -class RadioFatItem @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyleAttr: Int = 0, - defStyleRes: Int = 0 -) : - FrameLayout(context, attributeSet, defStyleAttr, defStyleRes) { - private val titleView: TextView - private val summaryView: TextView - private val operationView: View - private val radioView: MaterialRadioButton - private val clickable: View - private val operationClickable: View - - var isChecked: Boolean - get() = radioView.isChecked - set(value) { - radioView.isChecked = value - } - var title: CharSequence? - get() = titleView.text - set(value) { - titleView.text = value - } - var summary: CharSequence? - get() = summaryView.text - set(value) { - summaryView.text = value - if (value?.isNotBlank() != true) - summaryView.visibility = View.GONE - else - summaryView.visibility = View.VISIBLE - } - var operation: Drawable? - get() = operationView.background - set(value) { - operationView.background = value - if (value == null) - operationClickable.visibility = View.GONE - else - operationClickable.visibility = View.VISIBLE - } - - override fun setOnClickListener(l: OnClickListener?) { - clickable.setOnClickListener(l) - } - - override fun setOnLongClickListener(l: OnLongClickListener?) { - clickable.setOnLongClickListener(l) - } - - fun setOnOperationOnClickListener(l: OnClickListener?) { - operationClickable.setOnClickListener(l) - } - - fun setOnOperationOnLongClickListener(l: OnLongClickListener?) { - 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) - summaryView = findViewById(R.id.view_radio_fat_item_summary) - operationView = findViewById(R.id.view_radio_fat_item_operation) - radioView = findViewById(R.id.view_radio_fat_item_radio) - clickable = findViewById(R.id.view_radio_fat_item_clickable) - operationClickable = findViewById(R.id.view_radio_fat_item_operation_clickable) - } - - context.theme.obtainStyledAttributes(attributeSet, R.styleable.RadioFatItem, 0, 0).apply { - try { - title = getString(R.styleable.FatItem_title) - summary = getString(R.styleable.FatItem_summary) - operation = getDrawable(R.styleable.FatItem_operation) - } finally { - recycle() - } - } - } -} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_about.xml b/app/src/main/res/drawable/ic_about.xml index 0cbb996010..ee2ad3c098 100644 --- a/app/src/main/res/drawable/ic_about.xml +++ b/app/src/main/res/drawable/ic_about.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_boot.xml b/app/src/main/res/drawable/ic_boot.xml index b179b42706..9380e2fd89 100644 --- a/app/src/main/res/drawable/ic_boot.xml +++ b/app/src/main/res/drawable/ic_boot.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_cloud.xml b/app/src/main/res/drawable/ic_cloud.xml index ef9691dfda..db16c9da0e 100644 --- a/app/src/main/res/drawable/ic_cloud.xml +++ b/app/src/main/res/drawable/ic_cloud.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_feedback.xml b/app/src/main/res/drawable/ic_feedback.xml index e2ac499a77..15e9690592 100644 --- a/app/src/main/res/drawable/ic_feedback.xml +++ b/app/src/main/res/drawable/ic_feedback.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000000..2c8f2afa7b --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_logo.png b/app/src/main/res/drawable/ic_logo.png deleted file mode 100644 index 9efd87e5869fa120c2b30b450e9faad2a9fee6a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4001 zcmV;S4_@$zP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x00(qQO+^Re3lHq)$0dI0nQ~}STcbWhI4wy+q zK~#9!?OcC!RmGkE%zZzTKoCfP1QLEE5G)8NwGbB-J*};^)}z#FyTx`rd$69}p4PTn zJ?^e-x9hrfk5;=_XlrdNJ@jBL{b5V(R$PlLwiJ*+ARsY>M3R_<{Cq#|+?o0AAMf7# z?!E74lJ^Kb$H|#@--^Mq6HdU=w4E@}<>^#Z>zdu}%>-C`ES-$J853Haxm?vH-gA z3r~ke1a?WVJEf%0-1F%6Ib)$O8c2O3{8sBb0X0&x-(7y^e^yThNN2kAe-(I6Q03t$!8`n-cNDn;o z+se2h8VCf7IRFq+UDMRm`cD@MpxZa^3`XMmw+R#=#}gAmHWE?ZV=oFw$4cJHo9$#}IKst52?QTQJ)z)u|hnQ|G-n?~J#iW8XJQBUqoZAWJ$g%Di z1)_$&?Zz)W9V#Y3H{JWp^wGHSFHSzJt5Ou&*VMgY(m*027UO(%f+3SZax*Fwtvu5i z`BE`KI@>#RpHfuiV9fdgfG|7~Tb*xT1qbOM;E!{$JPC}`97zJD8^*(HKlkK%yk0dV$gz%Xlh^o=wrnIsiXh=eM+h6j+-T~ zCO=$=6-4CAr$2w889Z>Jqd`a&VvM;!aWkAK7gFB7;?8ewY5M5{djX*S#-~HRK>ti( z_!hz^W`VRyB37nI1_|WTG#}T%bTBm}B=rXTT8{>t4TLe={K94q03ZI+wyL*UyEhRL z7(;R*yOtXe*ZKVacy9L$0KFF+q=BK~6(CdLGV_DT_J0Vjr>c#PM4w*x(MR?GSR<)_ zsW4~;AQ_}JLIS|Rz(9&1O?n1E2n7Jr7)Aerk3Mpm0dKaw-BYE6ng+%a3WM2HTFB%)5J%>ED+5i;3{s&)TX4|_oc+g zz))l*fY)=~sh~26h%}vx_faOzmT2TDOXp`&hH__J4*xWbR!w9DfIvjhg^`yTP|%sQ ze}894=e#b#1Y9zKEJj^ZESDpmx1*k!hEji64^bFuIT5j35rKk&^!o#ER&qnymaN50 zLWrj_nJ!tYg0b9D9Lq?}q$8|4Z+k20I$sJP0Dxz^PeGU4k+#lyB~`#}n*ez&4|^c_ z0h9oM*)+NPb?{;?JwwQ8_x$sZYI4yQbS9l2j4X8tZNO4)PIi%fsWHb)V|ml0wzB|K zPNZDl3FLdLt9L1Y)(ZwwJkA?Z>=PF_>AvUNZmcwvp;@w?h!BbJXt;uDx2UeyCu2rlLo#t~T)D^v5CE)RHXj>4v^pbAYRY7!Ca!niw$26!NEn=V z-?|RXX?GMk4uq z8WvQ~G!08He*{CyOD$1RG>`!Jxo#}TWg{JXt0S!(KX$4khmE81wuO$K>P+dM8v-qD z=TgA2cr^^89$7?BK|p%;h5b`?Zp?BTYA&$MeCT*Po_+B(7f>(1ehj~S<#0;d@#g1# zz843Mwb}hkrSSL}0vMELqM zzr-(IIh4?DoxRw9w&3dbH(*Z1G#otEhEwf5Zh`HILJ*PvU~_9d zfUcr})bUPlU1Ci$)53NHkvuD%h;XE(9Y*Tc7_nx^G0(HV)JTR;X{KX z4FL8Q4Wxm=$RbPqdm{jOo80;OvKU9Z6gtN&GM8N3;0>9Y7epq-d2FF26rF#Ca!h?1 z!{=_j8h)QvAV)WqN5c>qL7MPPA|kfx;}6!kmY7vH0q1tcv@%?`W*IuV`thlcH6pGX zcP@UZ$NtkkD|lo`^KYnDRC+#B$Sg&gYZ8ZDcZFhiu1} z28W}F#Nt@dP=oL7d=4xB7!0KlA?e#5{oaihtNO+w|${=)U z5@?cvL1$cv&uePx346ym(V3)iZp=+_Nr+slF2%P@54h)dv1!O58WKPWAwy68czX?i zmV$v~+DLP3L(;j%{widXTf-3%Ywze^G#-#LisR>hwtt2YqTIQ@6JTW^v8DT1#zEdw z4-@ahSq}H?8ji$LY^AgytzW<1e=<7vW>WYJh^+*uKj33u?fCO{S193ld*^H+WXJ}E z2n3d0XphH$^3oKd4-Yt28(r|+AkA;Q>++Tnxs`}MV0XY!S6MOR{&CMHrZ~&5T(P+G{(C<0-`=g@p}~R6kon1bmr*7fr=glpz4zFmm&Wy*990A%j78pPfDO{TwHt5KIp2=Np(X&pl+v)g zYs2-w{qo;`^1&%nf+xIV`M9=`l$}YbWWBe{-YQZ#X>#T9(74;%QA%y^f#wCyv1*@xrZKNc4Ds!DZqb@v+&3hu43nD`Q(69?6|KY$4 zxP8s4OWTnn{bBdvnNrHq#FJxVH1NhHJc>+XxhVu`hy^Z?G^WiWYoGwQVF&;+SC_44 z&sGQ_1I%GaCT>x**LsVTQsdpiFlt>Og+l?@4l00nEH>}Io_{_JAd;=?zTS~}fP(ie zP3KXYbeb!D8OA|l4Q#wL$+DDqw-hx_TS+UQ67F_Nc%2*NKm5_Yt8?K$G&)q38gE91 zYaRl3%ruxFP39~NQ4PR;%}ZOhWMLCLK1_dK@5AfYZ_euca$U|%ka$#a#-lTYu^pwM zJlkbDqCW=rF>g={>uIz2HKoCb2?TntNUNCy`zNqhvvRF z|EGG@C$55LaT5_EC95cDfF&9B9^}fWE?qgsNkbE$ag4w1Oqcy%DEy2u`Nm(GA3gC_ z`?c@(4VHSqSkF1%TeIMRr@7l-4Ny*9>J=MA zgtF4`AG=!*{RuD~T*jCpqHijpDjwVNlY56pBEEF!16 zNc(MYm-b?e#fj*NYu8@(`yIRXd~x@l{U3YdU~}bYB&KEV*wsU#?4o+1BwxJT1wPGZ z{|Af|6>ml#j}p;ACs&{d~SpM78d`N27nXnZOc(%;??e6kxdTTmO!Dd~amLvPAZTVy2(j$I;S<+2wYAp{H|87Zd=V=`?=HSLe& zS*NsKhWmjqjUnLoQ^4;>AmFE9FhIe8pF*J^g@OSU4h3a66qKbUVWXrZ#7j#;(X!I; z=+v^(q3M^D_spxF)v|II9sp`4cmV(*0Hpv*%VF00000NkvXX Hu0mjfBNk`J diff --git a/app/src/main/res/drawable/ic_logo.xml b/app/src/main/res/drawable/ic_logo.xml new file mode 100644 index 0000000000..c6fe63f2d5 --- /dev/null +++ b/app/src/main/res/drawable/ic_logo.xml @@ -0,0 +1,4 @@ + + + diff --git a/app/src/main/res/drawable/ic_logs.xml b/app/src/main/res/drawable/ic_logs.xml index 1681cc0419..347a2721ee 100644 --- a/app/src/main/res/drawable/ic_logs.xml +++ b/app/src/main/res/drawable/ic_logs.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_profiles.xml b/app/src/main/res/drawable/ic_profiles.xml index 623aa9fad1..2b3ba7d80b 100644 --- a/app/src/main/res/drawable/ic_profiles.xml +++ b/app/src/main/res/drawable/ic_profiles.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_proxies.xml b/app/src/main/res/drawable/ic_proxies.xml index b8c4ab12e2..ea0365837d 100644 --- a/app/src/main/res/drawable/ic_proxies.xml +++ b/app/src/main/res/drawable/ic_proxies.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml index a671514fcf..6b463aa36e 100644 --- a/app/src/main/res/drawable/ic_settings.xml +++ b/app/src/main/res/drawable/ic_settings.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_settings_color.xml b/app/src/main/res/drawable/ic_settings_color.xml index b11ad710a8..9dfdbc9dd4 100644 --- a/app/src/main/res/drawable/ic_settings_color.xml +++ b/app/src/main/res/drawable/ic_settings_color.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_clash_started.xml b/app/src/main/res/drawable/ic_started.xml similarity index 88% rename from app/src/main/res/drawable/ic_clash_started.xml rename to app/src/main/res/drawable/ic_started.xml index 5ab22cc67b..27ec52c36b 100644 --- a/app/src/main/res/drawable/ic_clash_started.xml +++ b/app/src/main/res/drawable/ic_started.xml @@ -5,6 +5,6 @@ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/app/src/main/res/drawable/ic_clash_stopped.xml b/app/src/main/res/drawable/ic_stopped.xml similarity index 91% rename from app/src/main/res/drawable/ic_clash_stopped.xml rename to app/src/main/res/drawable/ic_stopped.xml index 554d6df98a..2de0b0b730 100644 --- a/app/src/main/res/drawable/ic_clash_stopped.xml +++ b/app/src/main/res/drawable/ic_stopped.xml @@ -5,6 +5,6 @@ android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 4070bf4312..1c847de74c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,147 +1,196 @@ - - - - - - - - - - - - - - + android:layout_height="match_parent"> - - - - + + + + + + + - - - - - - - + + - - - - - - - + + - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + android:background="?android:attr/selectableItemBackground" + android:focusable="true" + android:clickable="true" + android:orientation="horizontal"> + + + + + - + diff --git a/app/src/main/res/layout/activity_main_clash_status.xml b/app/src/main/res/layout/activity_main_clash_status.xml deleted file mode 100644 index 6155c4011f..0000000000 --- a/app/src/main/res/layout/activity_main_clash_status.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_profiles.xml b/app/src/main/res/layout/activity_main_profiles.xml deleted file mode 100644 index faa5dc1ddb..0000000000 --- a/app/src/main/res/layout/activity_main_profiles.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main_proxy_manage.xml b/app/src/main/res/layout/activity_main_proxy_manage.xml deleted file mode 100644 index f4cf800cf0..0000000000 --- a/app/src/main/res/layout/activity_main_proxy_manage.xml +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_form_text.xml b/app/src/main/res/layout/adapter_form_text.xml index cfaab8e373..33bd3e152f 100644 --- a/app/src/main/res/layout/adapter_form_text.xml +++ b/app/src/main/res/layout/adapter_form_text.xml @@ -33,7 +33,7 @@ android:paddingEnd="5dp" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textColor="@color/colorAccent" + android:textColor="@color/clashBlue" android:textAppearance="@style/TextAppearance.AppCompat.Small" /> + android:background="@color/clashBlue" /> diff --git a/app/src/main/res/layout/adapter_log.xml b/app/src/main/res/layout/adapter_log.xml index 1244128066..fa1e7594d4 100644 --- a/app/src/main/res/layout/adapter_log.xml +++ b/app/src/main/res/layout/adapter_log.xml @@ -4,7 +4,7 @@ android:layout_height="wrap_content" android:padding="10dp"> - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index c9ad5f98f1..7353dbd1fd 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index eb433d4cf10f7af3ea4c846491bc953b3f013a32..603514f78158c755ffd162d206c4d2bd8ce4172b 100644 GIT binary patch delta 1785 zcmV{p4j3Rpug*y;KgylFlx34E-P>L$X453J%3DCx(#NmN`#g+oiG3$JzGQp zq2{gG3$5CFjBRaAIRk?xgizR3wJQDt!uev;o@@&sbS^1fPG5J{*3}!g;kAGb)Xu>1 zA&^sWh5vx`_l>YHWWNOvYV`alD8E+klpTZ71dGE`tn9>-XyQE}oF_)lkG66QwOn)< zv^qVFB!B!&?X6}KA&sD{ZiH(!x1p}F8CobBp$8#Md>s51Y_YQu z`1MXYY4aKoe)G1D^_D(Z7y~s8YJLNXJ8+scKz}}g8=>S%Eq?)F_Pi=e!Wy7~indT6jAq2{nWiJJpTaQ(;HyH+J9uHS0$W?huBQS|ECQJnfwEUOZ?#wCHp$GY+w2$j;Ve*jC{cpz^AEcx6f$ zt4U(lm8|+^6Tbk-NUq}wIH*^rt%v;LYy1L~cCx@_fUw_mM0$~5fMO3wT?PmhlYcDb zE}+&n9WYI1q zy>6Q9Yzo`2jdxnP0jRn4E)8isFn`hREV~0Jt*nFL)5Bc~2*0S(%35v!s;q8=k)$-h zO1itA@SjCj?XF?5t(PXPgENlzr`ZKmQPn^r1P>}Jx76+cj(5`saz!pAIeCE_fa-7m zW9cH0oidv`S;6!w=m0mw*z1jnaa8df{gazLfl(96X5e&pVheSZSTiM9v( zs~EO9&DA=tW)qiggVoW$Ky=b6tCpxM^;JYFcK~5;`0TlIdOh9Trs!+UT)m5Xr3Utw zWA{tp^X&z7VI2Rcy0jvW9ukq0Dm-k(dSF$Nfg@vL)+NXis_0 zw|gL9#ZH*?<#u{nzW-?q_kc0gNT{E~RO->lWUsjcM^haq@t21M;UMX#{R3M={5 zv^U?4imM!!A$SwieFMr!JAf#J>Zr{7;QnX}7YnrCA%xq2-SI5shkrhjsnu#hB9W{> zUyF;2`#Jki?_)&I)zs9KVkn|P1SJHaFX&TANl6*;@dfnN=^q+MPftfc{R0C7$0!tv zvewpCFdB`3hWRW!tuz`96crUI{QdpMp^qYw$lHU-7&~_C<7D$Z(MYja943`YC3$&y z$8vLXGvspl(XNuoWO7!2+y3w9XV|dWxy`_1M<37^^a*|Q^YeS6CqKU6>FewJB-uVo zG?d7PXgJU1)9K4IBSwsP3VrPHZ(I;a{YW(RC))wU=+k@_`q1f1KMxj$r$FQ$?%^Kp b!He-PN_ojpYY`3?00000NkvXXu0mjfCk$%l delta 2211 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 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png deleted file mode 100644 index 96e652276fb87ec4919ac55cc29c8137b149885a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3901 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 UYHH2HzW+l-)qMKqe-^TjtpET3 diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png index 8b6d1a13d6a271552d9384fb3171e77f974b478a..d320f60f9f0d55f7aa9c1c2c2b571d748289b7be 100644 GIT binary patch literal 3657 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 bFoXMFldAg=??j5900000NkvXXu0mjf-W>nw literal 4290 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 delta 1455 zcmV;g1yK6Q3BU`GBYy>lNkl@c?Y+xvhB_$=VXf&Eyt${Ya zuRPPBHK00jD=5%h<_w|biVilX5ds#F@$p^80Wm6ko+sm=d8yJ-vl%?HuGm-_n|W|e`gKMoz~)I z7YSU0lVR%>$F+4~;kFSAR&K+jMVqJ{feFrQQKeAvXCN}^5X@$aiEu0q`34P5&G_kT z5t#~yIUVC-2g1Mn9?i6LCZ}a`eavQwalYsVe}4vi!nZ@<7)nCIF!O^roV$D-3A>MT zZC$(41qKwmmdRc37sX1g}`AI30bWrf>gI5useY^LY;H z8@nT&9|LzQ6|h|_f$7X(m^!Q=R{jGkw+MKJCc`^yE38~2o;uI(4-A z(?bMABw_l(FqGY^esbI^_zR4-S&A`lyx%X_7;#&r`ZI8^QjV#vDwARlsr4YK)w?fJ0#8fl-$3RCniMe_&(Sx&(5Ms$Q4T8f6_)_8x;@ z=w`etS_dl!T1BkfF~VZLZeH(%&VL+Nm|8D{i?^7&vjg%WW4~J-G&G`0uEd@4N?g5m z6Zw})keyeE(`WN>^4C24n3;==Q#nL7GJekC+9!UIA@j_6Wc_v#vVvk17MJ4s%`)7r zsG^Ibf~K`~@SnN=@1MIenasja@4&u&`zCc~fZH@UeE6`SrKLr|IvL6a2Y+dUQmL$o zkB^_gG-hRG4ef4=>4nnL(t~7z^#e1)S81PHtybg0g$wDVZCG#i$c*gm?JWxm3XaO< za%F98Z5_E+u{e;l!-lzDl1$L#Fv*yCf=+nvf`gwWa9*~#DE-(4gU@dBHB4i%=Q z$E)Ajgr}$H^Hd#9yu>rZnJAV%ZM?IWg`U0V_@v7hcCZ*o`3v2~!3Y!aygmQ`002ov JPDHLkV1iEhy;J}I diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png deleted file mode 100644 index 5ccf4a7b78e3510c6071d0c47fe1e342035991cc..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2356 zcma);=RX^Y0)}y7l;&DpW>u-#xM{=)p;}3e8Z9bn#EcrTYQ@SyTSc`>?V$FtHAl25 zMQE)WA!4Q0iCQITt*RKe_b<5j_j}&={q}r*ldUap0R<%mxwyE1rY6RBf3N?4Bf$5& zUtsKqxVQv3rp89Lku#eG(bl#r5*dVy zy6OJuevNaCDGDBmXy0z1*B{Z10x=VK1rp@`ALd=!=x|(v$}GDT?7UTV2i?E|UfOMZ z{Iz}kQNV~O7ix8A5h(cFLdGEmOL~NLsV& z&(Q0AV4?aVY!*-ZwulRj{TBeU$4*z=1W;@xWvAno`a==J6#;ifNBV1P2f0%7> zRk~d`ktq;rA8dGA{nGz|6RPe_gQ?8qtFwvog}d|$7ARe@WjhR4J>!N5XTyFwPkQ=j zu)sT)r>THZtew=mics5;N)T`cT78t+`>lmS++l@>#$`)Wi}9lf`a!2|Nt$qSCImd8 zTXNR(RDGFwCd0i@{C-`97c3OtY-(chmv&Fn|YcjES5#m(pGfJ>n z&uuSIXa916tjC4adG43Gt=*$X(_i{4nhRg{VEIAKl`0naYrgK?rAIuu&+Au@)mb9& zjQkVzG#c>=5o2I6DqGjQP&oEpSUmUA&4ZT+qRuA1&|Qk6a!=^VLQw|6B(}l+a^zq6 z$bZL6bd9re^gj9jQAGk0A|Xmzlc^G0&J{1*KByM%p;JRUJ|)?GeuCf}RB4@e6NGe{ z)4%&ADFgjvt0eEO<;OAyE0={hAAP^?{|bf*hD&P!5MM>?c*{??D&>x|kI5Cmq+*!y zJsu!7CWubO@2GR^XIeX2`HsKk_il<`#+9C%lOXe;(nV=_0`HuURcNEA_&X>*Z z&z%OP1n9^GLS(;tjd#p5YU*BCih2irq0lf!m?mO`!qj#d_^UOQ{GcRfl{WF8DTrPe zf?T}eabhJ$F28t~LHfr}0B(A+f3>v@3UcYrPP&Bf~ z?szbOer`gu)!HLyyUe{oV~s897$-mzp^W7c&BJR29cZU1LphI%$~(+M*8=~YwpL6j z%0Zl&cOiyAg07u_0eY;sj`?i@ehb}YJJ@;VXFoP)g_3c$;7_46;pf`Hhb?RSnYrEUvJc6t zF!yV3Iqs?e&RH@m<8_@G6p_e8S zpSz!%=JURI#SNYgz{DS=q%sd_s6OJGG(c8uNC z7U019+li%HsTxOC?6_E9Zp>^}um9#NfLEei8E#wYl&;&OW0q(^lKSpmV2N|ku#f5W zD<(Wz*T)aA+Hujpn6G<_DDrQmGXWT4d`4}$cA%=4)!+D*heYzRrwpj%SN5-oxoGU2 z+_aOZ*IQ{Ve(7db{!qO}yPr6lV_zO<=jZdy%Va?XpGm7kw!eY^~u+V|qh zl6Pe2e1c5fn663Fe(YN9mZ6g6gDFN?xFo{e_Mub!-mw*vXgpXy^dE&a~^;J(2 zNP&{pI7GmV&NQLH=mY7a;So15TPR03VD8hOO?-Z6sBv{|;#E+SE26(?ml$^4ORDqs z0Xk~jYjbWx+5okfLaKJGf<~@)N%lqQmu-FitxY}OXtpGZL*IW%jZAa+RbE7N&aMd+{CseUL#}l=@8`MbYS%aR|0tM=-8e)(ZRp40dQ4Me;fP`qG7hh( z+EMc@!DG3}gZ`qOFV9b&5P3^I(By4>mzn`NE zywoS^E6R@6`NIr*aw@T}1=Cx!xLyfwr0Ir&;7A4;Z$0_&ua4M-2N5H4+M$Vt$3ESQ z+9S4yhl7t3gh5AOW)Ir%MMw0{00D<2sZsNNx`dGk;D3`I$aTVxu;@wIE)U24enVWQ M2utIJ8?Fie0S2j%i~s-t diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index 48bb3f22b4ac7182922ebccfc83122f39e5bb8cd..bbb46a9d0f1da5dedb49ea24e4f2e264dd197073 100644 GIT binary patch delta 2309 zcmV+g3HtW%6p#{-BYz0jNklk*cR*QMkxq- zT$(PiP>9N*)+#8tg#c0rDTbgDOqE3l3C55JNfGcw5pS;#h- zWcIJ$?|R?lO_t&NrdjlRUTY^{YmqtvJGY(72uCEfYZD-<;=f%YCciuSs|w_fa~dGXunmKiToQP~$mniCe0P07{N z)bz`MaeqQ>_sqINwP6DS$vW^q0pjN0zf80KzL6RlTXhl0{Gg0(o4s80du`{TKzl-9 z=<4b^%FwPFjP(-J;njaiWjdPP=EV(r1PDczyqZHcyMwH}C(brs6y{+vH`AG>cIxV} z3JulqcbJaSCoW+JxdM16m=fb6*1}@Hrlc_9&wp!q^-U_@thtg8JhjP(z{LBP)1%LC zrH7wMqu)KgfqpZ8J^ku$$u#Y;4RrrMQt2@Ui{Hl2jQ94zbC3%ZC$Oljg}Q=*g6l8Q zg2Je`w>M6;`kLUoEmn$tc$E)<%aLEtOBT*RBX@H1UZ94ChJ`*_P`f6Z%~q(__94}# zMt_?0z=~lLxGQEgl~kFu;)B4{($eyYYA=L&14L_U>qKs3QCyfF0*6XYP}J;%VH1D{ z%P*?Z%$*o3;)12DtSs6a+!VLO_O(!&kM1;K``%)jFmu_k2|(V+EYy>NdtgRJqwyK9 zOI0+`)6=tEkKb3s>2%TRxAyyhczk&}y?>eYG2Q#ubpgR*bHADOiHHNrv!@fYbfp>a zVrH{B6VX|nUW8e#*3w`I^jdB7^x7;h0BGXNhV$g)2~u`^G$i;5_bsE%IR@cQKRw_WC*3m_0e>|x zf7uSPX_K}b^ge?#KwhOq0A}9S)^;j6Ir&B}f#b)I|9}IaJ` znK9tw;qMU})a~%>u0O2iNbKtX8qT%qBGBF4-I9@!G2Tm{s;VlA16W5e1mL%C*Knbe zAAE%tCA~+>Q}gK|1TPgae_}Xd)PF^Q_iJZfUfvyE0>#C}6N4pCcC0=m-bXE4at*o( zWM*bYdkGvoc<^QpVlBZC*jsosOV$!4P;M<^7NonEm8N;T*Kw=-IPpD}NaCwdzDJ zoPBG?Py{T!{Zz*8v9Pp;`fYY@JGm5PdrUX+n|AeGp) zYuDWz3%b?G9E$GeUfnI+<$o@&psccRH?zN<7MBuo0uUsKL#;lNIX3m4l$4ZvWdIIW zr_*COBQ>Z~SCof8{Zk@6vgkje%2Eb9W;{((S=}Q+AU(e%U^;CwnTi?fcYI0#WgnE3 zlsqeoa$!a#l_pWJqfAr}X85d@e*DP4MuY&;3qRmJjM2mC4(;2w4}azp?o(DO<=ND! zQ-8!Ns#D51<5x}e6HX1LarW5WY4HxY-*C)ZkcV2HUAKG4l9Iho{gf$F#``VoC8KW{ z42BgrixC^f?(iTnL+*l&j6*1B=-vGTClR5Rxi7tQ$>vq9{%TwO6!2M?{zSn#^{gu;oug0GH_4tru^Vl1$6;u)dmXz_-E zf`WDMxlo@HKw2(#6UZRv&pJ>}uV)m}x@{lQ%1!?jk$DlD*?*#>YrF|d{w|3+e(?{$^?OYc~x9Q*4pxQTRRKljrvGZf_Gy9+1KX+3oz+^Ya{mqR5hWpvhsk` zgiz^7AFnHBgMT^8%gaAx47UQykan-}f@2IzRB37HZq8N(32Z8OwGntPAYO1_4wwyl zL#+I5z#t;-0TRkguH-#~^@XER1P18mr>tJg)w#c`A}#>PI3*)O)R(1nKS1>PRCbq zGMLL|P=0}&h0DHX4nXY?+&4Bh*0cFlpbgrh5BI%8j&TjfT(f4)75UEu$XqXhVB<}W zjEwx*^y$;5#l*zSg5Vy{@SALNlk5}mjsMxhpGM#*PM$n@wcIC)cJTj)Z%esOK8uj{ f9f3Zfu=D=|*}N2lUv#vD00000NkvXXu0mjfBgT-9 delta 2664 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 UgIu26)h7z*YTehYMLv4@ALO45+5i9m literal 5406 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
  • k^@|8G^aL^*bP$c2!qu z92q8W7QjxT*}iewx{qI3Z%_(*4d}`l&=cse-aQy;I-fN?ZYun&{J~T?RR$n*zqhmh zc;F_%>|fsSV#R6wY~?i`@L0|2K~OgQO}l*hpmLIC5e&+jez!$p^rY5@h4%c7mKq8# z^GfNS`~BWSfY>(q*|*~DdvfPh({;%wclp&DPu=2mGaAZ=O7wC+{`H-7Pzs1Uaed@p zarTBUU*)j9!h%MTjJ!iW8v${BRK&vE0NR~@W4!#;U|ndo>y)v-H^EUbLO8Ya$@p;V z;boKY^FW{BvTAzsUZ>n(G{Dtiq{Ig1@DQdU{W4Q^uQ^C0ba2tvNyN@31sisqW#(yJ z`#vz6T4-V(ImbG-_W83gGsFN)y8mAC;|gY7QsJx(K3t&e-nMr3WB+V2sd>es zu2x>AvH`V!F5`kAdBmIuMG9`D!topWyB@j6m7qEYX3Nk_WtV%Uw|SkJDQ?3fx{e>_g! zRk)D*vbECx#5+%=scqflLul-xFNe1$O?}84K?yhM6b*i3^@bMbniKGr*W}nr>UGuu z`$<0G_irYd08aA99v5s;uZDo=tK^O^y^xx17@hKisLt6P^hf;Yo!e189v;J+JUgPB z7r#Sxq8^i|$ymq+c`vG{qCOH`N+A|10cS!h(2Eow*1v2DWHGPaMCI-K;hvchz1WZ^ zw(t)i7XX{C&R_D!v2jb*j4k!GqzzRbenbI=lmCvLwz$ z)pLgSEu791`_%DoWpkgHvgi4}P*kr=%j)m!a3tTVqF+`ug4Qtf$ zUAov1Sn2SeH=fDSflc3iQz9;59J9^S5N&uF71q4>nuVQ(IHYsH&caME0^u1NZE|4M zn)!7%axdUPR503*b9M3sQu$5ft9OO_{o)r27=ATn-^<_X{L-U}0zq_oIZ9mUcO_X_ z^lZ1f!qEwmoC(U@!(;zkI*#x0)Qbeu5)_4lgrB-?#Q-=V3}JwL6$k@W#Eqe#ipcv< z{?`)*;H+*`QNGv@CrQ+}sqNZT3=9l{X?YpUk%~<<5fKrm(F(%`97neyA|ll?f`S#& zLlPVu9R)qW!~OkI?K^90YtAMX78%2&K{5I7ABed6uMg+8&z753|B^6z`gA?$GY#$! z2H2+hA&!cgI$;zXJ+#Ua9jNLfAi~d&a_EiE<}_~`f>t^(pjuj67mEPda()kEFY))O z9IeqJx_K8DuQ^J<0#(I^}eu5J^MA7@o16f+tT6ow)+hPT8lzZDQXrzB}5GPEyf5#ayIXO9QA)(4ptJ^RG1IAmFHR-yA ze1p=SbD?-VzIl0h`N+jtuo)pZAorPO%IypS4<;{%FbZVWnc3%psDvT>?0EhQyI6Ok>G#27qWB0f;TKY;7P6ehd` z74VH+YrFL5_Q;u=nN2a@-`?IP#IZ`c#g>$;llnv@u%K?XspB0G!2Zm^VJyLMG|pK~ z+}>Q$VR}OPc!P^A@=x7z!fkt#_K?e8IvEn1$7`8h{+y@HqUj7=YM=7baqcb%2?4A* z3D$1M0^w#KbCtPIhsrUVO@ht-NBb@(I#n>cBlD;=fFk^@01qBKR3WXUj)#jAV zpC5Nuh{^X@Kr0x+!S1IY1gSInuWMqj>>eIQ^Dx2dYg1 Cr#;O8 diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png deleted file mode 100644 index 65f76c60a689874ccf34bf1781532e056cc49dbb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 10330 zcmd^F^;;B7xFsY+Qo2!Tkfl2oq!Cz_25~`38i_@6K~lOqq*+8#Kw3Hl=?2NAo27Rz z-@Sjvo#&Zf=6NUHnKSR4Gc#X7S}H^YbOaa}7({BSieL;3OvV2)KJH@)WKX3J1B3I0 znxeek=Y_*%JUxopMcfnZm|ZiauM?EdzHmn3JZYI|VbQN^V{C=WC zq2J;E`P6YSP$819C9q57M{uM;sHV>q(M4MLIy@oZMrB&XEp5P?_TpxT5raIb zV^b(<=7kX20I#6g1ec1@gJ8H0zHc%2!Vk*-v>UGhRRq!7q7l6Vstx8E)?8tv&jpeU ze+EqORaa-zM?}tlFz?JKws6myoks2E{HlKQ{ab;1kmr~o8HvHehHu9GW zeN|d@r@F+hoj|?Nf*e?9uyajR>)SR+*r57~Go$qRH_2M_^tjtpGJ(= z*-yCSc2l*+ZA4(O2fjb5CuR@MBUaSsbhA~gFbA29&A9aG_Mc>&mWT@ZTrETBBtyqc z3U^7g@=h*8Ew3x`0syajKPh5ZLg=msIPK>SMm2Lsp}p<4qs_ zw;+a%jg7tL9j}DX#m$U-=<4nTUZb`VfbfRm;Y(tCoihfK6XO;1h~e_X)%A^(qu)6v zJw%J;Y&61V=gG^aL*2BU`JfVh(<3r5ImLNu?`8mL_IynViQep$mT~G=F$0X~x!7Z3 zv0WP3a|Wp=XE?ZWE;QWb)<9Gq%8nw`uRxiw^9U zEsdhCF~p?VeJ;>sZdn#H=FuBC!;B>~mnP5^&u6;21iNE@?J3yT>VdOYiiBAnrT! zGZq93r=rg6Q5i^MF?DW zr+-OgM`GUe5Ad>b!92@@a9w|c<6qa!^yyre@+#v9`%>GPg&4Bdlv&zT6 zT3;N;J3ph+&@(FLl~!+v6rexO20#BCH+Y=6C}TP}G$4AM;Ci>%$8+a3F$y)!%VvYh z&Rd$?jqM!nB*AY0-l0~|Id&E_=l#cqjpE$;9!1J zEBcDWr1>qJEcr=U2YT>{z|%3e!-tg9iy-8w>3Jy2z5vf6z6L~s(3d8VtVvY|a2b(x zo}v|a5)x#@ViJY(62|g1#86)!y0t+0RM9TZxM^AT74>ML+(bwl;crz=v{ltfkrvPu zJE$)DCo*>w#^_uYfvZbA#tU+U8qSkW&Bp$Y!h~?1nU%B?6%u`ypDY_qP-@u)Ecd?0 zHD_H}J#%;PMem{~u+znW;=vF^40*e2+4Q^xbb93ZV_3NmaA;JyDz)5+9#*M>^rP)@ z`Kjik8ZR2@gj2vX!jX*8&1;-A5kA47omR&LV^izKj6#G5PItN#`IPt4d__invtw&X zihtX2IwevN;}x)@n?cB1-(CqzXCb;;P)Io6IBP1E72m-TqVW{fbU_+WAzXTPnc z6q81UMRX_c*T;VmN-x@ky;!c2GL#W2HqIF{|7#(` zjDAJBYR{Yv%VdnuH4e^_``D8jIT6+3)IQ8(y5yv4r*aTL;FJezS)eU&|G0uVF$S=3 z!RB{0MKaji9Sa?%P6zU;^HLP!>RBt)lw(ylA0=KCU-HSDC*y##&m#8^P&Sb9-7L}_8Xk+-V++Z5~$USXUsNw{mjC3G3!w${uy zw}?UL+w=|~x!=7ugI%||*XbYugs~WSaJ|dXzdJm+Am5c!k}JQRvj1)6-E*CiKevHF zC`C$t9rxboEiP|y1@9qnW`s `U-?Bi3envRe2=coElp&wxj8Rdt}+Xp7(A(_A; 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 Zjj4qxI+=$pQTLzMYD!v)l?vux{s#e0x3K^K diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index 260a36d2c80673936ebe076ffa3677ba5ceca2f7..a0486b25c2238e74d4e26b18ac66bff4c25cd804 100644 GIT binary patch literal 8074 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 literal 10203 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 VA5L(i;UoY6002ovPDHLkV1f{^&MW`` diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 9451e90e166fee72e107f3bb2cd4c0136f44dbd7..0f9cb71accf3bfc482749897bd84d20d4b216bac 100644 GIT binary patch literal 5240 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 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png deleted file mode 100644 index f8cf154e5c1b38a8f341fac4c1f6be1c81d86c1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16573 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 CRkiH^ literal 15094 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 + + false + \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000000..d689c57f18 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,7 @@ + + + #FFFFFF + #121212 + #000000 + #000000 + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 456fff9a6c..cc5ccc8b0f 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -4,6 +4,8 @@ + + diff --git a/app/src/main/res/values/bools.xml b/app/src/main/res/values/bools.xml new file mode 100644 index 0000000000..283ef35339 --- /dev/null +++ b/app/src/main/res/values/bools.xml @@ -0,0 +1,4 @@ + + + true + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e227b74439..a25e6f18d6 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,8 +1,12 @@ - #FAFAFA - #FAFAFA - #1E4376 + #1E4376 + #FF888888 + #FAFAFA + #FAFAFA + #121212 + + #1E4376 #FFFFFF #d3d3d3 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 7b258d49a5..68531ab730 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,15 +1,19 @@ - - + + diff --git a/build.gradle b/build.gradle index 835c2fccd1..aa90fcb7aa 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,8 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { +buildscript { + ext.kotlin_version = '1.3.61' + ext { kotlin_version = '1.3.61' room_version = '2.2.3' diff --git a/core/src/main/golang/bridge/statistics.go b/core/src/main/golang/bridge/statistics.go index f38ce271a6..d49ccfe5d1 100644 --- a/core/src/main/golang/bridge/statistics.go +++ b/core/src/main/golang/bridge/statistics.go @@ -27,6 +27,12 @@ type Logs interface { OnEvent(level, payload string) } +func QueryBandwidth() int64 { + current := tunnel.DefaultManager.Snapshot() + + return current.DownloadTotal + current.UploadTotal +} + func PollTraffic(traffic Traffic) *EventPoll { stopChannel := make(chan int, 1) ticker := time.NewTicker(time.Second) @@ -59,38 +65,6 @@ func PollTraffic(traffic Traffic) *EventPoll { } } -func PollBandwidth(bandwidth Bandwidth) *EventPoll { - stopChannel := make(chan int, 1) - ticker := time.NewTicker(time.Second) - - tick := func() { - s := tunnel.DefaultManager.Snapshot() - bandwidth.OnEvent(s.DownloadTotal + s.UploadTotal) - } - - tick() - - go func() { - defer close(stopChannel) - defer log.Infoln("Bandwidth Poll Stopped") - - for { - select { - case <-stopChannel: - return - case <-ticker.C: - tick() - } - } - }() - - return &EventPoll{ - onStop: func() { - stopChannel <- 0 - }, - } -} - func PollLogs(logs Logs) *EventPoll { stopChannel := make(chan int, 1) sub := log.Subscribe() diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 531c7067e2..7e405f50f9 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -127,16 +127,8 @@ object Clash{ } } - fun openBandwidthEvent(): EventStream { - return object: EventStream() { - val bandwidth = Bridge.pollBandwidth { - send(BandwidthEvent(it)) - } - - override fun onClose() { - bandwidth.stop() - } - } + fun queryBandwidth(): Long { + return Bridge.queryBandwidth() } fun openLogEvent(): EventStream { diff --git a/design/.gitignore b/design/.gitignore new file mode 100644 index 0000000000..796b96d1c4 --- /dev/null +++ b/design/.gitignore @@ -0,0 +1 @@ +/build diff --git a/design/build.gradle b/design/build.gradle new file mode 100644 index 0000000000..3e6bad8c16 --- /dev/null +++ b/design/build.gradle @@ -0,0 +1,34 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +android { + compileSdkVersion 29 + buildToolsVersion "29.0.2" + + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.core:core-ktx:1.1.0' + implementation "com.google.android.material:material:1.2.0-alpha04" +} diff --git a/design/consumer-rules.pro b/design/consumer-rules.pro new file mode 100644 index 0000000000..e69de29bb2 diff --git a/design/proguard-rules.pro b/design/proguard-rules.pro new file mode 100644 index 0000000000..f1b424510d --- /dev/null +++ b/design/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/design/src/main/AndroidManifest.xml b/design/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..2b1af1ddeb --- /dev/null +++ b/design/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt b/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt new file mode 100644 index 0000000000..ec5127880b --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt @@ -0,0 +1,58 @@ +package com.github.kr328.clash.design.view + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import com.github.kr328.clash.design.R +import com.google.android.material.card.MaterialCardView + +class ColorfulTextCard @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialCardView(context, attributeSet, defStyleAttr) { + private val iconView: View + private val titleView: TextView + private val summaryView: TextView + + var title: CharSequence + get() = titleView.text + set(value) { + titleView.text = value + } + + var summary: CharSequence + get() = summaryView.text + set(value) { + summaryView.text = value + } + + var icon: Drawable? + get() = iconView.background + set(value) { + iconView.background = value + } + + init { + LayoutInflater.from(context).inflate(R.layout.view_colorful_text_card, this, true).apply { + iconView = findViewById(android.R.id.icon) + titleView = findViewById(android.R.id.title) + summaryView = findViewById(android.R.id.summary) + } + + // Custom attrs + context.theme.obtainStyledAttributes(attributeSet, R.styleable.ColorfulTextCard, 0, 0).apply { + try { + iconView.background = getDrawable(R.styleable.ColorfulTextCard_icon) + titleView.text = getString(R.styleable.ColorfulTextCard_title) + summaryView.text = getString(R.styleable.ColorfulTextCard_summary) + } finally { + recycle() + } + } + } +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt b/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt new file mode 100644 index 0000000000..d8aa390b89 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt @@ -0,0 +1,58 @@ +package com.github.kr328.clash.design.view + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import com.github.kr328.clash.design.R +import com.google.android.material.card.MaterialCardView + +class TextCard @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyleAttr: Int = 0 +) : MaterialCardView(context, attributeSet, defStyleAttr) { + private val iconView: View + private val titleView: TextView + private val summaryView: TextView + + var title: CharSequence + get() = titleView.text + set(value) { + titleView.text = value + } + + var summary: CharSequence + get() = summaryView.text + set(value) { + summaryView.text = value + } + + var icon: Drawable? + get() = iconView.background + set(value) { + iconView.background = value + } + + init { + LayoutInflater.from(context).inflate(R.layout.view_text_card, this, true).apply { + iconView = findViewById(android.R.id.icon) + titleView = findViewById(android.R.id.title) + summaryView = findViewById(android.R.id.summary) + } + + // Custom attrs + context.theme.obtainStyledAttributes(attributeSet, R.styleable.TextCard, 0, 0).apply { + try { + iconView.background = getDrawable(R.styleable.TextCard_icon) + titleView.text = getString(R.styleable.TextCard_title) + summaryView.text = getString(R.styleable.TextCard_summary) + } finally { + recycle() + } + } + } +} \ No newline at end of file diff --git a/design/src/main/res/layout/view_colorful_text_card.xml b/design/src/main/res/layout/view_colorful_text_card.xml new file mode 100644 index 0000000000..1c20febde9 --- /dev/null +++ b/design/src/main/res/layout/view_colorful_text_card.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/design/src/main/res/layout/view_text_card.xml b/design/src/main/res/layout/view_text_card.xml new file mode 100644 index 0000000000..690d4d9af1 --- /dev/null +++ b/design/src/main/res/layout/view_text_card.xml @@ -0,0 +1,33 @@ + + + + + + + + + \ No newline at end of file diff --git a/design/src/main/res/values/attrs.xml b/design/src/main/res/values/attrs.xml new file mode 100644 index 0000000000..eba5c53d1c --- /dev/null +++ b/design/src/main/res/values/attrs.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml new file mode 100644 index 0000000000..b4e079e762 --- /dev/null +++ b/design/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Design + diff --git a/design/src/main/res/values/style.xml b/design/src/main/res/values/style.xml new file mode 100644 index 0000000000..0d2c4cc409 --- /dev/null +++ b/design/src/main/res/values/style.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index 29fab27802..7c0ad2b4ec 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -1,7 +1,7 @@ package com.github.kr328.clash.service; import com.github.kr328.clash.service.IClashProfileManager; -import com.github.kr328.clash.service.ipc.IPCParcelables; +import com.github.kr328.clash.service.ipc.IStreamCallback; import com.github.kr328.clash.core.model.Packet; interface IClashManager { @@ -10,15 +10,15 @@ interface IClashManager { // Control boolean setSelectProxy(String proxy, String selected); - ParcelableCompletedFuture startHealthCheck(String group); + void startHealthCheck(String group, IStreamCallback callback); // Query ProxyGroup[] queryAllProxies(); General queryGeneral(); + long queryBandwidth(); // Events - ParcelablePipe openBandwidthEvent(); - ParcelablePipe openLogEvent(); + void openLogEvent(IStreamCallback callback); // Settings boolean putSetting(String key, String value); diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl index 9a4584064a..7a18d50bee 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl @@ -1,10 +1,10 @@ package com.github.kr328.clash.service; -import com.github.kr328.clash.service.ipc.IPCParcelables; import com.github.kr328.clash.service.data.ClashProfileEntity; +import com.github.kr328.clash.service.ipc.IStreamCallback; interface IClashProfileManager { - ParcelableCompletedFuture addProfile(String name, int type, String uri); - ParcelableCompletedFuture updateProfile(int id); + void addProfile(String name, int type, String uri, IStreamCallback callback); + void updateProfile(int id, IStreamCallback callback); ClashProfileEntity[] queryAllProfiles(); } diff --git a/service/src/main/aidl/com/github/kr328/clash/service/ipc/IPCParcelables.aidl b/service/src/main/aidl/com/github/kr328/clash/service/ipc/IPCParcelables.aidl deleted file mode 100644 index 16f4928f4a..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/ipc/IPCParcelables.aidl +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.kr328.clash.service.ipc; - -parcelable ParcelablePipe; -parcelable ParcelableResult; -parcelable ParcelableCompletedFuture; diff --git a/service/src/main/aidl/com/github/kr328/clash/service/ipc/IStreamCallback.aidl b/service/src/main/aidl/com/github/kr328/clash/service/ipc/IStreamCallback.aidl new file mode 100644 index 0000000000..2114f796cd --- /dev/null +++ b/service/src/main/aidl/com/github/kr328/clash/service/ipc/IStreamCallback.aidl @@ -0,0 +1,9 @@ +package com.github.kr328.clash.service.ipc; + +import com.github.kr328.clash.service.ipc.ParcelableContainer; + +interface IStreamCallback { + void send(in ParcelableContainer data); + void complete(); + void completeExceptionally(String reason); +} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/ipc/ParcelableContainer.aidl b/service/src/main/aidl/com/github/kr328/clash/service/ipc/ParcelableContainer.aidl new file mode 100644 index 0000000000..9bdfa837ab --- /dev/null +++ b/service/src/main/aidl/com/github/kr328/clash/service/ipc/ParcelableContainer.aidl @@ -0,0 +1,3 @@ +package com.github.kr328.clash.service.ipc; + +parcelable ParcelableContainer; \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index d861a14a8f..5999860cc4 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -6,8 +6,11 @@ import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.ProxyGroup import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.ipc.IStreamCallback import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture +import com.github.kr328.clash.service.ipc.ParcelableContainer import com.github.kr328.clash.service.ipc.ParcelablePipe +import java.lang.Exception class ClashManager(private val context: Context) : IClashManager.Stub() { private val settings = context.getSharedPreferences("service", Context.MODE_PRIVATE) @@ -16,6 +19,7 @@ class ClashManager(private val context: Context) : IClashManager.Stub() { return ClashProfileManager(context, ClashDatabase.getInstance(context)) } + override fun queryAllProxies(): Array { return Clash.queryProxyGroups().toTypedArray() } @@ -30,52 +34,41 @@ class ClashManager(private val context: Context) : IClashManager.Stub() { return Clash.setSelectedProxy(proxy, selected) } - override fun openBandwidthEvent(): ParcelablePipe { - return object : ParcelablePipe() { - val stream = Clash.openBandwidthEvent().apply { - onEvent { - send(it) - } - } - - override fun onClose() { - stream.close() - } + override fun putSetting(key: String?, value: String?): Boolean { + settings.edit { + putString(key, value) } + return true } - override fun startHealthCheck(group: String?): ParcelableCompletedFuture { - require(group != null) - - return ParcelableCompletedFuture().apply { - Clash.startHealthCheck(group).whenComplete { _: Unit?, u: Throwable? -> - if (u != null) - this.completeExceptionally(u) - else - this.complete(null) - } - } + override fun queryBandwidth(): Long { + return Clash.queryBandwidth() } - override fun openLogEvent(): ParcelablePipe { - return object : ParcelablePipe() { - val stream = Clash.openLogEvent().apply { - onEvent { - send(it) - } - } + override fun openLogEvent(callback: IStreamCallback?) { + require(callback != null) - override fun onClose() { - stream.close() + Clash.openLogEvent().apply { + onEvent { + try { + callback.send(ParcelableContainer(it)) + } + catch (e: Exception) { + close() + } } } } - override fun putSetting(key: String?, value: String?): Boolean { - settings.edit { - putString(key, value) + override fun startHealthCheck(group: String?, callback: IStreamCallback?) { + require(group != null && callback != null) + + Clash.startHealthCheck(group).whenComplete { _, u -> + if ( u != null ) + callback.completeExceptionally(u.message) + else + callback.complete() } - return true } override fun getSetting(key: String?): String { diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt index 50d8454741..1a1ebc543c 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt @@ -6,6 +6,7 @@ import android.net.Uri import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.ipc.IStreamCallback import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture import com.github.kr328.clash.service.util.DefaultThreadPool import com.github.kr328.clash.service.util.FileUtils @@ -16,39 +17,34 @@ class ClashProfileManager(private val context: Context, private val database: Cl private val clashDir = context.filesDir.resolve(Constants.CLASH_DIR) private val profileDir = context.filesDir.resolve(Constants.PROFILES_DIR) - override fun updateProfile(id: Int): ParcelableCompletedFuture { + override fun updateProfile(id: Int, callback: IStreamCallback?) { val entity = database.openClashProfileDao().queryProfileById(id) require( - entity != null && (entity.type == ClashProfileEntity.Type.URL || - entity.type == ClashProfileEntity.Type.FILE) + entity != null && callback != null && (entity.type == ClashProfileEntity.TYPE_URL || + entity.type == ClashProfileEntity.TYPE_FILE) ) - val result = ParcelableCompletedFuture() - downloadProfile(Uri.parse(entity.uri), entity.file, entity.base, onSuccess = { - result.complete(null) + callback.complete() database.openClashProfileDao().touchProfile(id) sendChangedBroadcast() }, onFailure = { - result.completeExceptionally(it) + callback.completeExceptionally(it.message) }) - - return result } override fun queryAllProfiles(): Array { return database.openClashProfileDao().queryProfiles() } - override fun addProfile(name: String, type: Int, uri: String?): ParcelableCompletedFuture { - require(uri != null && (uri.startsWith("http") || uri.startsWith("content"))) + override fun addProfile(name: String, type: Int, uri: String?, callback: IStreamCallback?) { + require(uri != null && callback != null && (uri.startsWith("http") || uri.startsWith("content"))) - val result = ParcelableCompletedFuture() val fileName = FileUtils.generateRandomFileName(profileDir, ".yaml") val baseDirName = FileUtils.generateRandomFileName(clashDir) @@ -57,7 +53,7 @@ class ClashProfileManager(private val context: Context, private val database: Cl database.openClashProfileDao().addProfile( ClashProfileEntity( name, - ClashProfileEntity.intToType(type), + type, uri, fileName, baseDirName, @@ -68,13 +64,11 @@ class ClashProfileManager(private val context: Context, private val database: Cl sendChangedBroadcast() - result.complete(null) + callback.complete() }, onFailure = { - result.completeExceptionally(it) + callback.completeExceptionally(it.message) }) - - return result } private fun downloadProfile( diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt index 3972461b89..4e1f31f59c 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt @@ -1,9 +1,7 @@ package com.github.kr328.clash.service.data import android.content.Context -import androidx.room.Database -import androidx.room.Room -import androidx.room.RoomDatabase +import androidx.room.* @Database( version = 2, diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt index f06b909bfa..6e0f6da4e3 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt @@ -42,9 +42,9 @@ object ClashDatabaseMigrations { // new val type = when { - token.startsWith("url") -> ClashProfileEntity.Type.URL.id - token.startsWith("file") -> ClashProfileEntity.Type.FILE.id - else -> ClashProfileEntity.Type.UNKNOWN.id + token.startsWith("url") -> ClashProfileEntity.TYPE_URL + token.startsWith("file") -> ClashProfileEntity.TYPE_FILE + else -> ClashProfileEntity.TYPE_UNKNOWN } val uri = token.removePrefix("url|").removePrefix("file|") var base = random.nextLong().absoluteValue diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index 3997feb005..39f8ff7feb 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -13,7 +13,7 @@ import kotlinx.serialization.Serializable @Serializable data class ClashProfileEntity( @ColumnInfo(name = "name") val name: String, - @ColumnInfo(name = "type") val type: Type, + @ColumnInfo(name = "type") val type: Int, @ColumnInfo(name = "uri") val uri: String, @ColumnInfo(name = "file") val file: String, @ColumnInfo(name = "base") val base: String, @@ -21,10 +21,6 @@ data class ClashProfileEntity( @ColumnInfo(name = "last_update") val lastUpdate: Long, @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Int = 0 ) : Parcelable { - enum class Type(val id: Int) { - URL(1), FILE(2), UNKNOWN(-1) - } - override fun writeToParcel(parcel: Parcel, flags: Int) { Parcels.dump(serializer(), this, parcel) } @@ -34,20 +30,9 @@ data class ClashProfileEntity( } companion object { - @TypeConverter - fun typeToInt(value: Type?): Int { - return value?.id ?: -1 - } - - @TypeConverter - fun intToType(value: Int?): Type { - return when (value) { - Type.URL.id -> Type.URL - Type.FILE.id -> Type.FILE - Type.UNKNOWN.id -> Type.UNKNOWN - else -> Type.UNKNOWN - } - } + const val TYPE_FILE = 1 + const val TYPE_URL = 2 + const val TYPE_UNKNOWN = -1 @JvmField val CREATOR = object : Parcelable.Creator { diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt index 7055326da5..1df81319df 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt @@ -26,7 +26,7 @@ class ParcelableCompletedFuture private constructor(private val pipe: Parcelable override fun complete(value: Parcelable?): Boolean { if (super.complete(value)) { - pipe.send(ParcelableResult(value, null)) + pipe.sendRemote(ParcelableResult(value, null)) return true } return false @@ -34,7 +34,7 @@ class ParcelableCompletedFuture private constructor(private val pipe: Parcelable override fun completeExceptionally(ex: Throwable?): Boolean { if (super.completeExceptionally(ex)) { - pipe.send(ParcelableResult(null, ex?.message ?: "Unknown")) + pipe.sendRemote(ParcelableResult(null, ex?.message ?: "Unknown")) return true } return false diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableContainer.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableContainer.kt new file mode 100644 index 0000000000..000528d0d7 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableContainer.kt @@ -0,0 +1,28 @@ +package com.github.kr328.clash.service.ipc + +import android.os.Parcel +import android.os.Parcelable + +data class ParcelableContainer(val data: Parcelable?) : Parcelable { + constructor(parcel: Parcel): + this(parcel.readParcelable(ParcelableContainer::class.java.classLoader)) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeParcelable(data, 0) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ParcelableContainer { + return ParcelableContainer(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt index f0275c3587..1f5bd3b8dc 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt @@ -62,7 +62,7 @@ open class ParcelablePipe private constructor(val type: Type) : Parcelable { override fun describeContents(): Int = 0 - fun send(parcelable: Parcelable?): Boolean { + fun sendRemote(parcelable: Parcelable?): Boolean { if (type != Type.MASTER) return false diff --git a/settings.gradle b/settings.gradle index 38a9ed406e..e88caaec00 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':app', ':core', ':service' +include ':app', ':core', ':service', ':design' rootProject.name='Clash' From 896c57aa03f4884c7b316c1a4c8162c409ed3e72 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 2 Feb 2020 16:15:24 +0800 Subject: [PATCH 076/358] ProfileService refactored --- app/src/main/AndroidManifest.xml | 9 +- .../com/github/kr328/clash/BaseActivity.kt | 26 ++ .../com/github/kr328/clash/MainActivity.kt | 33 +- .../com/github/kr328/clash/MainApplication.kt | 42 +-- .../github/kr328/clash/ProfilesActivity.kt | 33 ++ .../github/kr328/clash/remote/Broadcasts.kt | 6 +- .../com/github/kr328/clash/remote/Calls.kt | 16 +- .../com/github/kr328/clash/remote/Channels.kt | 11 +- .../github/kr328/clash/remote/ClashClient.kt | 37 ++- .../com/github/kr328/clash/utils/DpUtils.kt | 4 +- .../res/drawable/ic_launcher_foreground.xml | 2 +- app/src/main/res/layout/activity_profiles.xml | 8 +- app/src/main/res/values-night/colors.xml | 1 + app/src/main/res/values/colors.xml | 2 + app/src/main/res/xml/full_backup_content.xml | 6 + core/src/main/golang/bridge/profiles.go | 54 +--- core/src/main/golang/bridge/statistics.go | 44 +-- core/src/main/golang/clash | 2 +- core/src/main/golang/profile/download.go | 14 +- .../java/com/github/kr328/clash/core/Clash.kt | 57 ++-- .../com/github/kr328/clash/core/Global.kt | 11 + .../kr328/clash/core/event/BandwidthEvent.kt | 28 -- .../github/kr328/clash/core/event/Event.kt | 2 +- .../kr328/clash/core/event/EventStream.kt | 4 +- .../github/kr328/clash/core/event/LogEvent.kt | 11 +- .../kr328/clash/core/event/ProcessEvent.kt | 29 -- .../clash/core/event/ProfileChangedEvent.kt | 27 -- .../clash/core/event/ProfileReloadEvent.kt | 27 -- .../clash/core/event/ProxyChangedEvent.kt | 27 -- .../kr328/clash/core/event/TrafficEvent.kt | 17 +- .../github/kr328/clash/core/model/General.kt | 11 +- .../kr328/clash/core/model/ProxyGroup.kt | 3 +- .../github/kr328/clash/core/model/Traffic.kt | 3 + .../clash/core/serialization/MergedParcels.kt | 54 +++- .../kr328/clash/core/serialization/Parcels.kt | 51 ++- .../clash/core/transact/DoneCallbackImpl.kt | 3 +- .../clash/core/transact/ProxyCollections.kt | 7 +- .../kr328/clash/core/utils/ByteFormatter.kt | 8 + service/src/main/AndroidManifest.xml | 13 +- .../kr328/clash/service/IClashManager.aidl | 6 +- .../clash/service/IClashProfileManager.aidl | 10 - .../service/transact/ProfileRequest.aidl | 3 + .../kr328/clash/service/ClashManager.kt | 18 +- .../kr328/clash/service/ClashNotification.kt | 68 ++-- .../clash/service/ClashProfileManager.kt | 125 -------- .../clash/service/ClashProfileReceiver.kt | 21 ++ .../clash/service/ClashProfileService.kt | 301 ++++++++++++++++++ .../kr328/clash/service/ClashService.kt | 8 +- .../github/kr328/clash/service/Constants.kt | 5 - .../com/github/kr328/clash/service/Intents.kt | 6 + .../kr328/clash/service/data/ClashDatabase.kt | 4 +- .../service/data/ClashDatabaseMigrations.kt | 4 +- .../clash/service/data/ClashProfileDao.kt | 22 +- .../clash/service/data/ClashProfileEntity.kt | 8 +- .../service/ipc/ParcelableCompletedFuture.kt | 60 ---- .../clash/service/ipc/ParcelableContainer.kt | 2 +- .../kr328/clash/service/ipc/ParcelablePipe.kt | 116 ------- .../clash/service/ipc/ParcelableResult.kt | 30 -- .../clash/service/transact/ProfileRequest.kt | 123 +++++++ .../clash/service/util/ComponentUtils.kt | 8 + .../kr328/clash/service/util/FileUtils.kt | 23 +- .../kr328/clash/service/util/RandomUtils.kt | 25 ++ .../src/main/res/drawable/ic_notification.xml | 9 + .../res/drawable/ic_notification_icon.png | Bin 4001 -> 0 bytes .../main/res/drawable/ic_update_completed.xml | 9 + .../main/res/drawable/ic_update_failure.xml | 9 + service/src/main/res/drawable/ic_updating.xml | 9 + .../main/res/layout/clash_notification.xml | 65 ---- service/src/main/res/values/strings.xml | 11 + 69 files changed, 962 insertions(+), 889 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt create mode 100644 app/src/main/res/xml/full_backup_content.xml create mode 100644 core/src/main/java/com/github/kr328/clash/core/Global.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/event/BandwidthEvent.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/event/ProcessEvent.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/event/ProfileChangedEvent.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/event/ProfileReloadEvent.kt delete mode 100644 core/src/main/java/com/github/kr328/clash/core/event/ProxyChangedEvent.kt create mode 100644 core/src/main/java/com/github/kr328/clash/core/model/Traffic.kt delete mode 100644 service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl create mode 100644 service/src/main/aidl/com/github/kr328/clash/service/transact/ProfileRequest.aidl delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashProfileReceiver.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/util/ComponentUtils.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/util/RandomUtils.kt create mode 100644 service/src/main/res/drawable/ic_notification.xml delete mode 100644 service/src/main/res/drawable/ic_notification_icon.png create mode 100644 service/src/main/res/drawable/ic_update_completed.xml create mode 100644 service/src/main/res/drawable/ic_update_failure.xml create mode 100644 service/src/main/res/drawable/ic_updating.xml delete mode 100644 service/src/main/res/layout/clash_notification.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e212bef28b..14eecf8c86 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,9 +7,9 @@ + diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 23ab3e229f..d65a462253 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -4,7 +4,10 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.res.Configuration +import android.os.Build +import android.os.Bundle import android.view.LayoutInflater +import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR import android.view.ViewGroup import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity @@ -76,6 +79,12 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() super.setContentView(base) } + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + resetLightNavigationBar() + } + override fun onStart() { super.onStart() @@ -120,4 +129,21 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() recreate() } + + private fun resetLightNavigationBar() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + return + + val light = resources.getBoolean(R.bool.lightStatusBar) + + if (light) { + window.decorView.systemUiVisibility = + window.decorView.systemUiVisibility or SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + } else { + window.decorView.systemUiVisibility = + window.decorView.systemUiVisibility and SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR.inv() + } + + window.navigationBarColor = getColor(R.color.backgroundColor) + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 2a096fe9e0..00ebea26d9 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -1,11 +1,13 @@ package com.github.kr328.clash +import android.content.Intent import android.os.Bundle -import com.github.kr328.clash.core.utils.ByteFormatter +import com.github.kr328.clash.core.utils.asBytesString import com.github.kr328.clash.remote.withClash import kotlinx.android.synthetic.main.activity_main.* import kotlinx.coroutines.Job import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch class MainActivity : BaseActivity() { @@ -15,6 +17,10 @@ class MainActivity : BaseActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + + profiles.setOnClickListener { + startActivity(Intent(this, ProfilesActivity::class.java)) + } } override fun onStart() { @@ -26,7 +32,7 @@ class MainActivity : BaseActivity() { status.title = getText(R.string.clash_status_started) status.summary = getString( R.string.clash_status_forwarded_traffic, - ByteFormatter.byteToString(0L) + 0L.asBytesString() ) } @@ -53,28 +59,25 @@ class MainActivity : BaseActivity() { } private fun startBandwidthPolling() { - if ( bandwidthJob != null ) + if (bandwidthJob != null) return - launch { + bandwidthJob = launch { withClash { - bandwidthJob = launch { - while (clashRunning) { - val bandwidth = queryBandwidth() - status.summary = getString( - R.string.clash_status_forwarded_traffic, - ByteFormatter.byteToString(bandwidth) - ) - delay(1000) - } - bandwidthJob = null + while (clashRunning && isActive) { + val bandwidth = queryBandwidth() + status.summary = getString( + R.string.clash_status_forwarded_traffic, + bandwidth.asBytesString() + ) + delay(1000) } + bandwidthJob = null } } } private fun stopBandwidthPolling() { bandwidthJob?.cancel() - bandwidthJob = null } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index a8d49efd74..461528549d 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -4,38 +4,28 @@ import android.app.Application import android.content.Context import android.os.Build import com.crashlytics.android.Crashlytics -import com.github.kr328.clash.core.Constants -import com.github.kr328.clash.core.utils.ByteFormatter -import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.core.Global +import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.remote.ClashClient import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric -import java.io.File -import java.lang.Exception import java.security.MessageDigest -import java.util.zip.ZipFile -import kotlin.concurrent.thread +@Suppress("unused") class MainApplication : Application() { companion object { const val KEY_PROXY_MODE = "key_proxy_mode" const val PROXY_MODE_VPN = "vpn" const val PROXY_MODE_PROXY_ONLY = "proxy_only" - val GOOGLE_PLAY_INSTALLER = listOf("com.android.vending", "com.google.android.feedback") - const val CRASHLYTICS_FROM_PLAY_KEY = "install_from_google" - const val CRASHLYTICS_SPLIT_APK_KEY = "split_apk" - const val CRASHLYTICS_BASE_SIZE_KEY = "base_size" - val userIdentifier: String by lazy { - val archive = instance.packageManager.getPackageInfo(instance.packageName, 0) + val archive = + Global.application.packageManager.getPackageInfo(Global.application.packageName, 0) val encoder = MessageDigest.getInstance("md5") encoder.digest((Build.ID + archive.lastUpdateTime).toByteArray()).toHexString() } - lateinit var instance: MainApplication - private fun ByteArray.toHexString(): String { return this.map { Integer.toHexString(it.toInt() and 0xff) @@ -51,7 +41,7 @@ class MainApplication : Application() { override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) - instance = this + Global.init(this) } override fun onCreate() { @@ -64,27 +54,9 @@ class MainApplication : Application() { Fabric.with(this, Crashlytics()) } - Crashlytics.setBool(CRASHLYTICS_FROM_PLAY_KEY, detectFromPlay()) - Crashlytics.setBool(CRASHLYTICS_SPLIT_APK_KEY, detectSplitArchive()) - Crashlytics.setString(CRASHLYTICS_BASE_SIZE_KEY, getBaseApkSize()) Crashlytics.setUserIdentifier(userIdentifier) ClashClient.init(this) - } - - private fun detectFromPlay(): Boolean { - val installer = packageManager.getInstallerPackageName(packageName) - return installer != null && GOOGLE_PLAY_INSTALLER.contains(installer) - } - - private fun detectSplitArchive(): Boolean { - val split = applicationInfo.splitSourceDirs - return split != null && split.isNotEmpty() - } - - private fun getBaseApkSize(): String { - val size = File(applicationInfo.sourceDir).length() - - return ByteFormatter.byteToString(size) + Broadcasts.init(this) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt new file mode 100644 index 0000000000..7192086873 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -0,0 +1,33 @@ +package com.github.kr328.clash + +import android.os.Bundle +import com.github.kr328.clash.remote.withClash +import com.github.kr328.clash.service.data.ClashProfileEntity +import kotlinx.android.synthetic.main.activity_profiles.* +import kotlinx.coroutines.launch + +class ProfilesActivity : BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_profiles) + + setSupportActionBar(toolbar) + + reloadProfiles() + } + + override suspend fun onClashProfileChanged(active: ClashProfileEntity?) { + super.onClashProfileChanged(active) + + reloadProfiles() + } + + private fun reloadProfiles() { + launch { + withClash { + + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt index c99997e651..0b43f83abe 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt @@ -20,9 +20,9 @@ object Broadcasts { private val receivers = mutableListOf() - private val broadcastReceiver = object: BroadcastReceiver() { + private val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - when ( intent?.action ) { + when (intent?.action) { Intents.INTENT_ACTION_CLASH_STARTED -> receivers.forEach { it.onStarted() @@ -48,7 +48,7 @@ object Broadcasts { } fun init(application: Application) { - ProcessLifecycleOwner.get().lifecycle.addObserver(object: DefaultLifecycleObserver { + ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { application.registerReceiver(broadcastReceiver, IntentFilter().apply { addAction(Intents.INTENT_ACTION_PROFILE_CHANGED) diff --git a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt index 1217896524..5c2e93e205 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt @@ -1,19 +1,7 @@ package com.github.kr328.clash.remote -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.async -import kotlinx.coroutines.launch - -suspend fun withClash(block: suspend ClashClient.() -> T): T? { - val client = ClashClient.instance ?: return null +suspend fun withClash(block: suspend ClashClient.() -> T): T? { + val client = ClashClient.clashInstanceChannel.receive() return client.block() -} - -fun CoroutineScope.launchClash(block: suspend ClashClient.() -> Unit) { - val client = ClashClient.instance ?: return - - launch { - client.block() - } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Channels.kt b/app/src/main/java/com/github/kr328/clash/remote/Channels.kt index fea3f0818e..e1a800ab25 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Channels.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Channels.kt @@ -1,18 +1,14 @@ package com.github.kr328.clash.remote import android.os.RemoteException -import com.github.kr328.clash.core.event.BandwidthEvent import com.github.kr328.clash.core.event.LogEvent import com.github.kr328.clash.service.ipc.IStreamCallback import com.github.kr328.clash.service.ipc.ParcelableContainer -import com.github.kr328.clash.service.ipc.ParcelablePipe -import kotlinx.coroutines.CancellationException import kotlinx.coroutines.channels.Channel -import java.lang.Exception -class LogChannel: Channel by Channel(Channel.CONFLATED) { +class LogChannel : Channel by Channel(Channel.CONFLATED) { fun createCallback(): IStreamCallback { - return object: IStreamCallback.Stub() { + return object : IStreamCallback.Stub() { override fun complete() { close() } @@ -24,8 +20,7 @@ class LogChannel: Channel by Channel(Channel.CONFLATED) { override fun send(data: ParcelableContainer?) { try { offer(data!!.data!! as LogEvent) - } - catch (e: Exception) { + } catch (e: Exception) { throw RemoteException(e.message) } } diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt index c70a118392..8448dabcee 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -1,33 +1,36 @@ package com.github.kr328.clash.remote import android.app.Application -import android.content.* +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.os.IBinder import android.os.RemoteException import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner -import com.github.kr328.clash.MainApplication -import com.github.kr328.clash.core.event.BandwidthEvent import com.github.kr328.clash.core.event.LogEvent import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.ProxyGroup import com.github.kr328.clash.service.ClashManagerService import com.github.kr328.clash.service.IClashManager +import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.ipc.IStreamCallback -import com.github.kr328.clash.service.ipc.ParcelableContainer -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel -import kotlinx.coroutines.withContext -class ClashClient(val service: IClashManager) { +class ClashClient(private val service: IClashManager) { companion object { - var instance: ClashClient? = null + var clashInstanceChannel = Channel() private val connection = object : ServiceConnection { + var instance: ClashClient? = null + var job: Job? = null + override fun onServiceDisconnected(name: ComponentName?) { + job?.cancel() instance?.close() instance = null } @@ -35,6 +38,13 @@ class ClashClient(val service: IClashManager) { override fun onServiceConnected(name: ComponentName?, service: IBinder?) { if (service != null) instance = ClashClient(IClashManager.Stub.asInterface(service)) + + job = GlobalScope.launch { + while (isActive) { + val clash = instance ?: return@launch + clashInstanceChannel.send(clash) + } + } } } @@ -47,6 +57,7 @@ class ClashClient(val service: IClashManager) { Context.BIND_AUTO_CREATE ) } + override fun onStop(owner: LifecycleOwner) { application.unbindService(connection) } @@ -62,7 +73,7 @@ class ClashClient(val service: IClashManager) { suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) { CompletableDeferred().apply { - service.startHealthCheck(group, object: IStreamCallback.Default() { + service.startHealthCheck(group, object : IStreamCallback.Default() { override fun complete() { this@apply.complete(Unit) } @@ -78,6 +89,10 @@ class ClashClient(val service: IClashManager) { service.queryAllProxies() } + suspend fun queryProfiles(): Array = withContext(Dispatchers.IO) { + service.queryAllProfiles() + } + suspend fun queryGeneral(): General = withContext(Dispatchers.IO) { service.queryGeneral() } @@ -89,7 +104,7 @@ class ClashClient(val service: IClashManager) { } suspend fun queryBandwidth(): Long = - withContext(Dispatchers.IO){ + withContext(Dispatchers.IO) { service.queryBandwidth() } diff --git a/app/src/main/java/com/github/kr328/clash/utils/DpUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/DpUtils.kt index 8d80ef0194..62de4d297d 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/DpUtils.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/DpUtils.kt @@ -1,13 +1,13 @@ package com.github.kr328.clash.utils import android.util.TypedValue -import com.github.kr328.clash.MainApplication +import com.github.kr328.clash.core.Global val Int.dp: Float get() { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, this.toFloat(), - MainApplication.instance.resources.displayMetrics + Global.application.resources.displayMetrics ) } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 2c8f2afa7b..2ed6af7ee4 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -5,6 +5,6 @@ android:viewportHeight="787.6923"> - + diff --git a/app/src/main/res/layout/activity_profiles.xml b/app/src/main/res/layout/activity_profiles.xml index 588220466c..6e4fa65799 100644 --- a/app/src/main/res/layout/activity_profiles.xml +++ b/app/src/main/res/layout/activity_profiles.xml @@ -1,6 +1,5 @@ + android:layout_height="wrap_content" + android:background="@color/toolbarColor"/> \ No newline at end of file diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index d689c57f18..5f7cb7777b 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -4,4 +4,5 @@ #121212 #000000 #000000 + #121212 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index a25e6f18d6..f4d0541d52 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -4,6 +4,8 @@ #FF888888 #FAFAFA #FAFAFA + #FAFAFA + #121212 #1E4376 diff --git a/app/src/main/res/xml/full_backup_content.xml b/app/src/main/res/xml/full_backup_content.xml new file mode 100644 index 0000000000..38f025dcf4 --- /dev/null +++ b/app/src/main/res/xml/full_backup_content.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go index e5c98b4240..5c8d5f5deb 100644 --- a/core/src/main/golang/bridge/profiles.go +++ b/core/src/main/golang/bridge/profiles.go @@ -2,16 +2,10 @@ package bridge import ( "github.com/kr328/cfa/profile" - "sync" ) -var mutex sync.Mutex - func LoadProfileFile(path, baseDir string, callback DoneCallback) { go func() { - mutex.Lock() - defer mutex.Unlock() - err := profile.LoadFromFile(path, baseDir) if err != nil { callback.DoneWithError(err) @@ -21,44 +15,22 @@ func LoadProfileFile(path, baseDir string, callback DoneCallback) { }() } -func DownloadProfileAndCheck(url, output, baseDir string, callback DoneCallback) { - go func() { - mutex.Lock() - defer mutex.Unlock() - - err := profile.DownloadAndCheck(url, output, baseDir) - if err != nil { - callback.DoneWithError(err) - } else { - callback.Done() - } - }() +func DownloadProfileAndCheck(url, output, baseDir string) error { + err := profile.DownloadAndCheck(url, output, baseDir) + if err != nil { + return err + } + return nil } -func SaveProfileAndCheck(data []byte, output, baseDir string, callback DoneCallback) { - go func() { - mutex.Lock() - defer mutex.Unlock() - - err := profile.SaveAndCheck(data, output, baseDir) - if err != nil { - callback.DoneWithError(err) - } else { - callback.Done() - } - }() +func ReadProfileAndCheck(fd int, output, baseDir string) error { + return profile.ReadAndCheck(fd, output, baseDir) } -func MoveProfileAndCheck(source, target, baseDir string, callback DoneCallback) { - go func() { - mutex.Lock() - defer mutex.Unlock() +func SaveProfileAndCheck(data []byte, output, baseDir string) error { + return profile.SaveAndCheck(data, output, baseDir) +} - err := profile.MoveAndCheck(source, target, baseDir) - if err != nil { - callback.DoneWithError(err) - } else { - callback.Done() - } - }() +func MoveProfileAndCheck(source, target, baseDir string) error { + return profile.MoveAndCheck(source, target, baseDir) } diff --git a/core/src/main/golang/bridge/statistics.go b/core/src/main/golang/bridge/statistics.go index d49ccfe5d1..e75eaf7890 100644 --- a/core/src/main/golang/bridge/statistics.go +++ b/core/src/main/golang/bridge/statistics.go @@ -1,8 +1,6 @@ package bridge import ( - "time" - "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/tunnel" ) @@ -15,8 +13,9 @@ func (e *EventPoll) Stop() { e.onStop() } -type Traffic interface { - OnEvent(down int64, up int64) +type Traffic struct { + Download int64 + Upload int64 } type Bandwidth interface { @@ -28,40 +27,15 @@ type Logs interface { } func QueryBandwidth() int64 { - current := tunnel.DefaultManager.Snapshot() - - return current.DownloadTotal + current.UploadTotal + return tunnel.DefaultManager.Forwarded() } -func PollTraffic(traffic Traffic) *EventPoll { - stopChannel := make(chan int, 1) - ticker := time.NewTicker(time.Second) - - tick := func() { - up, down := tunnel.DefaultManager.Now() - traffic.OnEvent(down, up) - } - - tick() +func QueryTraffic() *Traffic { + up, down := tunnel.DefaultManager.Now() - go func() { - defer close(stopChannel) - defer log.Infoln("Traffic Poll Stopped") - - for { - select { - case <-stopChannel: - return - case <-ticker.C: - tick() - } - } - }() - - return &EventPoll{ - onStop: func() { - stopChannel <- 0 - }, + return &Traffic{ + Upload: up, + Download: down, } } diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index 7adba59744..0a4aa8fcb6 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit 7adba597440b5d2d54677731c0df7a86b564e90f +Subproject commit 0a4aa8fcb6f9ef43d4e6c8b8beac8ef67ab31f80 diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go index 19fbd49e5d..f24e389d83 100644 --- a/core/src/main/golang/profile/download.go +++ b/core/src/main/golang/profile/download.go @@ -45,10 +45,6 @@ var client = &http.Client{ } func DownloadAndCheck(url, output, baseDir string) error { - original := constant.Path.HomeDir() - constant.SetHomeDir(baseDir) - defer constant.SetHomeDir(original) - response, err := client.Get(url) if err != nil { return err @@ -60,12 +56,18 @@ func DownloadAndCheck(url, output, baseDir string) error { return err } - _, err = parseConfig(data) + return SaveAndCheck(data, output, baseDir) +} + +func ReadAndCheck(fd int, output, baseDir string) error { + file := os.NewFile(uintptr(fd), "/dev/null") + + data, err := ioutil.ReadAll(file) if err != nil { return err } - return ioutil.WriteFile(output, data, defaultFileMode) + return SaveAndCheck(data, output, baseDir) } func SaveAndCheck(data []byte, output, baseDir string) error { diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 7e405f50f9..47e207036f 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -3,37 +3,32 @@ package com.github.kr328.clash.core import android.content.Context import bridge.Bridge import bridge.EventPoll -import com.github.kr328.clash.core.event.* +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 import com.github.kr328.clash.core.model.ProxyGroup +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 java.io.File import java.io.InputStream -import java.lang.IllegalStateException -import java.util.concurrent.BrokenBarrierException import java.util.concurrent.CompletableFuture -object Clash{ +object Clash { private var initialized = false - class Poll(private val poll: EventPoll) { - fun stop() { - poll.stop() - } - } - + @Synchronized fun initialize(context: Context) { - if ( initialized ) + if (initialized) return initialized = true - val country = context.assets.open("Country.mmdb") + val bytes = context.assets.open("Country.mmdb") .use(InputStream::readBytes) - Bridge.loadMMDB(country) + Bridge.loadMMDB(bytes) } fun start() { @@ -62,22 +57,20 @@ object Clash{ } } - fun downloadProfile(url: String, output: File, baseDir: File): CompletableFuture { - return DoneCallbackImpl().apply { - Bridge.downloadProfileAndCheck(url, output.absolutePath, baseDir.absolutePath, this) - } + fun downloadProfile(url: String, output: File, baseDir: File) { + Bridge.downloadProfileAndCheck(url, output.absolutePath, baseDir.absolutePath) } - fun saveProfile(data: ByteArray, output: File, baseDir: File): CompletableFuture { - return DoneCallbackImpl().apply { - Bridge.saveProfileAndCheck(data, output.absolutePath, baseDir.absolutePath, this) - } + fun copyProfile(fd: Int, output: File, baseDir: File) { + Bridge.readProfileAndCheck(fd.toLong(), output.absolutePath, baseDir.absolutePath) } - fun moveProfile(source: File, target: File, baseDir: File): CompletableFuture { - return DoneCallbackImpl().apply { - Bridge.moveProfileAndCheck(source.absolutePath, target.absolutePath, baseDir.absolutePath, this) - } + fun saveProfile(data: ByteArray, output: File, baseDir: File) { + Bridge.saveProfileAndCheck(data, output.absolutePath, baseDir.absolutePath) + } + + fun moveProfile(source: File, target: File, baseDir: File) { + Bridge.moveProfileAndCheck(source.absolutePath, target.absolutePath, baseDir.absolutePath) } fun queryProxyGroups(): List { @@ -115,16 +108,10 @@ object Clash{ ) } - fun openTrafficEvent(): EventStream { - return object: EventStream() { - val traffic = Bridge.pollTraffic { down, up -> - send(TrafficEvent(down, up)) - } + fun queryTrafficEvent(): Traffic { + val data = Bridge.queryTraffic() - override fun onClose() { - traffic.stop() - } - } + return Traffic(data.upload, data.download) } fun queryBandwidth(): Long { @@ -132,7 +119,7 @@ object Clash{ } fun openLogEvent(): EventStream { - return object: EventStream() { + return object : EventStream() { val log = Bridge.pollLogs { level, payload -> send(LogEvent(LogEvent.Level.fromString(level), payload)) } diff --git a/core/src/main/java/com/github/kr328/clash/core/Global.kt b/core/src/main/java/com/github/kr328/clash/core/Global.kt new file mode 100644 index 0000000000..b27c2cccac --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/Global.kt @@ -0,0 +1,11 @@ +package com.github.kr328.clash.core + +import android.app.Application + +object Global { + lateinit var application: Application + + fun init(application: Application) { + this.application = application + } +} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/event/BandwidthEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/BandwidthEvent.kt deleted file mode 100644 index 313b7ba22b..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/event/BandwidthEvent.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.kr328.clash.core.event - -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.Parcels -import kotlinx.serialization.Serializable - -@Serializable -data class BandwidthEvent(val total: Long): Event { - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): BandwidthEvent { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/event/Event.kt b/core/src/main/java/com/github/kr328/clash/core/event/Event.kt index 08fa5e9102..89cc9cab4f 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/Event.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/Event.kt @@ -2,7 +2,7 @@ package com.github.kr328.clash.core.event import android.os.Parcelable -interface Event: Parcelable { +interface Event : Parcelable { companion object { const val EVENT_LOG = 1 const val EVENT_TRAFFIC = 3 diff --git a/core/src/main/java/com/github/kr328/clash/core/event/EventStream.kt b/core/src/main/java/com/github/kr328/clash/core/event/EventStream.kt index 275484e196..f7a59dd1be 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/EventStream.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/EventStream.kt @@ -1,8 +1,6 @@ package com.github.kr328.clash.core.event -import android.os.Parcelable - -abstract class EventStream { +abstract class EventStream { private var on: (T) -> Unit = {} fun onEvent(callback: (T) -> Unit) { diff --git a/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt index 016af6ae14..c4eaf8c354 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt @@ -3,11 +3,14 @@ package com.github.kr328.clash.core.event import android.os.Parcel import android.os.Parcelable import com.github.kr328.clash.core.serialization.Parcels -import kotlinx.serialization.* -import kotlinx.serialization.internal.StringDescriptor +import kotlinx.serialization.Serializable @Serializable -data class LogEvent(val level: Level, val message: String, val time: Long = System.currentTimeMillis()) : +data class LogEvent( + val level: Level, + val message: String, + val time: Long = System.currentTimeMillis() +) : Event { companion object { const val DEBUG_VALUE = "debug" @@ -37,7 +40,7 @@ data class LogEvent(val level: Level, val message: String, val time: Long = Syst companion object { fun fromString(type: String): Level { - return when ( type ) { + return when (type) { DEBUG_VALUE -> DEBUG INFO_VALUE -> INFO WARN_VALUE -> WARN diff --git a/core/src/main/java/com/github/kr328/clash/core/event/ProcessEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/ProcessEvent.kt deleted file mode 100644 index f73c534f5f..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/event/ProcessEvent.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.github.kr328.clash.core.event - -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.Parcels -import kotlinx.serialization.Serializable - -@Serializable -enum class ProcessEvent : Parcelable, Event { - STARTED, STOPPED; - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ProcessEvent { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/event/ProfileChangedEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/ProfileChangedEvent.kt deleted file mode 100644 index 3a9d53dc87..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/event/ProfileChangedEvent.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.kr328.clash.core.event - -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.Parcels -import kotlinx.serialization.Serializable - -@Serializable -class ProfileChangedEvent : Event, Parcelable { - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ProfileChangedEvent { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/event/ProfileReloadEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/ProfileReloadEvent.kt deleted file mode 100644 index f7e7f6ba90..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/event/ProfileReloadEvent.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.kr328.clash.core.event - -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.Parcels -import kotlinx.serialization.Serializable - -@Serializable -class ProfileReloadEvent : Event, Parcelable { - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ProfileReloadEvent { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/event/ProxyChangedEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/ProxyChangedEvent.kt deleted file mode 100644 index 9f2e62af4c..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/event/ProxyChangedEvent.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.kr328.clash.core.event - -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.Parcels -import kotlinx.serialization.Serializable - -@Serializable -data class ProxyChangedEvent(val name: String, val selected: String) : Event, Parcelable { - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ProxyChangedEvent { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/event/TrafficEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/TrafficEvent.kt index 2086736881..630cfa9745 100644 --- a/core/src/main/java/com/github/kr328/clash/core/event/TrafficEvent.kt +++ b/core/src/main/java/com/github/kr328/clash/core/event/TrafficEvent.kt @@ -2,6 +2,7 @@ package com.github.kr328.clash.core.event import android.os.Parcel import android.os.Parcelable +import androidx.annotation.Keep import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable @@ -15,13 +16,17 @@ data class TrafficEvent(val down: Long, val up: Long) : Event, Parcelable { return 0 } - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): TrafficEvent { - return Parcels.load(serializer(), parcel) - } + companion object { + @JvmField + @Keep + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): TrafficEvent { + return Parcels.load(serializer(), parcel) + } - override fun newArray(size: Int): Array { - return arrayOfNulls(size) + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } } } } \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/General.kt b/core/src/main/java/com/github/kr328/clash/core/model/General.kt index f310e93eec..333bf19591 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/General.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/General.kt @@ -5,7 +5,6 @@ import android.os.Parcelable import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.* import kotlinx.serialization.internal.StringDescriptor -import kotlin.IllegalArgumentException @Serializable data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: Int) : Parcelable { @@ -14,7 +13,7 @@ data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: DIRECT, GLOBAL, RULE; override fun toString(): String { - return when ( this ) { + return when (this) { DIRECT -> "Direct" GLOBAL -> "Global" RULE -> "Rule" @@ -23,7 +22,7 @@ data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: companion object { fun fromString(mode: String): Mode { - return when ( mode ) { + return when (mode) { "Direct" -> DIRECT "Global" -> GLOBAL "Rule" -> RULE @@ -38,7 +37,7 @@ data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: get() = StringDescriptor override fun deserialize(decoder: Decoder): Mode { - return when ( decoder.decodeInt() ) { + return when (decoder.decodeInt()) { MODE_DIRECT -> Mode.DIRECT MODE_GLOBAL -> Mode.GLOBAL MODE_RULE -> Mode.RULE @@ -47,7 +46,7 @@ data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: } override fun serialize(encoder: Encoder, obj: Mode) { - when ( obj ) { + when (obj) { Mode.DIRECT -> encoder.encodeInt(MODE_DIRECT) Mode.GLOBAL -> encoder.encodeInt(MODE_GLOBAL) Mode.RULE -> encoder.encodeInt(MODE_RULE) @@ -69,7 +68,7 @@ data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: const val MODE_RULE = 3 @JvmField - val CREATOR = object: Parcelable.Creator { + val CREATOR = object : Parcelable.Creator { override fun createFromParcel(parcel: Parcel): General { return Parcels.load(serializer(), parcel) } diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt index 57cfa2f308..d7a64d0201 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt @@ -3,7 +3,6 @@ package com.github.kr328.clash.core.model import android.os.Parcel import android.os.Parcelable import com.github.kr328.clash.core.serialization.MergedParcels -import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable @Serializable @@ -13,7 +12,7 @@ data class ProxyGroup( val delay: Long, val current: String, val proxies: List -): Parcelable { +) : Parcelable { override fun writeToParcel(parcel: Parcel, flags: Int) { MergedParcels.dump(serializer(), this, parcel) } diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Traffic.kt b/core/src/main/java/com/github/kr328/clash/core/model/Traffic.kt new file mode 100644 index 0000000000..a642cbb733 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/model/Traffic.kt @@ -0,0 +1,3 @@ +package com.github.kr328.clash.core.model + +data class Traffic(val upload: Long, val download: Long) \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt index 98f7ff36f0..e265a78105 100644 --- a/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt +++ b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt @@ -5,7 +5,7 @@ import kotlinx.serialization.* import kotlinx.serialization.modules.EmptyModule import kotlinx.serialization.modules.SerialModule -object MergedParcels: AbstractSerialFormat(EmptyModule) { +object MergedParcels : AbstractSerialFormat(EmptyModule) { fun dump(serializer: SerializationStrategy, obj: T, parcel: Parcel) { val data = Parcel.obtain() val encoder = ParcelsEncoder(data) @@ -17,8 +17,7 @@ object MergedParcels: AbstractSerialFormat(EmptyModule) { parcel.writeStringList(encoder.getStringList()) parcel.appendFrom(data, 0, data.dataSize()) - } - finally { + } finally { data.recycle() } } @@ -55,33 +54,44 @@ object MergedParcels: AbstractSerialFormat(EmptyModule) { override fun encodeBooleanElement(desc: SerialDescriptor, index: Int, value: Boolean) = encodeBoolean(value) + override fun encodeByteElement(desc: SerialDescriptor, index: Int, value: Byte) = encodeByte(value) + override fun encodeCharElement(desc: SerialDescriptor, index: Int, value: Char) = encodeChar(value) + override fun encodeDoubleElement(desc: SerialDescriptor, index: Int, value: Double) = encodeDouble(value) + override fun encodeFloatElement(desc: SerialDescriptor, index: Int, value: Float) = encodeFloat(value) + override fun encodeIntElement(desc: SerialDescriptor, index: Int, value: Int) = encodeInt(value) + override fun encodeLongElement(desc: SerialDescriptor, index: Int, value: Long) = encodeLong(value) + override fun encodeShortElement(desc: SerialDescriptor, index: Int, value: Short) = encodeShort(value) + override fun encodeStringElement(desc: SerialDescriptor, index: Int, value: String) = encodeString(value) + override fun encodeUnitElement(desc: SerialDescriptor, index: Int) = encodeUnit() override fun encodeNonSerializableElement(desc: SerialDescriptor, index: Int, value: Any) = throw IllegalArgumentException("Unsupported") + override fun encodeNullableSerializableElement( desc: SerialDescriptor, index: Int, serializer: SerializationStrategy, value: T? ) = encodeNullableSerializableValue(serializer, value) + override fun encodeSerializableElement( desc: SerialDescriptor, index: Int, @@ -95,27 +105,38 @@ object MergedParcels: AbstractSerialFormat(EmptyModule) { ): CompositeEncoder = this override fun encodeBoolean(value: Boolean) = - parcel.writeByte(if ( value ) 1 else 0) + parcel.writeByte(if (value) 1 else 0) + override fun encodeByte(value: Byte) = parcel.writeByte(value) + override fun encodeChar(value: Char) = parcel.writeInt(value.toInt()) + override fun encodeDouble(value: Double) = parcel.writeDouble(value) + override fun encodeEnum(enumDescription: SerialDescriptor, ordinal: Int) = parcel.writeInt(ordinal) + override fun encodeFloat(value: Float) = parcel.writeFloat(value) + override fun encodeInt(value: Int) = parcel.writeInt(value) + override fun encodeLong(value: Long) = parcel.writeLong(value) + override fun encodeNotNullMark() = encodeBoolean(true) + override fun encodeNull() = encodeBoolean(false) + override fun encodeShort(value: Short) = parcel.writeInt(value.toInt()) + override fun encodeUnit() {} override fun encodeString(value: String) { val index = strings.computeIfAbsent(value) { @@ -125,7 +146,8 @@ object MergedParcels: AbstractSerialFormat(EmptyModule) { } } - class ParcelsDecoder(private val strings: List, private val parcel: Parcel) : Decoder, CompositeDecoder { + class ParcelsDecoder(private val strings: List, private val parcel: Parcel) : Decoder, + CompositeDecoder { override val context: SerialModule get() = EmptyModule override val updateMode: UpdateMode @@ -133,27 +155,37 @@ object MergedParcels: AbstractSerialFormat(EmptyModule) { override fun decodeElementIndex(desc: SerialDescriptor) = CompositeDecoder.READ_ALL + override fun decodeCollectionSize(desc: SerialDescriptor) = decodeInt() override fun decodeBooleanElement(desc: SerialDescriptor, index: Int) = decodeBoolean() + override fun decodeByteElement(desc: SerialDescriptor, index: Int) = decodeByte() + override fun decodeCharElement(desc: SerialDescriptor, index: Int) = decodeChar() + override fun decodeDoubleElement(desc: SerialDescriptor, index: Int) = decodeDouble() + override fun decodeFloatElement(desc: SerialDescriptor, index: Int) = decodeFloat() + override fun decodeIntElement(desc: SerialDescriptor, index: Int) = decodeInt() + override fun decodeShortElement(desc: SerialDescriptor, index: Int) = decodeShort() + override fun decodeLongElement(desc: SerialDescriptor, index: Int) = decodeLong() + override fun decodeStringElement(desc: SerialDescriptor, index: Int) = decodeString() + override fun decodeUnitElement(desc: SerialDescriptor, index: Int) = decodeUnit() @@ -162,6 +194,7 @@ object MergedParcels: AbstractSerialFormat(EmptyModule) { index: Int, deserializer: DeserializationStrategy ) = decodeNullableSerializableValue(deserializer) + override fun decodeSerializableElement( desc: SerialDescriptor, index: Int, @@ -189,26 +222,37 @@ object MergedParcels: AbstractSerialFormat(EmptyModule) { override fun decodeBoolean() = parcel.readByte() != 0.toByte() + override fun decodeByte() = parcel.readByte() + override fun decodeChar() = parcel.readInt().toChar() + override fun decodeDouble() = parcel.readDouble() + override fun decodeEnum(enumDescription: SerialDescriptor) = parcel.readInt() + override fun decodeFloat() = parcel.readFloat() + override fun decodeInt() = parcel.readInt() + override fun decodeLong() = parcel.readLong() + override fun decodeNotNullMark() = decodeBoolean() + override fun decodeNull() = null + override fun decodeShort() = parcel.readInt().toShort() + override fun decodeUnit() {} override fun decodeString() = strings[parcel.readInt()] diff --git a/core/src/main/java/com/github/kr328/clash/core/serialization/Parcels.kt b/core/src/main/java/com/github/kr328/clash/core/serialization/Parcels.kt index 30368d062d..ff9ba8a46b 100644 --- a/core/src/main/java/com/github/kr328/clash/core/serialization/Parcels.kt +++ b/core/src/main/java/com/github/kr328/clash/core/serialization/Parcels.kt @@ -1,12 +1,9 @@ package com.github.kr328.clash.core.serialization import android.os.Parcel -import com.github.kr328.clash.core.utils.Log import kotlinx.serialization.* import kotlinx.serialization.modules.EmptyModule import kotlinx.serialization.modules.SerialModule -import java.lang.IllegalArgumentException -import java.lang.NullPointerException object Parcels : AbstractSerialFormat(EmptyModule) { fun dump(serializer: SerializationStrategy, obj: T, parcel: Parcel) { @@ -33,33 +30,44 @@ object Parcels : AbstractSerialFormat(EmptyModule) { override fun encodeBooleanElement(desc: SerialDescriptor, index: Int, value: Boolean) = encodeBoolean(value) + override fun encodeByteElement(desc: SerialDescriptor, index: Int, value: Byte) = encodeByte(value) + override fun encodeCharElement(desc: SerialDescriptor, index: Int, value: Char) = encodeChar(value) + override fun encodeDoubleElement(desc: SerialDescriptor, index: Int, value: Double) = encodeDouble(value) + override fun encodeFloatElement(desc: SerialDescriptor, index: Int, value: Float) = encodeFloat(value) + override fun encodeIntElement(desc: SerialDescriptor, index: Int, value: Int) = encodeInt(value) + override fun encodeLongElement(desc: SerialDescriptor, index: Int, value: Long) = encodeLong(value) + override fun encodeShortElement(desc: SerialDescriptor, index: Int, value: Short) = encodeShort(value) + override fun encodeStringElement(desc: SerialDescriptor, index: Int, value: String) = encodeString(value) + override fun encodeUnitElement(desc: SerialDescriptor, index: Int) = encodeUnit() override fun encodeNonSerializableElement(desc: SerialDescriptor, index: Int, value: Any) = throw IllegalArgumentException("Unsupported") + override fun encodeNullableSerializableElement( desc: SerialDescriptor, index: Int, serializer: SerializationStrategy, value: T? ) = encodeNullableSerializableValue(serializer, value) + override fun encodeSerializableElement( desc: SerialDescriptor, index: Int, @@ -73,29 +81,41 @@ object Parcels : AbstractSerialFormat(EmptyModule) { ): CompositeEncoder = this override fun encodeBoolean(value: Boolean) = - parcel.writeByte(if ( value ) 1 else 0) + parcel.writeByte(if (value) 1 else 0) + override fun encodeByte(value: Byte) = parcel.writeByte(value) + override fun encodeChar(value: Char) = parcel.writeInt(value.toInt()) + override fun encodeDouble(value: Double) = parcel.writeDouble(value) + override fun encodeEnum(enumDescription: SerialDescriptor, ordinal: Int) = parcel.writeInt(ordinal) + override fun encodeFloat(value: Float) = parcel.writeFloat(value) + override fun encodeInt(value: Int) = parcel.writeInt(value) + override fun encodeLong(value: Long) = parcel.writeLong(value) + override fun encodeNotNullMark() = encodeBoolean(true) + override fun encodeNull() = encodeBoolean(false) + override fun encodeShort(value: Short) = parcel.writeInt(value.toInt()) + override fun encodeString(value: String) = parcel.writeString(value) + override fun encodeUnit() {} } @@ -107,27 +127,37 @@ object Parcels : AbstractSerialFormat(EmptyModule) { override fun decodeElementIndex(desc: SerialDescriptor) = CompositeDecoder.READ_ALL + override fun decodeCollectionSize(desc: SerialDescriptor) = decodeInt() override fun decodeBooleanElement(desc: SerialDescriptor, index: Int) = decodeBoolean() + override fun decodeByteElement(desc: SerialDescriptor, index: Int) = decodeByte() + override fun decodeCharElement(desc: SerialDescriptor, index: Int) = decodeChar() + override fun decodeDoubleElement(desc: SerialDescriptor, index: Int) = decodeDouble() + override fun decodeFloatElement(desc: SerialDescriptor, index: Int) = decodeFloat() + override fun decodeIntElement(desc: SerialDescriptor, index: Int) = decodeInt() + override fun decodeShortElement(desc: SerialDescriptor, index: Int) = decodeShort() + override fun decodeLongElement(desc: SerialDescriptor, index: Int) = decodeLong() + override fun decodeStringElement(desc: SerialDescriptor, index: Int) = decodeString() + override fun decodeUnitElement(desc: SerialDescriptor, index: Int) = decodeUnit() @@ -136,6 +166,7 @@ object Parcels : AbstractSerialFormat(EmptyModule) { index: Int, deserializer: DeserializationStrategy ) = decodeNullableSerializableValue(deserializer) + override fun decodeSerializableElement( desc: SerialDescriptor, index: Int, @@ -163,28 +194,40 @@ object Parcels : AbstractSerialFormat(EmptyModule) { override fun decodeBoolean() = parcel.readByte() != 0.toByte() + override fun decodeByte() = parcel.readByte() + override fun decodeChar() = parcel.readInt().toChar() + override fun decodeDouble() = parcel.readDouble() + override fun decodeEnum(enumDescription: SerialDescriptor) = parcel.readInt() + override fun decodeFloat() = parcel.readFloat() + override fun decodeInt() = parcel.readInt() + override fun decodeLong() = parcel.readLong() + override fun decodeNotNullMark() = decodeBoolean() + override fun decodeNull() = null + override fun decodeShort() = parcel.readInt().toShort() + override fun decodeString() = parcel.readString() ?: throw NullPointerException("String null") + override fun decodeUnit() {} } } diff --git a/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt index 0f3a77cd6e..de56a4bba9 100644 --- a/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt +++ b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt @@ -1,12 +1,11 @@ package com.github.kr328.clash.core.transact import bridge.DoneCallback -import java.lang.Exception import java.util.concurrent.CompletableFuture class DoneCallbackImpl : DoneCallback, CompletableFuture() { override fun doneWithError(e: Exception?) { - if ( e == null ) + if (e == null) complete(Unit) else completeExceptionally(e) diff --git a/core/src/main/java/com/github/kr328/clash/core/transact/ProxyCollections.kt b/core/src/main/java/com/github/kr328/clash/core/transact/ProxyCollections.kt index ecbe6674d4..b43db5fc1f 100644 --- a/core/src/main/java/com/github/kr328/clash/core/transact/ProxyCollections.kt +++ b/core/src/main/java/com/github/kr328/clash/core/transact/ProxyCollections.kt @@ -1,7 +1,10 @@ package com.github.kr328.clash.core.transact -import bridge.* +import bridge.ProxyCollection +import bridge.ProxyGroupCollection +import bridge.ProxyGroupItem +import bridge.ProxyItem import java.util.* class ProxyCollectionImpl : LinkedList(), ProxyCollection -class ProxyGroupCollectionImpl: LinkedList(), ProxyGroupCollection \ No newline at end of file +class ProxyGroupCollectionImpl : LinkedList(), ProxyGroupCollection \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/utils/ByteFormatter.kt b/core/src/main/java/com/github/kr328/clash/core/utils/ByteFormatter.kt index bcb09ee62e..4e332e18c1 100644 --- a/core/src/main/java/com/github/kr328/clash/core/utils/ByteFormatter.kt +++ b/core/src/main/java/com/github/kr328/clash/core/utils/ByteFormatter.kt @@ -17,4 +17,12 @@ object ByteFormatter { fun byteToStringSecond(bytes: Long): String { return byteToString(bytes) + "/s" } +} + +fun Long.asBytesString(): String { + return ByteFormatter.byteToString(this) +} + +fun Long.asSpeedString(): String { + return ByteFormatter.byteToStringSecond(this) } \ No newline at end of file diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index 5ca8d4afa8..96459c561b 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -20,9 +20,20 @@ - + + + diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index 7c0ad2b4ec..f323ca08dc 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -1,13 +1,10 @@ package com.github.kr328.clash.service; -import com.github.kr328.clash.service.IClashProfileManager; import com.github.kr328.clash.service.ipc.IStreamCallback; +import com.github.kr328.clash.service.data.ClashProfileEntity; import com.github.kr328.clash.core.model.Packet; interface IClashManager { - // Profile - IClashProfileManager getProfileManager(); - // Control boolean setSelectProxy(String proxy, String selected); void startHealthCheck(String group, IStreamCallback callback); @@ -16,6 +13,7 @@ interface IClashManager { ProxyGroup[] queryAllProxies(); General queryGeneral(); long queryBandwidth(); + ClashProfileEntity[] queryAllProfiles(); // Events void openLogEvent(IStreamCallback callback); diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl deleted file mode 100644 index 7a18d50bee..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashProfileManager.aidl +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.kr328.clash.service; - -import com.github.kr328.clash.service.data.ClashProfileEntity; -import com.github.kr328.clash.service.ipc.IStreamCallback; - -interface IClashProfileManager { - void addProfile(String name, int type, String uri, IStreamCallback callback); - void updateProfile(int id, IStreamCallback callback); - ClashProfileEntity[] queryAllProfiles(); -} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/transact/ProfileRequest.aidl b/service/src/main/aidl/com/github/kr328/clash/service/transact/ProfileRequest.aidl new file mode 100644 index 0000000000..2d518628ae --- /dev/null +++ b/service/src/main/aidl/com/github/kr328/clash/service/transact/ProfileRequest.aidl @@ -0,0 +1,3 @@ +package com.github.kr328.clash.service.transact; + +parcelable ProfileRequest; \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index 5999860cc4..f24528af63 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -6,20 +6,13 @@ import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.ProxyGroup import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.ipc.IStreamCallback -import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture import com.github.kr328.clash.service.ipc.ParcelableContainer -import com.github.kr328.clash.service.ipc.ParcelablePipe -import java.lang.Exception class ClashManager(private val context: Context) : IClashManager.Stub() { private val settings = context.getSharedPreferences("service", Context.MODE_PRIVATE) - override fun getProfileManager(): IClashProfileManager { - return ClashProfileManager(context, ClashDatabase.getInstance(context)) - } - - override fun queryAllProxies(): Array { return Clash.queryProxyGroups().toTypedArray() } @@ -41,6 +34,10 @@ class ClashManager(private val context: Context) : IClashManager.Stub() { return true } + override fun queryAllProfiles(): Array { + return ClashDatabase.getInstance(context).openClashProfileDao().queryProfiles() + } + override fun queryBandwidth(): Long { return Clash.queryBandwidth() } @@ -52,8 +49,7 @@ class ClashManager(private val context: Context) : IClashManager.Stub() { onEvent { try { callback.send(ParcelableContainer(it)) - } - catch (e: Exception) { + } catch (e: Exception) { close() } } @@ -64,7 +60,7 @@ class ClashManager(private val context: Context) : IClashManager.Stub() { require(group != null && callback != null) Clash.startHealthCheck(group).whenComplete { _, u -> - if ( u != null ) + if (u != null) callback.completeExceptionally(u.message) else callback.complete() diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 660586fc03..81170846d3 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -1,30 +1,33 @@ package com.github.kr328.clash.service import android.app.* -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.Build import android.os.Handler import android.os.PowerManager import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.core.event.EventStream -import com.github.kr328.clash.core.event.TrafficEvent -import com.github.kr328.clash.core.utils.ByteFormatter +import com.github.kr328.clash.core.utils.asSpeedString class ClashNotification(private val context: Service) { companion object { private const val CLASH_STATUS_NOTIFICATION_CHANNEL = "clash_status_channel" private const val CLASH_STATUS_NOTIFICATION_ID = 413 - - private const val MAIN_ACTIVITY_NAME = ".MainActivity" } private val handler = Handler() private var showing = false + private val contentIntent = Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_LAUNCHER) + .setPackage(context.packageName) + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) private val baseBuilder = NotificationCompat.Builder(context, CLASH_STATUS_NOTIFICATION_CHANNEL) - .setSmallIcon(R.drawable.ic_notification_icon) + .setSmallIcon(R.drawable.ic_notification) .setOngoing(true) .setColor(context.getColor(R.color.colorAccentService)) .setOnlyAlertOnce(true) @@ -32,24 +35,15 @@ class ClashNotification(private val context: Service) { .setContentIntent( PendingIntent.getActivity( context, - (Math.random() * 100).toInt(), - Intent().setComponent( - ComponentName.createRelative( - context, - MAIN_ACTIVITY_NAME - ) - ).setFlags( - Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - ), + CLASH_STATUS_NOTIFICATION_ID, + contentIntent, PendingIntent.FLAG_UPDATE_CURRENT ) ) + private var auto = false private var vpn = false - private var up = 0L - private var down = 0L private var profile = "None" - private var traffic: EventStream? = null private val observer = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { @@ -68,7 +62,7 @@ class ClashNotification(private val context: Service) { NotificationChannel( CLASH_STATUS_NOTIFICATION_CHANNEL, context.getString(R.string.clash_service_status_channel), - NotificationManager.IMPORTANCE_MIN + NotificationManager.IMPORTANCE_LOW ) ) } @@ -116,35 +110,31 @@ class ClashNotification(private val context: Service) { } } - private fun setSpeed(up: Long, down: Long) { - handler.post { - this.up = up - this.down = down + private fun updateSpeed() { + handler.postDelayed({ + if (!auto) + return@postDelayed update() - } + + updateSpeed() + }, 1000) } private fun enableUpdate() { handler.post { - if (traffic != null) + if (auto) return@post - traffic = Clash.openTrafficEvent().apply { - onEvent { - setSpeed(it.up, it.down) - } - } + auto = true + + updateSpeed() } } private fun disableUpdate() { handler.post { - if (traffic == null) - return@post - - traffic?.close() - traffic = null + auto = false } } @@ -154,13 +144,15 @@ class ClashNotification(private val context: Service) { } private fun createNotification(): Notification { + val traffic = Clash.queryTrafficEvent() + return baseBuilder .setContentTitle(profile) .setContentText( context.getString( R.string.clash_notification_content, - ByteFormatter.byteToStringSecond(up), - ByteFormatter.byteToStringSecond(down) + traffic.upload.asSpeedString(), + traffic.download.asSpeedString() ) ) .setSubText(if (vpn) context.getText(R.string.clash_service_vpn_mode) else null) diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt deleted file mode 100644 index 1a1ebc543c..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileManager.kt +++ /dev/null @@ -1,125 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.Context -import android.content.Intent -import android.net.Uri -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.service.data.ClashDatabase -import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.service.ipc.IStreamCallback -import com.github.kr328.clash.service.ipc.ParcelableCompletedFuture -import com.github.kr328.clash.service.util.DefaultThreadPool -import com.github.kr328.clash.service.util.FileUtils -import com.github.kr328.clash.service.util.sendBroadcastSelf - -class ClashProfileManager(private val context: Context, private val database: ClashDatabase) : - IClashProfileManager.Stub() { - private val clashDir = context.filesDir.resolve(Constants.CLASH_DIR) - private val profileDir = context.filesDir.resolve(Constants.PROFILES_DIR) - - override fun updateProfile(id: Int, callback: IStreamCallback?) { - val entity = database.openClashProfileDao().queryProfileById(id) - - require( - entity != null && callback != null && (entity.type == ClashProfileEntity.TYPE_URL || - entity.type == ClashProfileEntity.TYPE_FILE) - ) - - downloadProfile(Uri.parse(entity.uri), entity.file, entity.base, - onSuccess = { - callback.complete() - - database.openClashProfileDao().touchProfile(id) - - sendChangedBroadcast() - }, - onFailure = { - callback.completeExceptionally(it.message) - }) - } - - override fun queryAllProfiles(): Array { - return database.openClashProfileDao().queryProfiles() - } - - override fun addProfile(name: String, type: Int, uri: String?, callback: IStreamCallback?) { - require(uri != null && callback != null && (uri.startsWith("http") || uri.startsWith("content"))) - - val fileName = FileUtils.generateRandomFileName(profileDir, ".yaml") - val baseDirName = FileUtils.generateRandomFileName(clashDir) - - downloadProfile(Uri.parse(uri), fileName, baseDirName, - onSuccess = { - database.openClashProfileDao().addProfile( - ClashProfileEntity( - name, - type, - uri, - fileName, - baseDirName, - false, - System.currentTimeMillis() - ) - ) - - sendChangedBroadcast() - - callback.complete() - }, - onFailure = { - callback.completeExceptionally(it.message) - }) - } - - private fun downloadProfile( - uri: Uri?, - fileName: String, - baseDirName: String, - onSuccess: () -> Unit, - onFailure: (Throwable) -> Unit - ) { - require(uri != null && uri != Uri.EMPTY) - - if (uri.scheme == "http" || uri.scheme == "https") { - Clash.downloadProfile( - uri.toString(), - profileDir.resolve(fileName), - clashDir.resolve(baseDirName) - ) - .whenComplete { _, u -> - if (u != null) - onFailure(u) - else - onSuccess() - } - } else { - DefaultThreadPool.submit { - try { - val input = context.contentResolver.openInputStream(uri) - ?: throw NullPointerException("Unable to open profile") - - input.use { - Clash.saveProfile( - it.readBytes(), - profileDir.resolve(fileName), - clashDir.resolve(baseDirName) - ) - } - - onSuccess() - } catch (e: Exception) { - onFailure(e) - } - } - } - } - - private fun sendChangedBroadcast() { - val active = database.openClashProfileDao().queryActiveProfile() - - context.sendBroadcastSelf( - Intent(Intents.INTENT_ACTION_PROFILE_CHANGED) - .putExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE, active) - ) - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileReceiver.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileReceiver.kt new file mode 100644 index 0000000000..7a1430667b --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileReceiver.kt @@ -0,0 +1,21 @@ +package com.github.kr328.clash.service + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.os.Build +import com.github.kr328.clash.service.util.componentName + +class ClashProfileReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action != Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST || context == null) + return + + intent.component = ClashProfileService::class.componentName + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + context.startForegroundService(intent) + else + context.startService(intent) + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt new file mode 100644 index 0000000000..c3dac0b983 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt @@ -0,0 +1,301 @@ +package com.github.kr328.clash.service + +import android.app.* +import android.content.Intent +import android.net.Uri +import android.os.Binder +import android.os.Build +import android.os.IBinder +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.data.ClashProfileDao +import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.transact.ProfileRequest +import com.github.kr328.clash.service.util.* +import java.io.File +import java.io.FileNotFoundException +import java.util.concurrent.LinkedBlockingQueue +import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread + +class ClashProfileService : Service() { + companion object { + private const val SERVICE_STATUS_CHANNEL = "profile_service_status" + private const val SERVICE_RESULT_CHANNEL = "profile_service_result" + private const val SERVICE_NOTIFICATION_ID = 10000 + } + + private val requestQueue = LinkedBlockingQueue() + + val service: ClashProfileService + get() = this + val profiles: ClashProfileDao by lazy { + ClashDatabase.getInstance(service).openClashProfileDao() + } + + override fun onCreate() { + super.onCreate() + + createNotificationChannels() + + updateNotificationWaiting() + + thread { + while (true) { + val request = try { + updateNotificationWaiting() + requestQueue.poll(60, TimeUnit.SECONDS) ?: break + } catch (e: InterruptedException) { + break + } + + handleRequest(request) + } + + stopSelf() + } + } + + override fun onBind(intent: Intent?): IBinder? { + return Binder() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + + when (intent?.action) { + Intents.INTENT_ACTION_PROFILE_SETUP -> { + resetProfileUpdateAlarm() + } + Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST -> { + val request = + intent.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST) + if (request != null) + enqueueRequest(request) + } + } + + return START_NOT_STICKY + } + + override fun onDestroy() { + stopForeground(true) + + super.onDestroy() + } + + private fun createNotificationChannels() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + return + + NotificationManagerCompat.from(this).createNotificationChannels( + listOf( + NotificationChannel( + SERVICE_STATUS_CHANNEL, + getText(R.string.profile_service_status_channel), + NotificationManager.IMPORTANCE_LOW + ), + NotificationChannel( + SERVICE_RESULT_CHANNEL, + getText(R.string.profile_service_result), + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + ) + } + + private fun createServiceNotification(content: CharSequence): Notification { + return NotificationCompat.Builder(this, SERVICE_STATUS_CHANNEL) + .setContentTitle(getText(R.string.profile_service_status_title)) + .setContentText(content) + .setSmallIcon(R.drawable.ic_updating) + .setOnlyAlertOnce(true) + .setProgress(Int.MAX_VALUE, 0, true) + .build() + } + + private fun createResultNotification(success: Boolean, content: CharSequence): Notification { + return NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) + .setContentTitle(getText(R.string.profile_process_result)) + .setContentText(content) + .setSmallIcon(if (success) R.drawable.ic_update_completed else R.drawable.ic_update_failure) + .build() + } + + private fun updateNotificationWaiting() { + startForeground( + SERVICE_NOTIFICATION_ID, + createServiceNotification(getText(R.string.profile_service_status_waiting)) + ) + } + + private fun updateNotificationUpdating(profileName: String) { + createServiceNotification( + getString( + R.string.profile_service_status_updating, + profileName + ) + ) + } + + private fun notifyResultNotification(success: Boolean, content: CharSequence) { + NotificationManagerCompat.from(this) + .notify(RandomUtils.nextInt(), createResultNotification(success, content)) + } + + private fun enqueueRequest(request: ProfileRequest) { + requestQueue.offer(request) + } + + private fun handleRequest(request: ProfileRequest) { + when (request.action) { + ProfileRequest.Action.UPDATE_OR_CREATE -> + handleUpdateOrCreate(request) + ProfileRequest.Action.REMOVE -> + removeProfile(request) + } + + sendBroadcastSelf( + Intent(Intents.INTENT_ACTION_PROFILE_CHANGED) + .putExtra( + Intents.INTENT_EXTRA_PROFILE_ACTIVE, + profiles.queryActiveProfile() + ) + ) + } + + private fun handleUpdateOrCreate(request: ProfileRequest) { + val id = request.id ?: 0 + + var entity: ClashProfileEntity? + + try { + if (id == 0L) { + entity = ClashProfileEntity( + requireNotNull(request.name), + requireNotNull(request.type), + requireNotNull(request.url), + RandomUtils.fileName(profileDir, ".yaml"), + RandomUtils.fileName(clashDir), + false, + request.interval ?: 0, + 0 + ) + } else { + entity = + profiles.queryProfileById(id) ?: throw NullPointerException("Profile not found") + + if (request.name != null) + entity = entity.copy(name = requireNotNull(request.name)) + + if (request.url != null) + entity = entity.copy(uri = requireNotNull(request.url)) + + if (request.interval != null) + entity = entity.copy(updateInterval = requireNotNull(request.interval)) + } + } catch (e: Exception) { + notifyResultNotification( + false, + getString(R.string.profile_update_failure, "ID:$id") + ) + return + } + + updateNotificationUpdating(entity.name) + + try { + val url = Uri.parse(entity.uri) + + if (url == null || url == Uri.EMPTY) + throw IllegalArgumentException("Invalid url $url") + + downloadProfile(url, profileDir.resolve(entity.file), clashDir.resolve(entity.base)) + + entity = entity.copy(lastUpdate = System.currentTimeMillis()) + + val newId = if (entity.id == 0L) + profiles.getId(profiles.addProfile(entity)) + else + profiles.updateProfile(entity).run { entity.id } + + if (entity.updateInterval > 0) { + val nextRequest = + ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE).withId(newId) + + requireNotNull(getSystemService(AlarmManager::class.java)).set( + AlarmManager.RTC, + entity.lastUpdate + entity.updateInterval, + PendingIntent.getBroadcast( + this, + RandomUtils.nextInt(), + Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) + .setComponent(ClashProfileReceiver::class.componentName) + .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, nextRequest), + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + } + + notifyResultNotification( + true, + getString(R.string.profile_update_completed, entity.name) + ) + } catch (e: Exception) { + notifyResultNotification( + false, + getString(R.string.profile_update_failure, entity.name) + ) + throw e + } + } + + private fun removeProfile(request: ProfileRequest) { + val entity = profiles.queryProfileById(request.id ?: return) ?: return + + clashDir.resolve(entity.base).deleteRecursively() + profileDir.resolve(entity.file).delete() + + profiles.removeProfile(entity.id) + + notifyResultNotification(true, getString(R.string.profile_deleted, entity.name)) + } + + private fun resetProfileUpdateAlarm() { + DefaultThreadPool.submit { + for (entity in profiles.queryProfiles()) { + if (entity.updateInterval <= 0) continue + + val nextRequest = + ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE) + .withId(entity.id) + + requireNotNull(getSystemService(AlarmManager::class.java)).set( + AlarmManager.RTC, + entity.lastUpdate + entity.updateInterval, + PendingIntent.getBroadcast( + this, + RandomUtils.nextInt(), + Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) + .setComponent(ClashProfileReceiver::class.componentName) + .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, nextRequest), + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + } + } + } + + private fun downloadProfile(source: Uri, target: File, baseDir: File) { + if (source.scheme == "content" || source.scheme == "file") { + val fd = contentResolver.openFileDescriptor(source, "r") + ?: throw FileNotFoundException("Unable to open file $source") + + Clash.copyProfile(fd.fd, target, baseDir) + } else { + Clash.downloadProfile(source.toString(), target, baseDir) + } + } +} \ No newline at end of file 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 682e981d63..c562edcb93 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 @@ -7,6 +7,8 @@ import android.os.IBinder import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.util.DefaultThreadPool +import com.github.kr328.clash.service.util.clashDir +import com.github.kr328.clash.service.util.profileDir import com.github.kr328.clash.service.util.sendBroadcastSelf class ClashService : Service() { @@ -99,11 +101,13 @@ class ClashService : Service() { ?: return@submit stopSelf("Empty active profile") Clash.loadProfile( - filesDir.resolve(Constants.PROFILES_DIR).resolve(active.file), - filesDir.resolve(Constants.CLASH_DIR).resolve(active.base) + profileDir.resolve(active.file), + clashDir.resolve(active.base) ).whenComplete { _, u -> if (u != null) return@whenComplete stopSelf(u.message ?: "Load profile failure") + else + notification.setProfile(active.name) } } } diff --git a/service/src/main/java/com/github/kr328/clash/service/Constants.kt b/service/src/main/java/com/github/kr328/clash/service/Constants.kt index b47169d775..a0a16428c0 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Constants.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Constants.kt @@ -1,11 +1,6 @@ package com.github.kr328.clash.service object Constants { - const val CLASH_PROCESS_BROADCAST_ACTION = - "com.github.kr328.clash.ClashService.ClashProcessEvent" - const val CLASH_RELOAD_BROADCAST_ACTION = - "com.github.kr328.clash.ClashService.ProfileReloadEvent" - const val CLASH_DIR = "clash" const val PROFILES_DIR = "profiles" } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/Intents.kt b/service/src/main/java/com/github/kr328/clash/service/Intents.kt index 551fe8f587..e1d487dbff 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Intents.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Intents.kt @@ -9,9 +9,15 @@ object Intents { "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.stopped" const val INTENT_ACTION_PROFILE_CHANGED = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.changed" + const val INTENT_ACTION_PROFILE_ENQUEUE_REQUEST = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.enqueue.request" + const val INTENT_ACTION_PROFILE_SETUP = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.setup" const val INTENT_EXTRA_CLASH_STOP_REASON = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason" const val INTENT_EXTRA_PROFILE_ACTIVE = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.active.name" + const val INTENT_EXTRA_PROFILE_REQUEST = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.request" } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt index 4e1f31f59c..3972461b89 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabase.kt @@ -1,7 +1,9 @@ package com.github.kr328.clash.service.data import android.content.Context -import androidx.room.* +import androidx.room.Database +import androidx.room.Room +import androidx.room.RoomDatabase @Database( version = 2, diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt index 6e0f6da4e3..ecd28776bb 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt @@ -42,8 +42,8 @@ object ClashDatabaseMigrations { // new val type = when { - token.startsWith("url") -> ClashProfileEntity.TYPE_URL - token.startsWith("file") -> ClashProfileEntity.TYPE_FILE + token.startsWith("url") -> ClashProfileEntity.TYPE_REMOTE + token.startsWith("file") -> ClashProfileEntity.TYPE_LOCAL else -> ClashProfileEntity.TYPE_UNKNOWN } val uri = token.removePrefix("url|").removePrefix("file|") diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt index f55744b5c0..8219106f2f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt @@ -1,14 +1,11 @@ package com.github.kr328.clash.service.data -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query +import androidx.room.* @Dao interface ClashProfileDao { @Query("UPDATE profiles SET active = CASE WHEN id = :id THEN 1 ELSE 0 END") - fun setActiveProfile(id: Int) + fun setActiveProfile(id: Long) @Query("SELECT * FROM profiles WHERE active = 1 LIMIT 1") fun queryActiveProfile(): ClashProfileEntity? @@ -17,14 +14,17 @@ interface ClashProfileDao { fun queryProfiles(): Array @Query("SELECT * FROM profiles WHERE id = :id") - fun queryProfileById(id: Int): ClashProfileEntity? + fun queryProfileById(id: Long): ClashProfileEntity? - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun addProfile(profile: ClashProfileEntity) + @Insert(onConflict = OnConflictStrategy.ABORT) + fun addProfile(profile: ClashProfileEntity): Long + + @Update(onConflict = OnConflictStrategy.ABORT) + fun updateProfile(profile: ClashProfileEntity) @Query("DELETE FROM profiles WHERE id = :id") - fun removeProfile(id: Int) + fun removeProfile(id: Long) - @Query("UPDATE profiles SET last_update = :lastUpdate WHERE id = :id") - fun touchProfile(id: Int, lastUpdate: Long = System.currentTimeMillis()) + @Query("SELECT id FROM profiles WHERE rowId = :rowId") + fun getId(rowId: Long): Long } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index 39f8ff7feb..461b19e4de 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -5,7 +5,6 @@ import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey -import androidx.room.TypeConverter import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable @@ -19,7 +18,8 @@ data class ClashProfileEntity( @ColumnInfo(name = "base") val base: String, @ColumnInfo(name = "active") val active: Boolean, @ColumnInfo(name = "last_update") val lastUpdate: Long, - @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Int = 0 + @ColumnInfo(name = "update_interval") val updateInterval: Long, + @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Long = 0 ) : Parcelable { override fun writeToParcel(parcel: Parcel, flags: Int) { Parcels.dump(serializer(), this, parcel) @@ -30,8 +30,8 @@ data class ClashProfileEntity( } companion object { - const val TYPE_FILE = 1 - const val TYPE_URL = 2 + const val TYPE_LOCAL = 1 + const val TYPE_REMOTE = 2 const val TYPE_UNKNOWN = -1 @JvmField diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt deleted file mode 100644 index 1df81319df..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableCompletedFuture.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.github.kr328.clash.service.ipc - -import android.os.Parcel -import android.os.Parcelable -import android.os.RemoteException -import java.util.concurrent.CompletableFuture - -class ParcelableCompletedFuture private constructor(private val pipe: ParcelablePipe) : - CompletableFuture(), Parcelable { - constructor() : this(ParcelablePipe()) - constructor(parcel: Parcel) : this( - parcel.readParcelable( - ParcelableCompletedFuture::class.java.classLoader - ) ?: throw NullPointerException() - ) { - pipe.onReceive { - val result = (it ?: throw NullPointerException()) as ParcelableResult - - if (result.exception != null) { - completeExceptionally(RemoteException(result.exception)) - } else { - complete(result.data) - } - } - } - - override fun complete(value: Parcelable?): Boolean { - if (super.complete(value)) { - pipe.sendRemote(ParcelableResult(value, null)) - return true - } - return false - } - - override fun completeExceptionally(ex: Throwable?): Boolean { - if (super.completeExceptionally(ex)) { - pipe.sendRemote(ParcelableResult(null, ex?.message ?: "Unknown")) - return true - } - return false - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeParcelable(pipe, 0) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ParcelableCompletedFuture { - return ParcelableCompletedFuture(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableContainer.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableContainer.kt index 000528d0d7..579546fd3b 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableContainer.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableContainer.kt @@ -4,7 +4,7 @@ import android.os.Parcel import android.os.Parcelable data class ParcelableContainer(val data: Parcelable?) : Parcelable { - constructor(parcel: Parcel): + constructor(parcel: Parcel) : this(parcel.readParcelable(ParcelableContainer::class.java.classLoader)) override fun writeToParcel(parcel: Parcel, flags: Int) { diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt deleted file mode 100644 index 1f5bd3b8dc..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelablePipe.kt +++ /dev/null @@ -1,116 +0,0 @@ -package com.github.kr328.clash.service.ipc - -import android.os.Binder -import android.os.IBinder -import android.os.Parcel -import android.os.Parcelable - -open class ParcelablePipe private constructor(val type: Type) : Parcelable { - enum class Type { - MASTER, SLAVE - } - - private inner class Slave : Binder() { - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - if (code == TRANSACT_CODE_SEND_PARCELABLE) { - receiveCallback(data.readParcelable(ParcelablePipe::class.java.classLoader)) - return true - } - return false - } - } - - private inner class Master : Binder() { - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - if (code == TRANSACT_CODE_SET_SLAVE) { - connection = data.readStrongBinder() - return true - } - if (code == TRANSACT_CODE_CLOSE) { - onClose() - } - return false - } - } - - private var connection: IBinder? = null - private var receiveCallback: (Parcelable?) -> Unit = {} - - constructor() : this(Type.MASTER) - - constructor(parcel: Parcel) : this(Type.SLAVE) { - val master = parcel.readStrongBinder() - val data = Parcel.obtain() - - try { - data.writeStrongBinder(Slave()) - - master.transact(TRANSACT_CODE_SET_SLAVE, data, null, 0) - - connection = master - } finally { - data.recycle() - } - } - - override fun writeToParcel(dest: Parcel?, flags: Int) { - if (dest == null) - return - - dest.writeStrongBinder(Master()) - } - - override fun describeContents(): Int = 0 - - fun sendRemote(parcelable: Parcelable?): Boolean { - if (type != Type.MASTER) - return false - - val s = connection ?: return false - val data = Parcel.obtain() - - try { - data.writeParcelable(parcelable, 0) - - s.transact(TRANSACT_CODE_SEND_PARCELABLE, data, null, 0) - } finally { - data.recycle() - } - - return true - } - - fun close() { - val s = connection ?: return - val data = Parcel.obtain() - - try { - s.transact(TRANSACT_CODE_CLOSE, data, null, 0) - } finally { - data.recycle() - } - } - - fun onReceive(callback: (Parcelable?) -> Unit) { - this.receiveCallback = callback - } - - open fun onClose() {} - - companion object { - private const val TRANSACT_CODE_SET_SLAVE = 1 - private const val TRANSACT_CODE_SEND_PARCELABLE = 2 - private const val TRANSACT_CODE_CLOSE = 3 - - @JvmField - val CREATOR = object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ParcelablePipe { - return ParcelablePipe(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt b/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt deleted file mode 100644 index f73cc1f877..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ipc/ParcelableResult.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.kr328.clash.service.ipc - -import android.os.Parcel -import android.os.Parcelable - -data class ParcelableResult(val data: Parcelable?, val exception: String?) : Parcelable { - constructor(parcel: Parcel) : this( - parcel.readParcelable(ParcelableResult::class.java.classLoader), - parcel.readString() - ) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeParcelable(data, 0) - parcel.writeString(exception) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ParcelableResult { - return ParcelableResult(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt new file mode 100644 index 0000000000..90651d2a3e --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt @@ -0,0 +1,123 @@ +package com.github.kr328.clash.service.transact + +import android.net.Uri +import android.os.Bundle +import android.os.Parcel +import android.os.Parcelable +import com.github.kr328.clash.service.ipc.IStreamCallback + +class ProfileRequest private constructor(private val bundle: Bundle) : Parcelable { + constructor() : this(Bundle()) + constructor(parcel: Parcel) : this( + parcel.readBundle(ProfileRequest::class.java.classLoader) + ?: throw NullPointerException("Empty bundle") + ) + + enum class Action { + UPDATE_OR_CREATE, REMOVE + } + + val action: Action + get() = Action.valueOf(requireNotNull(bundle.getString(KEY_ACTION))) + val type: Int? + get() = bundle.getInt(KEY_TYPE) + val id: Long? + get() = bundle.getLong(KEY_ID) + val name: String? + get() = bundle.getString(KEY_NAME) + val url: String? + get() = bundle.getString(KEY_URL) + val interval: Long? + get() = bundle.getLong(KEY_UPDATE_INTERVAL) + val callback: IStreamCallback? + get() = IStreamCallback.Stub.asInterface(bundle.getBinder(KEY_CALLBACK)) + + fun action(action: Action): ProfileRequest { + return apply { + bundle.putString(KEY_ACTION, action.toString()) + } + } + + fun withType(type: Int): ProfileRequest { + return apply { + bundle.putInt(KEY_TYPE, type) + } + } + + fun withId(id: Long): ProfileRequest { + return apply { + bundle.putLong(KEY_ID, id) + } + } + + fun withName(name: String): ProfileRequest { + return apply { + bundle.putString(KEY_NAME, name) + } + } + + fun withURL(url: Uri): ProfileRequest { + return apply { + bundle.putString(KEY_URL, url.toString()) + } + } + + fun withUpdateInterval(interval: Long): ProfileRequest { + return apply { + bundle.putLong(KEY_UPDATE_INTERVAL, interval) + } + } + + fun withCallback(callback: IStreamCallback.Stub): ProfileRequest { + return apply { + bundle.putBinder(KEY_CALLBACK, callback) + } + } + + override fun equals(other: Any?): Boolean { + if (other !is ProfileRequest) + return false + + for (key in bundle.keySet()) { + if (bundle.get(key) != other.bundle.get(key)) + return false + } + + return true + } + + override fun hashCode(): Int { + return bundle.keySet() + .joinToString { it + bundle.get(it)?.hashCode() } + .hashCode() + } + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeBundle(bundle) + } + + override fun describeContents(): Int { + return 0 + } + + companion object { + private const val KEY_ACTION = "action" + private const val KEY_ID = "id" + private const val KEY_NAME = "name" + private const val KEY_URL = "url" + private const val KEY_TYPE = "type" + private const val KEY_CALLBACK = "callback" + private const val KEY_UPDATE_INTERVAL = "update_interval" + + @JvmField + val CREATOR = object : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ProfileRequest { + return ProfileRequest(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/ComponentUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/ComponentUtils.kt new file mode 100644 index 0000000000..5654d4eb7c --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/ComponentUtils.kt @@ -0,0 +1,8 @@ +package com.github.kr328.clash.service.util + +import android.content.ComponentName +import com.github.kr328.clash.core.Global +import kotlin.reflect.KClass + +val KClass<*>.componentName: ComponentName + get() = ComponentName.createRelative(Global.application, this.java.name) \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt index aecba39fff..b6d3618142 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt @@ -1,21 +1,10 @@ package com.github.kr328.clash.service.util +import android.content.Context +import com.github.kr328.clash.service.Constants import java.io.File -import java.security.SecureRandom -import kotlin.math.absoluteValue -object FileUtils { - private val random = SecureRandom() - - fun generateRandomFileName(dir: File, suffix: String = ""): String { - dir.mkdirs() - - var fileName: String - - do { - fileName = random.nextLong().absoluteValue.toString() + suffix - } while (dir.resolve(fileName).exists()) - - return fileName - } -} \ No newline at end of file +val Context.profileDir: File + get() = this.filesDir.resolve(Constants.PROFILES_DIR) +val Context.clashDir: File + get() = this.filesDir.resolve(Constants.CLASH_DIR) \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/RandomUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/RandomUtils.kt new file mode 100644 index 0000000000..3a57f791de --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/RandomUtils.kt @@ -0,0 +1,25 @@ +package com.github.kr328.clash.service.util + +import java.io.File +import java.security.SecureRandom +import kotlin.math.absoluteValue + +object RandomUtils { + private val random = SecureRandom() + + fun fileName(dir: File, suffix: String = ""): String { + dir.mkdirs() + + var fileName: String + + do { + fileName = random.nextLong().absoluteValue.toString() + suffix + } while (dir.resolve(fileName).exists()) + + return fileName + } + + fun nextInt(): Int { + return random.nextInt() + } +} \ No newline at end of file diff --git a/service/src/main/res/drawable/ic_notification.xml b/service/src/main/res/drawable/ic_notification.xml new file mode 100644 index 0000000000..9189393130 --- /dev/null +++ b/service/src/main/res/drawable/ic_notification.xml @@ -0,0 +1,9 @@ + + + diff --git a/service/src/main/res/drawable/ic_notification_icon.png b/service/src/main/res/drawable/ic_notification_icon.png deleted file mode 100644 index 9efd87e5869fa120c2b30b450e9faad2a9fee6a4..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4001 zcmV;S4_@$zP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 Hu0mjfBNk`J diff --git a/service/src/main/res/drawable/ic_update_completed.xml b/service/src/main/res/drawable/ic_update_completed.xml new file mode 100644 index 0000000000..c24862da57 --- /dev/null +++ b/service/src/main/res/drawable/ic_update_completed.xml @@ -0,0 +1,9 @@ + + + diff --git a/service/src/main/res/drawable/ic_update_failure.xml b/service/src/main/res/drawable/ic_update_failure.xml new file mode 100644 index 0000000000..0cbb996010 --- /dev/null +++ b/service/src/main/res/drawable/ic_update_failure.xml @@ -0,0 +1,9 @@ + + + diff --git a/service/src/main/res/drawable/ic_updating.xml b/service/src/main/res/drawable/ic_updating.xml new file mode 100644 index 0000000000..1d835e94b5 --- /dev/null +++ b/service/src/main/res/drawable/ic_updating.xml @@ -0,0 +1,9 @@ + + + diff --git a/service/src/main/res/layout/clash_notification.xml b/service/src/main/res/layout/clash_notification.xml deleted file mode 100644 index 19c06f41cc..0000000000 --- a/service/src/main/res/layout/clash_notification.xml +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/service/src/main/res/values/strings.xml b/service/src/main/res/values/strings.xml index deac4f007b..df3547d9cf 100644 --- a/service/src/main/res/values/strings.xml +++ b/service/src/main/res/values/strings.xml @@ -13,7 +13,18 @@ "%1$s↑\t%2$s↓" + Profile Service Status + Processing Profiles + Waiting Request + Updating %s + Profile Service Result + Process Result + Update %s Completed + Update %s Failure + Profile %s deleted + Clash Core Service Tun Device Service Clash Manager Service + Clash Profile Manager Service From 23027b881360045403f2f1aa6b750e85660f97cb Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 8 Feb 2020 02:30:09 +0800 Subject: [PATCH 077/358] [WIP] refactor profile service --- app/build.gradle | 9 +- app/src/main/AndroidManifest.xml | 7 +- .../com/github/kr328/clash/BaseActivity.kt | 20 +- .../kr328/clash/CreateProfileActivity.kt | 6 + .../com/github/kr328/clash/MainActivity.kt | 14 +- .../com/github/kr328/clash/MainApplication.kt | 29 -- .../github/kr328/clash/ProfilesActivity.kt | 45 ++- .../kr328/clash/adapter/ProfileAdapter.kt | 139 ++++++++ .../github/kr328/clash/remote/Broadcasts.kt | 12 +- .../com/github/kr328/clash/remote/Calls.kt | 2 +- .../{ic_new_profile.xml => ic_new.xml} | 2 +- .../drawable/{ic_vert.xml => ic_vertex.xml} | 2 +- app/src/main/res/layout/activity_main.xml | 28 +- .../res/layout/adapter_profile_entity.xml | 60 ++++ .../res/layout/adapter_profile_footer.xml | 26 ++ app/src/main/res/values/strings.xml | 49 +-- build.gradle | 5 +- core/build.gradle | 5 +- core/src/main/golang/bridge/profiles.go | 43 +-- core/src/main/golang/bridge/statistics.go | 14 +- core/src/main/golang/bridge/tun.go | 16 +- core/src/main/golang/clash | 2 +- .../java/com/github/kr328/clash/core/Clash.kt | 38 +-- .../clash/core/transact/DoneCallbackImpl.kt | 8 +- design/build.gradle | 4 +- service/build.gradle | 5 +- service/src/main/AndroidManifest.xml | 9 +- .../clash/service/IClashSettingService.aidl | 16 - .../kr328/clash/service/IProfileService.aidl | 7 + .../github/kr328/clash/service/BaseService.kt | 21 ++ .../kr328/clash/service/ClashManager.kt | 6 +- .../kr328/clash/service/ClashNotification.kt | 146 ++++----- .../clash/service/ClashProfileService.kt | 301 ------------------ .../kr328/clash/service/ClashService.kt | 86 ++--- .../clash/service/ProfileBackgroundService.kt | 198 ++++++++++++ .../clash/service/ProfileProcessService.kt | 164 ++++++++++ ...eReceiver.kt => ProfileRequestReceiver.kt} | 4 +- .../github/kr328/clash/service/TunService.kt | 106 +++--- ...rkObserver.kt => DefaultNetworkChannel.kt} | 45 ++- .../clash/service/transact/ProfileRequest.kt | 12 +- .../clash/service/util/BroadcastUtils.kt | 26 ++ .../clash/service/util/ComponentUtils.kt | 6 +- .../github/kr328/clash/service/util/Ticker.kt | 15 + .../kr328/clash/service/util/Timeout.kt | 9 + 44 files changed, 1054 insertions(+), 713 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt create mode 100644 app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt rename app/src/main/res/drawable/{ic_new_profile.xml => ic_new.xml} (84%) rename app/src/main/res/drawable/{ic_vert.xml => ic_vertex.xml} (89%) create mode 100644 app/src/main/res/layout/adapter_profile_entity.xml create mode 100644 app/src/main/res/layout/adapter_profile_footer.xml delete mode 100644 service/src/main/aidl/com/github/kr328/clash/service/IClashSettingService.aidl create mode 100644 service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl create mode 100644 service/src/main/java/com/github/kr328/clash/service/BaseService.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt rename service/src/main/java/com/github/kr328/clash/service/{ClashProfileReceiver.kt => ProfileRequestReceiver.kt} (82%) rename service/src/main/java/com/github/kr328/clash/service/net/{DefaultNetworkObserver.kt => DefaultNetworkChannel.kt} (74%) create mode 100644 service/src/main/java/com/github/kr328/clash/service/util/Ticker.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/util/Timeout.kt diff --git a/app/build.gradle b/app/build.gradle index c826806db1..86242d0445 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,7 +8,7 @@ apply plugin: 'io.fabric' android { compileSdkVersion 29 - buildToolsVersion "29.0.2" + buildToolsVersion "29.0.3" defaultConfig { applicationId "com.github.kr328.clash" minSdkVersion 24 @@ -29,9 +29,6 @@ android { kotlinOptions { jvmTarget = "1.8" } - bundle { - - } } dependencies { @@ -41,13 +38,13 @@ dependencies { implementation project(":service") implementation project(":design") implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.3' + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutine_version" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation 'androidx.core:core-ktx:1.2.0-rc01' + implementation 'androidx.core:core-ktx:1.3.0-alpha01' implementation 'androidx.fragment:fragment-ktx:1.2.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 14eecf8c86..46ed2e6d9c 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,8 +28,13 @@ + diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index d65a462253..704a89b40e 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -13,7 +13,6 @@ import android.widget.FrameLayout import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import com.github.kr328.clash.remote.Broadcasts -import com.github.kr328.clash.service.ClashService import com.github.kr328.clash.service.data.ClashProfileEntity import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope @@ -45,17 +44,11 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() } } - var clashRunning: Boolean = false - private set - - open suspend fun onClashStarted() { - clashRunning = true - } - - open suspend fun onClashStopped(reason: String?) { - clashRunning = false - } + val clashRunning: Boolean + get() = Broadcasts.clashRunning + open suspend fun onClashStarted() {} + open suspend fun onClashStopped(reason: String?) {} open suspend fun onClashProfileChanged(active: ClashProfileEntity?) {} override fun setContentView(layoutResID: Int) { @@ -89,11 +82,6 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() super.onStart() Broadcasts.register(receiver) - - clashRunning = EmptyBroadcastReceiver().peekService( - this, - Intent(this, ClashService::class.java) - ) != null } override fun onStop() { diff --git a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt new file mode 100644 index 0000000000..8bc8476917 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt @@ -0,0 +1,6 @@ +package com.github.kr328.clash + +import android.os.Bundle + +class CreateProfileActivity : BaseActivity() { +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 00ebea26d9..7aabe436e4 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -29,14 +29,14 @@ class MainActivity : BaseActivity() { launch { if (clashRunning) { status.icon = getDrawable(R.drawable.ic_started) - status.title = getText(R.string.clash_status_started) + status.title = getText(R.string.running) status.summary = getString( - R.string.clash_status_forwarded_traffic, + R.string.format_traffic_forwarded, 0L.asBytesString() ) - } - startBandwidthPolling() + startBandwidthPolling() + } } } @@ -47,14 +47,10 @@ class MainActivity : BaseActivity() { } override suspend fun onClashStarted() { - super.onClashStarted() - startBandwidthPolling() } override suspend fun onClashStopped(reason: String?) { - super.onClashStopped(reason) - stopBandwidthPolling() } @@ -67,7 +63,7 @@ class MainActivity : BaseActivity() { while (clashRunning && isActive) { val bandwidth = queryBandwidth() status.summary = getString( - R.string.clash_status_forwarded_traffic, + R.string.format_traffic_forwarded, bandwidth.asBytesString() ) delay(1000) diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 461528549d..583f3aa253 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,42 +2,15 @@ package com.github.kr328.clash import android.app.Application import android.content.Context -import android.os.Build import com.crashlytics.android.Crashlytics import com.github.kr328.clash.core.Global import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.remote.ClashClient import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric -import java.security.MessageDigest @Suppress("unused") class MainApplication : Application() { - companion object { - const val KEY_PROXY_MODE = "key_proxy_mode" - const val PROXY_MODE_VPN = "vpn" - const val PROXY_MODE_PROXY_ONLY = "proxy_only" - - val userIdentifier: String by lazy { - val archive = - Global.application.packageManager.getPackageInfo(Global.application.packageName, 0) - val encoder = MessageDigest.getInstance("md5") - - encoder.digest((Build.ID + archive.lastUpdateTime).toByteArray()).toHexString() - } - - private fun ByteArray.toHexString(): String { - return this.map { - Integer.toHexString(it.toInt() and 0xff) - }.joinToString(separator = "") { - if (it.length < 2) - "0$it" - else - it - } - } - } - override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) @@ -54,8 +27,6 @@ class MainApplication : Application() { Fabric.with(this, Crashlytics()) } - Crashlytics.setUserIdentifier(userIdentifier) - ClashClient.init(this) Broadcasts.init(this) } diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 7192086873..bd16d19e4b 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -1,20 +1,48 @@ package com.github.kr328.clash import android.os.Bundle +import androidx.recyclerview.widget.LinearLayoutManager +import com.github.kr328.clash.adapter.ProfileAdapter import com.github.kr328.clash.remote.withClash import com.github.kr328.clash.service.data.ClashProfileEntity import kotlinx.android.synthetic.main.activity_profiles.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch class ProfilesActivity : BaseActivity() { + private var backgroundJob: Job? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_profiles) - setSupportActionBar(toolbar) - reloadProfiles() + mainList.layoutManager = LinearLayoutManager(this) + mainList.adapter = ProfileAdapter(this) + } + + override fun onStart() { + super.onStart() + + backgroundJob = launch { + reloadProfiles() + + while (isActive) { + delay(1000 * 60) + + // Refresh without animation + (mainList.adapter as ProfileAdapter).notifyDataSetChanged() + } + } + } + + override fun onStop() { + super.onStop() + + backgroundJob?.cancel() + backgroundJob = null } override suspend fun onClashProfileChanged(active: ClashProfileEntity?) { @@ -23,11 +51,12 @@ class ProfilesActivity : BaseActivity() { reloadProfiles() } - private fun reloadProfiles() { - launch { - withClash { - - } + private suspend fun reloadProfiles() { + val profiles = withClash { + queryProfiles() } + + (mainList.adapter as ProfileAdapter) + .setEntitiesAsync(profiles.toList()) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt new file mode 100644 index 0000000000..83839c8e8c --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt @@ -0,0 +1,139 @@ +package com.github.kr328.clash.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.RadioButton +import android.widget.TextView +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.R +import com.github.kr328.clash.service.data.ClashProfileEntity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.* + +class ProfileAdapter(private val context: Context) : + RecyclerView.Adapter() { + var entities: List = emptyList() + private set + + class EntityHolder(view: View) : RecyclerView.ViewHolder(view) { + val root: View = view.findViewById(R.id.root) + val menu: View = view.findViewById(R.id.menu) + val radio: RadioButton = view.findViewById(R.id.radio) + val name: TextView = view.findViewById(R.id.name) + val type: TextView = view.findViewById(R.id.type) + val interval: TextView = view.findViewById(R.id.interval) + } + + class FooterHolder(view: View) : RecyclerView.ViewHolder(view) { + val root: View = view.findViewById(R.id.root) + } + + suspend fun setEntitiesAsync(new: List) { + val old = withContext(Dispatchers.Main) { + entities + } + + val result = withContext(Dispatchers.Default) { + DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = + old[oldItemPosition] === new[newItemPosition] + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean = old[oldItemPosition] == new[newItemPosition] + + override fun getOldListSize(): Int = old.size + override fun getNewListSize(): Int = new.size + }, false) + } + + withContext(Dispatchers.Main) { + entities = old + result.dispatchUpdatesTo(this@ProfileAdapter) + } + } + + override fun getItemViewType(position: Int): Int { + return if (position == entities.size) + Int.MAX_VALUE + else + super.getItemViewType(position) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + if (viewType == Int.MAX_VALUE) { + return FooterHolder( + LayoutInflater.from(context).inflate( + R.layout.adapter_profile_footer, + parent, + false + ) + ) + } + return EntityHolder( + LayoutInflater.from(context).inflate( + R.layout.adapter_profile_entity, + parent, + false + ) + ) + } + + override fun getItemCount(): Int { + return entities.size + 1 + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is EntityHolder -> { + val current = entities[position] + + holder.radio.isChecked = current.active + holder.name.text = current.name + holder.type.text = getTypeName(current.type) + holder.interval.text = offsetDate(current.lastUpdate) + } + is FooterHolder -> { + + } + } + } + + private fun getTypeName(type: Int): CharSequence { + return when (type) { + ClashProfileEntity.TYPE_LOCAL -> + context.getText(R.string.local) + ClashProfileEntity.TYPE_REMOTE -> + context.getText(R.string.remote) + else -> + context.getText(R.string.unknown) + } + } + + private fun offsetDate(date: Long): CharSequence { + val current = Calendar.getInstance() + val base = Calendar.getInstance().apply { + timeInMillis = date + } + + val year = current.get(Calendar.YEAR) - base.get(Calendar.YEAR) + val month = current.get(Calendar.MONTH) - base.get(Calendar.MONTH) + val day = current.get(Calendar.DAY_OF_YEAR) - base.get(Calendar.DAY_OF_YEAR) + val hour = current.get(Calendar.HOUR) - base.get(Calendar.HOUR) + val minute = current.get(Calendar.MINUTE) - base.get(Calendar.MINUTE) + + return when { + year > 0 -> context.getString(R.string.format_years, year) + month > 0 -> context.getString(R.string.format_months, month) + day > 0 -> context.getString(R.string.format_days, day) + hour > 0 -> context.getString(R.string.format_hours, hour) + minute > 0 -> context.getString(R.string.format_minutes, minute) + else -> context.getText(R.string.recently) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt index 0b43f83abe..379c3a7e87 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt @@ -8,8 +8,10 @@ import android.content.IntentFilter import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner +import com.github.kr328.clash.service.ClashService import com.github.kr328.clash.service.Intents import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.util.componentName object Broadcasts { interface Receiver { @@ -18,8 +20,9 @@ object Broadcasts { fun onProfileChanged(active: ClashProfileEntity?) } - private val receivers = mutableListOf() + var clashRunning: Boolean = false + private val receivers = mutableListOf() private val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { @@ -55,10 +58,17 @@ object Broadcasts { addAction(Intents.INTENT_ACTION_CLASH_STOPPED) addAction(Intents.INTENT_ACTION_CLASH_STARTED) }) + + clashRunning = broadcastReceiver.peekService( + application, + Intent().setComponent(ClashService::class.componentName) + ) != null } override fun onStop(owner: LifecycleOwner) { application.unregisterReceiver(broadcastReceiver) + + clashRunning = false } }) } diff --git a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt index 5c2e93e205..d0b7dffe52 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt @@ -1,6 +1,6 @@ package com.github.kr328.clash.remote -suspend fun withClash(block: suspend ClashClient.() -> T): T? { +suspend fun withClash(block: suspend ClashClient.() -> T): T { val client = ClashClient.clashInstanceChannel.receive() return client.block() diff --git a/app/src/main/res/drawable/ic_new_profile.xml b/app/src/main/res/drawable/ic_new.xml similarity index 84% rename from app/src/main/res/drawable/ic_new_profile.xml rename to app/src/main/res/drawable/ic_new.xml index fedd077d84..9107935652 100644 --- a/app/src/main/res/drawable/ic_new_profile.xml +++ b/app/src/main/res/drawable/ic_new.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_vert.xml b/app/src/main/res/drawable/ic_vertex.xml similarity index 89% rename from app/src/main/res/drawable/ic_vert.xml rename to app/src/main/res/drawable/ic_vertex.xml index 7b7f195546..c40dc0c0da 100644 --- a/app/src/main/res/drawable/ic_vert.xml +++ b/app/src/main/res/drawable/ic_vertex.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 1c847de74c..ee6cdfec0a 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -48,8 +48,8 @@ app:cardCornerRadius="8dp" app:cardBackgroundColor="@color/primaryCardColor" app:icon="@drawable/ic_stopped" - app:title="@string/clash_status_stopped" - app:summary="@string/clash_status_tap_to_start"/> + app:title="@string/stopped" + app:summary="@string/tap_to_start"/> + app:title="@string/proxy" + app:summary="@string/direct_mode"/> + app:title="@string/profiles" + app:summary="@string/not_selected"/> @@ -109,7 +109,7 @@ android:layout_height="wrap_content" android:layout_marginStart="25dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium" - android:text="@string/clash_logs" /> + android:text="@string/logs" /> @@ -136,7 +136,7 @@ android:layout_height="wrap_content" android:layout_marginStart="25dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium" - android:text="@string/clash_settings" /> + android:text="@string/settings" /> @@ -163,7 +163,7 @@ android:layout_height="wrap_content" android:layout_marginStart="25dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium" - android:text="@string/clash_feedback" /> + android:text="@string/feedback" /> @@ -190,7 +190,7 @@ android:layout_height="wrap_content" android:layout_marginStart="25dp" android:textAppearance="@style/TextAppearance.AppCompat.Medium" - android:text="@string/clash_about" /> + android:text="@string/about" /> diff --git a/app/src/main/res/layout/adapter_profile_entity.xml b/app/src/main/res/layout/adapter_profile_entity.xml new file mode 100644 index 0000000000..65956b6bc3 --- /dev/null +++ b/app/src/main/res/layout/adapter_profile_entity.xml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_profile_footer.xml b/app/src/main/res/layout/adapter_profile_footer.xml new file mode 100644 index 0000000000..bf981c4901 --- /dev/null +++ b/app/src/main/res/layout/adapter_profile_footer.xml @@ -0,0 +1,26 @@ + + + + + + + \ 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 63624bbb8e..64cd461290 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2,24 +2,37 @@ Clash Clash for Android - Stopped - Tap to start - Running - %s Forwarded - - Proxy - Rule Mode - Direct Mode - Global Mode - - Profiles - %s Activated - Not selected - - Logs - Settings - Feedback - About + Stopped + Tap to start + Running + %s Forwarded + + Proxy + Rule Mode + Direct Mode + Global Mode + + Profiles + %s Activated + Not selected + + Logs + Settings + Feedback + About + + Local + Remote + Unknown + + Recently + %d minutes + %d hours + %d days + %d months + %d years + + Create Profile %s (File) %s (URL) diff --git a/build.gradle b/build.gradle index aa90fcb7aa..e205f9c6ca 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,9 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -buildscript { - ext.kotlin_version = '1.3.61' - +buildscript { ext { kotlin_version = '1.3.61' + kotlin_coroutine_version = '1.3.3' room_version = '2.2.3' } repositories { diff --git a/core/build.gradle b/core/build.gradle index c02b234e45..b41a763bf4 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -5,7 +5,7 @@ apply plugin: 'kotlinx-serialization' android { compileSdkVersion 29 - buildToolsVersion "29.0.2" + buildToolsVersion "29.0.3" defaultConfig { minSdkVersion 24 @@ -37,8 +37,9 @@ android { } dependencies { - implementation "androidx.core:core-ktx:1.1.0" + implementation "androidx.core:core-ktx:1.2.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutine_version" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" } diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go index 5c8d5f5deb..a0c339d931 100644 --- a/core/src/main/golang/bridge/profiles.go +++ b/core/src/main/golang/bridge/profiles.go @@ -6,31 +6,38 @@ import ( func LoadProfileFile(path, baseDir string, callback DoneCallback) { go func() { - err := profile.LoadFromFile(path, baseDir) - if err != nil { - callback.DoneWithError(err) - } else { - callback.Done() - } + call(profile.LoadFromFile(path, baseDir), callback) }() } -func DownloadProfileAndCheck(url, output, baseDir string) error { - err := profile.DownloadAndCheck(url, output, baseDir) - if err != nil { - return err - } - return nil +func DownloadProfileAndCheck(url, output, baseDir string, callback DoneCallback) { + go func() { + call(profile.DownloadAndCheck(url, output, baseDir), callback) + }() +} + +func ReadProfileAndCheck(fd int, output, baseDir string, callback DoneCallback) { + go func() { + call(profile.ReadAndCheck(fd, output, baseDir), callback) + }() } -func ReadProfileAndCheck(fd int, output, baseDir string) error { - return profile.ReadAndCheck(fd, output, baseDir) +func SaveProfileAndCheck(data []byte, output, baseDir string, callback DoneCallback) { + go func() { + call(profile.SaveAndCheck(data, output, baseDir), callback) + }() } -func SaveProfileAndCheck(data []byte, output, baseDir string) error { - return profile.SaveAndCheck(data, output, baseDir) +func MoveProfileAndCheck(source, target, baseDir string, callback DoneCallback) { + go func() { + call(profile.MoveAndCheck(source, target, baseDir), callback) + }() } -func MoveProfileAndCheck(source, target, baseDir string) error { - return profile.MoveAndCheck(source, target, baseDir) +func call(err error, callback DoneCallback) { + if err != nil { + callback.DoneWithError(err) + } else { + callback.Done() + } } diff --git a/core/src/main/golang/bridge/statistics.go b/core/src/main/golang/bridge/statistics.go index e75eaf7890..dc307575a0 100644 --- a/core/src/main/golang/bridge/statistics.go +++ b/core/src/main/golang/bridge/statistics.go @@ -18,16 +18,18 @@ type Traffic struct { Upload int64 } -type Bandwidth interface { - OnEvent(bandwidth int64) -} - type Logs interface { OnEvent(level, payload string) } -func QueryBandwidth() int64 { - return tunnel.DefaultManager.Forwarded() +func QueryBandwidth() *Traffic { + upload := tunnel.DefaultManager.UploadTotal() + download := tunnel.DefaultManager.DownloadTotal() + + return &Traffic{ + Upload: upload, + Download: download, + } } func QueryTraffic() *Traffic { diff --git a/core/src/main/golang/bridge/tun.go b/core/src/main/golang/bridge/tun.go index 7cc527816c..ad566e87f2 100644 --- a/core/src/main/golang/bridge/tun.go +++ b/core/src/main/golang/bridge/tun.go @@ -4,10 +4,24 @@ import ( "github.com/kr328/cfa/tun" ) -func StartTunDevice(fd, mtu int, dns string) error { +type TunCallback interface { + OnStop() +} + +var callback TunCallback + +func StartTunDevice(fd, mtu int, dns string, cb TunCallback) error { + callback = cb + return tun.StartTunDevice(fd, mtu, dns) } func StopTunDevice() { + if c := callback; c != nil { + c.OnStop() + } + + callback = nil + tun.StopTunDevice() } diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index 0a4aa8fcb6..102408d736 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit 0a4aa8fcb6f9ef43d4e6c8b8beac8ef67ab31f80 +Subproject commit 102408d736d602f6ec1091caaf0adcbb4f702adf diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 47e207036f..22e7b28f42 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -12,6 +12,7 @@ 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 kotlinx.coroutines.CompletableDeferred import java.io.File import java.io.InputStream import java.util.concurrent.CompletableFuture @@ -42,35 +43,32 @@ object Clash { fun startTunDevice( fd: Int, mtu: Int, - dns: String + dns: String, + onStop: () -> Unit ) { - Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns) + Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns, onStop) } fun stopTunDevice() { Bridge.stopTunDevice() } - fun loadProfile(path: File, baseDir: File): CompletableFuture { + fun loadProfile(path: File, baseDir: File): CompletableDeferred { return DoneCallbackImpl().apply { Bridge.loadProfileFile(path.absolutePath, baseDir.absolutePath, this) } } - fun downloadProfile(url: String, output: File, baseDir: File) { - Bridge.downloadProfileAndCheck(url, output.absolutePath, baseDir.absolutePath) - } - - fun copyProfile(fd: Int, output: File, baseDir: File) { - Bridge.readProfileAndCheck(fd.toLong(), output.absolutePath, baseDir.absolutePath) - } - - fun saveProfile(data: ByteArray, output: File, baseDir: File) { - Bridge.saveProfileAndCheck(data, output.absolutePath, baseDir.absolutePath) + fun downloadProfile(url: String, output: File, baseDir: File): CompletableDeferred { + return DoneCallbackImpl().apply { + Bridge.downloadProfileAndCheck(url, output.absolutePath, baseDir.absolutePath, this) + } } - fun moveProfile(source: File, target: File, baseDir: File) { - Bridge.moveProfileAndCheck(source.absolutePath, target.absolutePath, baseDir.absolutePath) + fun downloadProfile(fd: Int, output: File, baseDir: File): CompletableDeferred { + return DoneCallbackImpl().apply { + Bridge.readProfileAndCheck(fd.toLong(), output.absolutePath, baseDir.absolutePath, this) + } } fun queryProxyGroups(): List { @@ -93,7 +91,7 @@ object Clash { return Bridge.setSelectedProxy(name, selected) } - fun startHealthCheck(name: String): CompletableFuture { + fun startHealthCheck(name: String): CompletableDeferred { return DoneCallbackImpl().apply { Bridge.startUrlTest(name, this) } @@ -108,14 +106,16 @@ object Clash { ) } - fun queryTrafficEvent(): Traffic { + fun queryTraffic(): Traffic { val data = Bridge.queryTraffic() return Traffic(data.upload, data.download) } - fun queryBandwidth(): Long { - return Bridge.queryBandwidth() + fun queryBandwidth(): Traffic { + val data = Bridge.queryBandwidth() + + return Traffic(data.upload, data.download) } fun openLogEvent(): EventStream { diff --git a/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt index de56a4bba9..2319d6d422 100644 --- a/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt +++ b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt @@ -1,14 +1,12 @@ package com.github.kr328.clash.core.transact import bridge.DoneCallback +import kotlinx.coroutines.CompletableDeferred import java.util.concurrent.CompletableFuture -class DoneCallbackImpl : DoneCallback, CompletableFuture() { +class DoneCallbackImpl : DoneCallback, CompletableDeferred by CompletableDeferred() { override fun doneWithError(e: Exception?) { - if (e == null) - complete(Unit) - else - completeExceptionally(e) + completeExceptionally(e ?: return done()) } override fun done() { diff --git a/design/build.gradle b/design/build.gradle index 3e6bad8c16..a061493f9e 100644 --- a/design/build.gradle +++ b/design/build.gradle @@ -3,7 +3,7 @@ apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 29 - buildToolsVersion "29.0.2" + buildToolsVersion "29.0.3" defaultConfig { @@ -29,6 +29,6 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.appcompat:appcompat:1.1.0' - implementation 'androidx.core:core-ktx:1.1.0' + implementation 'androidx.core:core-ktx:1.2.0' implementation "com.google.android.material:material:1.2.0-alpha04" } diff --git a/service/build.gradle b/service/build.gradle index 8c6c1d702a..1d0f15cc3c 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -6,7 +6,7 @@ apply plugin: 'kotlinx-serialization' android { compileSdkVersion 29 - buildToolsVersion "29.0.2" + buildToolsVersion "29.0.3" defaultConfig { @@ -39,6 +39,7 @@ dependencies { implementation project(":core") implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutine_version" implementation "androidx.room:room-runtime:$room_version" - implementation 'androidx.core:core-ktx:1.2.0-rc01' + implementation 'androidx.core:core-ktx:1.3.0-alpha01' } \ No newline at end of file diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index 96459c561b..9bff042148 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -26,13 +26,16 @@ android:exported="false" android:process=":background" /> + - 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 deleted file mode 100644 index 2670625301..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashSettingService.aidl +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.kr328.clash.service; - -interface IClashSettingService { - // Set - void setIPv6Enabled(boolean enabled); - void setBypassPrivateNetwork(boolean enabled); - void setDnsHijackingEnabled(boolean enabled); - void setAccessControl(int mode, in String[] applications); - - // Get - boolean isIPv6Enabled(); - boolean isBypassPrivateNetwork(); - boolean isDnsHijackingEnabled(); - String[] getAccessControlApps(); - int getAccessControlMode(); -} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl new file mode 100644 index 0000000000..0123d0647e --- /dev/null +++ b/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl @@ -0,0 +1,7 @@ +package com.github.kr328.clash.service; + +import com.github.kr328.clash.service.transact.ProfileRequest; + +interface IProfileService { + void enqueueRequest(in ProfileRequest request); +} diff --git a/service/src/main/java/com/github/kr328/clash/service/BaseService.kt b/service/src/main/java/com/github/kr328/clash/service/BaseService.kt new file mode 100644 index 0000000000..1fb43e91ac --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/BaseService.kt @@ -0,0 +1,21 @@ +package com.github.kr328.clash.service + +import android.app.Service +import com.github.kr328.clash.core.Clash +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel + +abstract class BaseService : Service(), CoroutineScope by MainScope() { + override fun onCreate() { + super.onCreate() + + Clash.initialize(this) + } + + override fun onDestroy() { + super.onDestroy() + + cancel() + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index f24528af63..69152382c2 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -39,7 +39,9 @@ class ClashManager(private val context: Context) : IClashManager.Stub() { } override fun queryBandwidth(): Long { - return Clash.queryBandwidth() + val data = Clash.queryBandwidth() + + return data.download + data.upload } override fun openLogEvent(callback: IStreamCallback?) { @@ -59,7 +61,7 @@ class ClashManager(private val context: Context) : IClashManager.Stub() { override fun startHealthCheck(group: String?, callback: IStreamCallback?) { require(group != null && callback != null) - Clash.startHealthCheck(group).whenComplete { _, u -> + Clash.startHealthCheck(group).invokeOnCompletion { u -> if (u != null) callback.completeExceptionally(u.message) else diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 81170846d3..8eb2d5c512 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -6,23 +6,27 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.Build -import android.os.Handler import android.os.PowerManager import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.utils.asBytesString import com.github.kr328.clash.core.utils.asSpeedString - -class ClashNotification(private val context: Service) { +import com.github.kr328.clash.service.util.ticker +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.selects.select +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext + +class ClashNotification(private val context: Service) : CoroutineScope { companion object { private const val CLASH_STATUS_NOTIFICATION_CHANNEL = "clash_status_channel" private const val CLASH_STATUS_NOTIFICATION_ID = 413 } - private val handler = Handler() - private var showing = false - private val contentIntent = Intent(Intent.ACTION_MAIN) + .addCategory(Intent.CATEGORY_DEFAULT) .addCategory(Intent.CATEGORY_LAUNCHER) .setPackage(context.packageName) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) @@ -40,111 +44,92 @@ class ClashNotification(private val context: Service) { PendingIntent.FLAG_UPDATE_CURRENT ) ) + private val screenChannel: Channel = Channel(Channel.CONFLATED) - private var auto = false - private var vpn = false private var profile = "None" private val observer = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { when (intent?.action) { Intent.ACTION_SCREEN_ON -> - enableUpdate() + screenChannel.offer(true) Intent.ACTION_SCREEN_OFF -> - disableUpdate() + screenChannel.offer(false) } } } init { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationManagerCompat.from(context) - .createNotificationChannel( - NotificationChannel( - CLASH_STATUS_NOTIFICATION_CHANNEL, - context.getString(R.string.clash_service_status_channel), - NotificationManager.IMPORTANCE_LOW - ) - ) + runBlocking { + update() } - handler.post { - showing = true + launch { + withContext(Dispatchers.IO) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationManagerCompat.from(context) + .createNotificationChannel( + NotificationChannel( + CLASH_STATUS_NOTIFICATION_CHANNEL, + context.getString(R.string.clash_service_status_channel), + NotificationManager.IMPORTANCE_LOW + ) + ) + } + } - update() + while (isActive) { + val powerManager = + requireNotNull(context.getSystemService(PowerManager::class.java)) + val tickerChannel = Channel(Channel.CONFLATED) + var tickerJob = if (powerManager.isInteractive) + ticker(1000, tickerChannel) + else + EmptyCoroutineContext + + select { + screenChannel.onReceive { + tickerJob.cancel() + + if (it) { + tickerJob = ticker(1000, tickerChannel) + } + } + tickerChannel.onReceive { + update() + } + } + } } context.registerReceiver(observer, IntentFilter().apply { addAction(Intent.ACTION_SCREEN_ON) addAction(Intent.ACTION_SCREEN_OFF) }) - - if (context.getSystemService(PowerManager::class.java)!!.isInteractive) { - enableUpdate() - } } fun destroy() { - handler.post { - disableUpdate() + cancel() - if (showing) - context.stopForeground(true) - - showing = false - } + context.unregisterReceiver(observer) + context.stopForeground(true) } fun setProfile(profile: String) { - handler.post { - this.profile = profile - - update() - } - } - - fun setVpn(vpn: Boolean) { - handler.post { - this.vpn = vpn - - update() - } + this.profile = profile } - private fun updateSpeed() { - handler.postDelayed({ - if (!auto) - return@postDelayed - update() - - updateSpeed() - }, 1000) - } - - private fun enableUpdate() { - handler.post { - if (auto) - return@post - - auto = true - - updateSpeed() + private suspend fun update() { + val notification = withContext(Dispatchers.Default) { + createNotification() } - } - private fun disableUpdate() { - handler.post { - auto = false - } - } - - private fun update() { - if (showing) - context.startForeground(CLASH_STATUS_NOTIFICATION_ID, createNotification()) + context.startForeground(CLASH_STATUS_NOTIFICATION_ID, notification) } private fun createNotification(): Notification { - val traffic = Clash.queryTrafficEvent() + val traffic = Clash.queryTraffic() + val bandwidth = Clash.queryBandwidth() return baseBuilder .setContentTitle(profile) @@ -155,7 +140,16 @@ class ClashNotification(private val context: Service) { traffic.download.asSpeedString() ) ) - .setSubText(if (vpn) context.getText(R.string.clash_service_vpn_mode) else null) + .setSubText( + context.getString( + R.string.clash_notification_content, + bandwidth.upload.asBytesString(), + traffic.download.asBytesString() + ) + ) .build() } + + override val coroutineContext: CoroutineContext + get() = SupervisorJob() } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt deleted file mode 100644 index c3dac0b983..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileService.kt +++ /dev/null @@ -1,301 +0,0 @@ -package com.github.kr328.clash.service - -import android.app.* -import android.content.Intent -import android.net.Uri -import android.os.Binder -import android.os.Build -import android.os.IBinder -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.service.data.ClashDatabase -import com.github.kr328.clash.service.data.ClashProfileDao -import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.service.transact.ProfileRequest -import com.github.kr328.clash.service.util.* -import java.io.File -import java.io.FileNotFoundException -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread - -class ClashProfileService : Service() { - companion object { - private const val SERVICE_STATUS_CHANNEL = "profile_service_status" - private const val SERVICE_RESULT_CHANNEL = "profile_service_result" - private const val SERVICE_NOTIFICATION_ID = 10000 - } - - private val requestQueue = LinkedBlockingQueue() - - val service: ClashProfileService - get() = this - val profiles: ClashProfileDao by lazy { - ClashDatabase.getInstance(service).openClashProfileDao() - } - - override fun onCreate() { - super.onCreate() - - createNotificationChannels() - - updateNotificationWaiting() - - thread { - while (true) { - val request = try { - updateNotificationWaiting() - requestQueue.poll(60, TimeUnit.SECONDS) ?: break - } catch (e: InterruptedException) { - break - } - - handleRequest(request) - } - - stopSelf() - } - } - - override fun onBind(intent: Intent?): IBinder? { - return Binder() - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - when (intent?.action) { - Intents.INTENT_ACTION_PROFILE_SETUP -> { - resetProfileUpdateAlarm() - } - Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST -> { - val request = - intent.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST) - if (request != null) - enqueueRequest(request) - } - } - - return START_NOT_STICKY - } - - override fun onDestroy() { - stopForeground(true) - - super.onDestroy() - } - - private fun createNotificationChannels() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - return - - NotificationManagerCompat.from(this).createNotificationChannels( - listOf( - NotificationChannel( - SERVICE_STATUS_CHANNEL, - getText(R.string.profile_service_status_channel), - NotificationManager.IMPORTANCE_LOW - ), - NotificationChannel( - SERVICE_RESULT_CHANNEL, - getText(R.string.profile_service_result), - NotificationManager.IMPORTANCE_DEFAULT - ) - ) - ) - } - - private fun createServiceNotification(content: CharSequence): Notification { - return NotificationCompat.Builder(this, SERVICE_STATUS_CHANNEL) - .setContentTitle(getText(R.string.profile_service_status_title)) - .setContentText(content) - .setSmallIcon(R.drawable.ic_updating) - .setOnlyAlertOnce(true) - .setProgress(Int.MAX_VALUE, 0, true) - .build() - } - - private fun createResultNotification(success: Boolean, content: CharSequence): Notification { - return NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) - .setContentTitle(getText(R.string.profile_process_result)) - .setContentText(content) - .setSmallIcon(if (success) R.drawable.ic_update_completed else R.drawable.ic_update_failure) - .build() - } - - private fun updateNotificationWaiting() { - startForeground( - SERVICE_NOTIFICATION_ID, - createServiceNotification(getText(R.string.profile_service_status_waiting)) - ) - } - - private fun updateNotificationUpdating(profileName: String) { - createServiceNotification( - getString( - R.string.profile_service_status_updating, - profileName - ) - ) - } - - private fun notifyResultNotification(success: Boolean, content: CharSequence) { - NotificationManagerCompat.from(this) - .notify(RandomUtils.nextInt(), createResultNotification(success, content)) - } - - private fun enqueueRequest(request: ProfileRequest) { - requestQueue.offer(request) - } - - private fun handleRequest(request: ProfileRequest) { - when (request.action) { - ProfileRequest.Action.UPDATE_OR_CREATE -> - handleUpdateOrCreate(request) - ProfileRequest.Action.REMOVE -> - removeProfile(request) - } - - sendBroadcastSelf( - Intent(Intents.INTENT_ACTION_PROFILE_CHANGED) - .putExtra( - Intents.INTENT_EXTRA_PROFILE_ACTIVE, - profiles.queryActiveProfile() - ) - ) - } - - private fun handleUpdateOrCreate(request: ProfileRequest) { - val id = request.id ?: 0 - - var entity: ClashProfileEntity? - - try { - if (id == 0L) { - entity = ClashProfileEntity( - requireNotNull(request.name), - requireNotNull(request.type), - requireNotNull(request.url), - RandomUtils.fileName(profileDir, ".yaml"), - RandomUtils.fileName(clashDir), - false, - request.interval ?: 0, - 0 - ) - } else { - entity = - profiles.queryProfileById(id) ?: throw NullPointerException("Profile not found") - - if (request.name != null) - entity = entity.copy(name = requireNotNull(request.name)) - - if (request.url != null) - entity = entity.copy(uri = requireNotNull(request.url)) - - if (request.interval != null) - entity = entity.copy(updateInterval = requireNotNull(request.interval)) - } - } catch (e: Exception) { - notifyResultNotification( - false, - getString(R.string.profile_update_failure, "ID:$id") - ) - return - } - - updateNotificationUpdating(entity.name) - - try { - val url = Uri.parse(entity.uri) - - if (url == null || url == Uri.EMPTY) - throw IllegalArgumentException("Invalid url $url") - - downloadProfile(url, profileDir.resolve(entity.file), clashDir.resolve(entity.base)) - - entity = entity.copy(lastUpdate = System.currentTimeMillis()) - - val newId = if (entity.id == 0L) - profiles.getId(profiles.addProfile(entity)) - else - profiles.updateProfile(entity).run { entity.id } - - if (entity.updateInterval > 0) { - val nextRequest = - ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE).withId(newId) - - requireNotNull(getSystemService(AlarmManager::class.java)).set( - AlarmManager.RTC, - entity.lastUpdate + entity.updateInterval, - PendingIntent.getBroadcast( - this, - RandomUtils.nextInt(), - Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) - .setComponent(ClashProfileReceiver::class.componentName) - .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, nextRequest), - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - } - - notifyResultNotification( - true, - getString(R.string.profile_update_completed, entity.name) - ) - } catch (e: Exception) { - notifyResultNotification( - false, - getString(R.string.profile_update_failure, entity.name) - ) - throw e - } - } - - private fun removeProfile(request: ProfileRequest) { - val entity = profiles.queryProfileById(request.id ?: return) ?: return - - clashDir.resolve(entity.base).deleteRecursively() - profileDir.resolve(entity.file).delete() - - profiles.removeProfile(entity.id) - - notifyResultNotification(true, getString(R.string.profile_deleted, entity.name)) - } - - private fun resetProfileUpdateAlarm() { - DefaultThreadPool.submit { - for (entity in profiles.queryProfiles()) { - if (entity.updateInterval <= 0) continue - - val nextRequest = - ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE) - .withId(entity.id) - - requireNotNull(getSystemService(AlarmManager::class.java)).set( - AlarmManager.RTC, - entity.lastUpdate + entity.updateInterval, - PendingIntent.getBroadcast( - this, - RandomUtils.nextInt(), - Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) - .setComponent(ClashProfileReceiver::class.componentName) - .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, nextRequest), - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - } - } - } - - private fun downloadProfile(source: Uri, target: File, baseDir: File) { - if (source.scheme == "content" || source.scheme == "file") { - val fd = contentResolver.openFileDescriptor(source, "r") - ?: throw FileNotFoundException("Unable to open file $source") - - Clash.copyProfile(fd.fd, target, baseDir) - } else { - Clash.downloadProfile(source.toString(), target, baseDir) - } - } -} \ No newline at end of file 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 c562edcb93..1cbaf91ba9 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 @@ -1,51 +1,43 @@ package com.github.kr328.clash.service -import android.app.Service -import android.content.* +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter import android.os.Binder import android.os.IBinder import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.data.ClashDatabase -import com.github.kr328.clash.service.util.DefaultThreadPool -import com.github.kr328.clash.service.util.clashDir -import com.github.kr328.clash.service.util.profileDir -import com.github.kr328.clash.service.util.sendBroadcastSelf +import com.github.kr328.clash.service.util.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel -class ClashService : Service() { +class ClashService : BaseService() { companion object { const val INTENT_EXTRA_START_TUN = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.start.tun" } + private val service = this + private val notification by lazy { ClashNotification(service) } private var stopReason: String? = null - private lateinit var notification: ClashNotification - private val tunConnection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName?) {} - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - require(service != null) - - val tun = service.queryLocalInterface(TunService::class.java.name) as TunService - - tun.startTun().whenComplete { _, u -> - if (u != null) - return@whenComplete stopSelf(u.message ?: "Start tun failure") - - notification.setVpn(true) - } - } - } + private val reloadChannel = Channel(Channel.CONFLATED) private val profileObserver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - reloadProfile() + reloadChannel.offer(Unit) } } override fun onCreate() { super.onCreate() - notification = ClashNotification(this) + launch { + while (isActive) { + reloadChannel.receive() - Clash.initialize(this) + reloadProfile() + } + } } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -53,22 +45,17 @@ class ClashService : Service() { Clash.start() - sendBroadcastSelf(Intent(Intents.INTENT_ACTION_CLASH_STARTED)) + broadcastClashStarted(this) val startVpn = intent?.getBooleanExtra(INTENT_EXTRA_START_TUN, true) ?: true if (startVpn) - bindService( - Intent(this, TunService::class.java) - .setAction(Intents.INTENT_ACTION_BIND_TUN_SERVICE), - tunConnection, - Context.BIND_AUTO_CREATE - ) - - reloadProfile() + startService(TunService::class.intent) registerReceiver(profileObserver, IntentFilter(Intents.INTENT_ACTION_PROFILE_CHANGED)) + reloadChannel.offer(Unit) + return START_NOT_STICKY } @@ -77,38 +64,33 @@ class ClashService : Service() { } override fun onDestroy() { - runCatching { - unbindService(tunConnection) - } + cancel() + Clash.stopTunDevice() Clash.stop() notification.destroy() - sendBroadcastSelf( - Intent(Intents.INTENT_ACTION_CLASH_STOPPED) - .putExtra(Intents.INTENT_EXTRA_CLASH_STOP_REASON, stopReason) - ) + broadcastClashStopped(this, stopReason) unregisterReceiver(profileObserver) super.onDestroy() } - private fun reloadProfile() { - DefaultThreadPool.submit { - val active = ClashDatabase.getInstance(this).openClashProfileDao().queryActiveProfile() - ?: return@submit stopSelf("Empty active profile") + private suspend fun reloadProfile() = withContext(Dispatchers.IO) { + val active = ClashDatabase.getInstance(service).openClashProfileDao().queryActiveProfile() + ?: return@withContext stopSelf("Empty active profile") + try { Clash.loadProfile( profileDir.resolve(active.file), clashDir.resolve(active.base) - ).whenComplete { _, u -> - if (u != null) - return@whenComplete stopSelf(u.message ?: "Load profile failure") - else - notification.setProfile(active.name) - } + ).await() + + notification.setProfile(active.name) + } catch (e: Exception) { + stopSelf("Load profile failure") } } diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt new file mode 100644 index 0000000000..fe83127025 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt @@ -0,0 +1,198 @@ +package com.github.kr328.clash.service + +import android.app.* +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.os.RemoteException +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.data.ClashProfileDao +import com.github.kr328.clash.service.ipc.IStreamCallback +import com.github.kr328.clash.service.ipc.ParcelableContainer +import com.github.kr328.clash.service.transact.ProfileRequest +import com.github.kr328.clash.service.util.RandomUtils +import com.github.kr328.clash.service.util.componentName +import com.github.kr328.clash.service.util.intent +import com.github.kr328.clash.service.util.timeout +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.selects.select + +class ProfileBackgroundService : BaseService() { + companion object { + private const val SERVICE_STATUS_CHANNEL = "profile_service_status" + private const val SERVICE_RESULT_CHANNEL = "profile_service_result" + private const val SERVICE_NOTIFICATION_ID = 10000 + } + + private val channel = Channel(2) + private val queue = mutableListOf>() + private val profiles: ClashProfileDao by lazy { + ClashDatabase.getInstance(this).openClashProfileDao() + } + private val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) {} + + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + val service = IProfileService.Stub.asInterface(binder) ?: return stopSelf() + + launch { + while (isActive) { + val timeout = timeout(1000 * 60L) + + select { + channel.onReceive { + val deferred = CompletableDeferred() + + it.withCallback(object : IStreamCallback.Stub() { + override fun complete() { + deferred.complete(it) + } + + override fun completeExceptionally(reason: String?) { + deferred.completeExceptionally(RemoteException(reason)) + } + + override fun send(data: ParcelableContainer?) {} + }) + + service.enqueueRequest(it) + + queue.add(deferred) + } + if (queue.isNotEmpty()) { + for (task in queue) { + task.onAwait { + queue.remove(task) + } + } + } else { + timeout.onJoin { + stopSelf() + cancel() + } + } + } + + timeout.cancel() + } + } + } + } + + override fun onCreate() { + super.onCreate() + + bindService(ProfileProcessService::class.intent, connection, Context.BIND_AUTO_CREATE) + } + + override fun onDestroy() { + unbindService(connection) + + super.onDestroy() + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + + val request = + intent?.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST) + ?: return START_NOT_STICKY + + channel.offer(request) + + return START_NOT_STICKY + } + + override fun onBind(intent: Intent?): IBinder? { + return Binder() + } + + private fun resetProfileUpdateAlarm() { + for (entity in profiles.queryProfiles()) { + if (entity.updateInterval <= 0) continue + + val nextRequest = + ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE) + .withId(entity.id) + + requireNotNull(getSystemService(AlarmManager::class.java)).set( + AlarmManager.RTC, + entity.lastUpdate + entity.updateInterval, + PendingIntent.getBroadcast( + this, + RandomUtils.nextInt(), + Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) + .setComponent(ProfileRequestReceiver::class.componentName) + .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, nextRequest), + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + } + } + + private fun createNotificationChannels() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + return + + NotificationManagerCompat.from(this).createNotificationChannels( + listOf( + NotificationChannel( + SERVICE_STATUS_CHANNEL, + getText(R.string.profile_service_status_channel), + NotificationManager.IMPORTANCE_LOW + ), + NotificationChannel( + SERVICE_RESULT_CHANNEL, + getText(R.string.profile_service_result), + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + ) + } + + private fun createServiceNotification(content: CharSequence): Notification { + return NotificationCompat.Builder(this, SERVICE_STATUS_CHANNEL) + .setContentTitle(getText(R.string.profile_service_status_title)) + .setContentText(content) + .setSmallIcon(R.drawable.ic_updating) + .setOnlyAlertOnce(true) + .setProgress(Int.MAX_VALUE, 0, true) + .build() + } + + private fun createResultNotification(success: Boolean, content: CharSequence): Notification { + return NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) + .setContentTitle(getText(R.string.profile_process_result)) + .setContentText(content) + .setSmallIcon(if (success) R.drawable.ic_update_completed else R.drawable.ic_update_failure) + .build() + } + + private fun updateNotificationWaiting() { + startForeground( + SERVICE_NOTIFICATION_ID, + createServiceNotification(getText(R.string.profile_service_status_waiting)) + ) + } + + private fun updateNotificationUpdating(profileName: String) { + createServiceNotification( + getString( + R.string.profile_service_status_updating, + profileName + ) + ) + } + + private fun notifyResultNotification(success: Boolean, content: CharSequence) { + NotificationManagerCompat.from(this) + .notify(RandomUtils.nextInt(), createResultNotification(success, content)) + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt new file mode 100644 index 0000000000..2cd4ecdae0 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt @@ -0,0 +1,164 @@ +package com.github.kr328.clash.service + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Intent +import android.net.Uri +import android.os.IBinder +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.data.ClashProfileDao +import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.transact.ProfileRequest +import com.github.kr328.clash.service.util.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import java.io.File +import java.io.FileNotFoundException +import java.util.* + +class ProfileProcessService : BaseService() { + private val service = this + private val queue: MutableMap> = Hashtable() + + private val profiles: ClashProfileDao by lazy { + ClashDatabase.getInstance(service).openClashProfileDao() + } + + override fun onBind(intent: Intent?): IBinder? { + return object : IProfileService.Stub() { + override fun enqueueRequest(request: ProfileRequest?) { + service.enqueueRequest(request ?: return) + } + } + } + + private fun createChannelForRequests(): Channel { + return Channel(Channel.UNLIMITED).also { + launch { + while (isActive) { + val request = withTimeout(1000 * 30) { + it.receive() + } + + handleRequest(request) + } + } + } + } + + private fun enqueueRequest(request: ProfileRequest) { + launch { + queue.computeIfAbsent(request.id) { + createChannelForRequests() + }.send(request) + } + } + + private suspend fun handleRequest(request: ProfileRequest) { + try { + when (request.action) { + ProfileRequest.Action.UPDATE_OR_CREATE -> + handleUpdateOrCreate(request) + ProfileRequest.Action.REMOVE -> + removeProfile(request) + } + + request.callback?.complete() + + broadcastProfileChanged(this) + } catch (e: Exception) { + request.callback?.completeExceptionally(e.message) + } + } + + private suspend fun handleUpdateOrCreate(request: ProfileRequest) { + val id = request.id + + val entity: ClashProfileEntity = + if (id == 0L) { + ClashProfileEntity( + requireNotNull(request.name), + requireNotNull(request.type), + requireNotNull(request.url), + RandomUtils.fileName(profileDir, ".yaml"), + RandomUtils.fileName(clashDir), + false, + 0, + request.interval.takeIf { it >= 0 } ?: 0 + ) + } else { + val e = profiles.queryProfileById(id) ?: return + + e.copy( + name = request.name ?: e.name, + uri = request.url ?: e.uri, + updateInterval = request.interval.takeIf { it >= 0 } ?: e.updateInterval + ) + } + + val url = Uri.parse(entity.uri) + + if (url == null || url == Uri.EMPTY) + throw IllegalArgumentException("Invalid url $url") + + downloadProfile(url, profileDir.resolve(entity.file), clashDir.resolve(entity.base)) + + val newEntity = entity.copy(lastUpdate = System.currentTimeMillis()) + + val newId = if (entity.id == 0L) + profiles.getId(profiles.addProfile(newEntity)) + else + profiles.updateProfile(newEntity).run { entity.id } + + if (entity.updateInterval > 0) { + val nextRequest = + ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE).withId(newId) + + requireNotNull(getSystemService(AlarmManager::class.java)).set( + AlarmManager.RTC, + entity.lastUpdate + entity.updateInterval, + PendingIntent.getBroadcast( + this, + RandomUtils.nextInt(), + Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) + .setComponent(ProfileRequestReceiver::class.componentName) + .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, nextRequest), + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + } + } + + private fun removeProfile(request: ProfileRequest) { + val entity = profiles.queryProfileById(request.id) ?: return + + clashDir.resolve(entity.base).deleteRecursively() + profileDir.resolve(entity.file).delete() + + profiles.removeProfile(entity.id) + } + + private suspend fun downloadProfile(source: Uri, target: File, baseDir: File) { + try { + target.parentFile?.mkdirs() + baseDir.mkdirs() + + if (source.scheme == "content" || source.scheme == "file") { + val fd = contentResolver.openFileDescriptor(source, "r") + ?: throw FileNotFoundException("Unable to open file $source") + + Clash.downloadProfile(fd.fd, target, baseDir).await() + } else { + Clash.downloadProfile(source.toString(), target, baseDir).await() + } + } catch (e: Exception) { + target.delete() + baseDir.deleteRecursively() + + throw e + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashProfileReceiver.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt similarity index 82% rename from service/src/main/java/com/github/kr328/clash/service/ClashProfileReceiver.kt rename to service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt index 7a1430667b..610159797f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashProfileReceiver.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt @@ -6,12 +6,12 @@ import android.content.Intent import android.os.Build import com.github.kr328.clash.service.util.componentName -class ClashProfileReceiver : BroadcastReceiver() { +class ProfileRequestReceiver : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action != Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST || context == null) return - intent.component = ClashProfileService::class.componentName + intent.component = ProfileProcessService::class.componentName if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent) 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 62004bb129..160cff0bfc 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 @@ -1,18 +1,13 @@ package com.github.kr328.clash.service -import android.content.Intent import android.net.VpnService -import android.os.Binder import android.os.Build -import android.os.IBinder -import android.os.IInterface import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.utils.Log -import com.github.kr328.clash.service.net.DefaultNetworkObserver -import com.github.kr328.clash.service.util.DefaultThreadPool -import java.util.concurrent.CompletableFuture +import com.github.kr328.clash.service.net.DefaultNetworkChannel +import kotlinx.coroutines.* -class TunService : VpnService(), IInterface { +class TunService : VpnService(), CoroutineScope by MainScope() { companion object { // from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/bg/VpnService.kt private const val VPN_MTU = 1500 @@ -22,53 +17,30 @@ class TunService : VpnService(), IInterface { private const val VLAN4_ANY = "0.0.0.0" } - private lateinit var defaultNetworkObserver: DefaultNetworkObserver + private lateinit var defaultNetworkChannel: DefaultNetworkChannel private lateinit var settings: Settings - // export to ClashService - fun startTun(): CompletableFuture { - val result = CompletableFuture() - - DefaultThreadPool.submit { - val fd = Builder() - .addAddress() - .addDnsServer(PRIVATE_VLAN_DNS) - .addBypassApplications() - .addBypassPrivateRoute() - .setMtu(VPN_MTU) - .setBlocking(false) - .setMeteredCompat(false) - .establish() - - if (fd == null) { - result.completeExceptionally(NullPointerException("Unable to establish VPN")) - return@submit - } - - val dnsAddress = - if (settings.get(Settings.DNS_HIJACKING)) - "$VLAN4_ANY:53" - else - "$PRIVATE_VLAN_DNS:53" - - Clash.startTunDevice(fd.fd, VPN_MTU, dnsAddress) - - result.complete(Unit) - } - - return result - } - - override fun onBind(intent: Intent?): IBinder? { - if (Intents.INTENT_ACTION_BIND_TUN_SERVICE == intent?.action) { - return object : Binder() { - override fun queryLocalInterface(descriptor: String): IInterface? { - return this@TunService - } - } + private fun startTun() { + val fd = Builder() + .addAddress() + .addDnsServer(PRIVATE_VLAN_DNS) + .addBypassApplications() + .addBypassPrivateRoute() + .setMtu(VPN_MTU) + .setBlocking(false) + .setMeteredCompat(false) + .establish() + ?: throw NullPointerException("Unable to create VPN Service") + + val dnsAddress = + if (settings.get(Settings.DNS_HIJACKING)) + "$VLAN4_ANY:53" + else + "$PRIVATE_VLAN_DNS:53" + + Clash.startTunDevice(fd.fd, VPN_MTU, dnsAddress) { + stopSelf() } - - return super.onBind(intent) } override fun onCreate() { @@ -76,19 +48,27 @@ class TunService : VpnService(), IInterface { settings = Settings(ClashManager(this)) - defaultNetworkObserver = DefaultNetworkObserver(this) { - setUnderlyingNetworks(it?.run { arrayOf(it) }) - } + defaultNetworkChannel = DefaultNetworkChannel(this, this) - defaultNetworkObserver.register() + defaultNetworkChannel.register() + + launch { + withContext(Dispatchers.IO) { + startTun() + } + + while (isActive) { + setUnderlyingNetworks(defaultNetworkChannel.receive()?.let { arrayOf(it) }) + } + } } override fun onDestroy() { - super.onDestroy() + cancel() - Clash.stopTunDevice() + defaultNetworkChannel.unregister() - defaultNetworkObserver.unregister() + super.onDestroy() } private fun Builder.setMeteredCompat(isMetered: Boolean): Builder { @@ -154,12 +134,4 @@ class TunService : VpnService(), IInterface { return this } - - override fun asBinder(): IBinder { - return object : Binder() { - override fun queryLocalInterface(descriptor: String): IInterface? { - return this@TunService - } - } - } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkObserver.kt b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt similarity index 74% rename from service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkObserver.kt rename to service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt index c3e5b4ab40..3d737c90ec 100644 --- a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkObserver.kt +++ b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt @@ -6,35 +6,35 @@ import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest -import android.os.Handler import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.core.utils.Log.handler +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext -class DefaultNetworkObserver(val context: Context, val listener: (Network?) -> Unit) { - private val handler = Handler() +class DefaultNetworkChannel(val context: Context, scope: CoroutineScope): + CoroutineScope by scope, Channel by Channel(Channel.CONFLATED) { private val connectivity = context.getSystemService(ConnectivityManager::class.java)!! private val callback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { - handler.removeMessages(0) - handler.postDelayed({ - listener(rebuildNetworkList()) - }, 500) + launch { + send(rebuildNetworkList()) + } } - override fun onLost(network: Network) { - handler.removeMessages(0) - handler.postDelayed({ - listener(rebuildNetworkList()) - }, 500) + launch { + send(rebuildNetworkList()) + } } - override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { - handler.removeMessages(0) - handler.postDelayed({ - listener(rebuildNetworkList()) - }, 500) + launch { + send(rebuildNetworkList()) + } } } @@ -46,14 +46,13 @@ class DefaultNetworkObserver(val context: Context, val listener: (Network?) -> U connectivity.unregisterNetworkCallback(callback) } - private fun rebuildNetworkList(): Network? { - return try { + private suspend fun rebuildNetworkList(): Network? = withContext(Dispatchers.Default) { + return@withContext try { connectivity.allNetworks - .flatMap { network -> - connectivity.getNetworkCapabilities(network)?.let { listOf(it to network) } - ?: emptyList() - } .asSequence() + .mapNotNull { network -> + connectivity.getNetworkCapabilities(network)?.let { it to network } + } .filterNot { it.first.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || !it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) diff --git a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt index 90651d2a3e..717bc98ba0 100644 --- a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt +++ b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt @@ -19,16 +19,16 @@ class ProfileRequest private constructor(private val bundle: Bundle) : Parcelabl val action: Action get() = Action.valueOf(requireNotNull(bundle.getString(KEY_ACTION))) - val type: Int? - get() = bundle.getInt(KEY_TYPE) - val id: Long? - get() = bundle.getLong(KEY_ID) + val type: Int + get() = bundle.getInt(KEY_TYPE, -1) + val id: Long + get() = bundle.getLong(KEY_ID, 0) val name: String? get() = bundle.getString(KEY_NAME) val url: String? get() = bundle.getString(KEY_URL) - val interval: Long? - get() = bundle.getLong(KEY_UPDATE_INTERVAL) + val interval: Long + get() = bundle.getLong(KEY_UPDATE_INTERVAL, -1) val callback: IStreamCallback? get() = IStreamCallback.Stub.asInterface(bundle.getBinder(KEY_CALLBACK)) diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt index a15f08f5d4..76676b57d8 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt @@ -2,7 +2,33 @@ package com.github.kr328.clash.service.util import android.content.Context import android.content.Intent +import com.github.kr328.clash.service.Intents +import com.github.kr328.clash.service.data.ClashDatabase fun Context.sendBroadcastSelf(intent: Intent) { this.sendBroadcast(intent.setPackage(this.packageName)) +} + +fun broadcastProfileChanged(context: Context) { + val active = ClashDatabase.getInstance(context).openClashProfileDao().queryActiveProfile() + + context.sendBroadcastSelf( + Intent(Intents.INTENT_ACTION_PROFILE_CHANGED).putExtra( + Intents.INTENT_EXTRA_PROFILE_ACTIVE, + active + ) + ) +} + +fun broadcastClashStarted(context: Context) { + context.sendBroadcastSelf(Intent(Intents.INTENT_ACTION_CLASH_STARTED)) +} + +fun broadcastClashStopped(context: Context, reason: String?) { + context.sendBroadcastSelf( + Intent(Intents.INTENT_ACTION_CLASH_STOPPED).putExtra( + Intents.INTENT_EXTRA_CLASH_STOP_REASON, + reason + ) + ) } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/ComponentUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/ComponentUtils.kt index 5654d4eb7c..f6b7956543 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/ComponentUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/ComponentUtils.kt @@ -1,8 +1,12 @@ package com.github.kr328.clash.service.util import android.content.ComponentName +import android.content.Intent import com.github.kr328.clash.core.Global import kotlin.reflect.KClass val KClass<*>.componentName: ComponentName - get() = ComponentName.createRelative(Global.application, this.java.name) \ No newline at end of file + get() = ComponentName.createRelative(Global.application, this.java.name) + +val KClass<*>.intent: Intent + get() = Intent(Global.application, this.java) diff --git a/service/src/main/java/com/github/kr328/clash/service/util/Ticker.kt b/service/src/main/java/com/github/kr328/clash/service/util/Ticker.kt new file mode 100644 index 0000000000..dd3c9bc922 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/Ticker.kt @@ -0,0 +1,15 @@ +package com.github.kr328.clash.service.util + +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.SendChannel + +fun CoroutineScope.ticker(tick: Long, channel: SendChannel): Job { + return launch { + var count = 0 + + while (isActive) { + channel.send(count++) + delay(tick) + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/Timeout.kt b/service/src/main/java/com/github/kr328/clash/service/util/Timeout.kt new file mode 100644 index 0000000000..e19d43703b --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/Timeout.kt @@ -0,0 +1,9 @@ +package com.github.kr328.clash.service.util + +import kotlinx.coroutines.* + +fun CoroutineScope.timeout(timeout: Long): Job { + return launch { + delay(timeout) + } +} \ No newline at end of file From f76bed2f8b5e61e6616c44c8062e30126ac4ec9c Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 9 Feb 2020 00:53:40 +0800 Subject: [PATCH 078/358] add profile process service --- .../clash/service/ProfileBackgroundService.kt | 211 +++++++++++------- .../clash/service/ProfileProcessService.kt | 2 + .../clash/service/transact/ProfileRequest.kt | 18 -- .../clash/service/util/DefaultThreadPool.kt | 6 - ...ate_completed.xml => ic_update_normal.xml} | 0 service/src/main/res/values/strings.xml | 15 +- 6 files changed, 142 insertions(+), 110 deletions(-) delete mode 100644 service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt rename service/src/main/res/drawable/{ic_update_completed.xml => ic_update_normal.xml} (100%) diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt index fe83127025..0d0ecd4295 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt @@ -1,6 +1,9 @@ package com.github.kr328.clash.service -import android.app.* +import android.app.AlarmManager +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent import android.content.ComponentName import android.content.Context import android.content.Intent @@ -8,7 +11,6 @@ import android.content.ServiceConnection import android.os.Binder import android.os.Build import android.os.IBinder -import android.os.RemoteException import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.github.kr328.clash.service.data.ClashDatabase @@ -20,92 +22,72 @@ import com.github.kr328.clash.service.util.RandomUtils import com.github.kr328.clash.service.util.componentName import com.github.kr328.clash.service.util.intent import com.github.kr328.clash.service.util.timeout -import kotlinx.coroutines.* +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.selects.select class ProfileBackgroundService : BaseService() { companion object { private const val SERVICE_STATUS_CHANNEL = "profile_service_status" private const val SERVICE_RESULT_CHANNEL = "profile_service_result" - private const val SERVICE_NOTIFICATION_ID = 10000 + private const val SERVICE_NOTIFICATION_ID_BASE = 10000 } + private val database by lazy { ClashDatabase.getInstance(this).openClashProfileDao() } private val channel = Channel(2) - private val queue = mutableListOf>() + private val queue = mutableListOf>() private val profiles: ClashProfileDao by lazy { ClashDatabase.getInstance(this).openClashProfileDao() } private val connection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName?) {} + override fun onServiceDisconnected(name: ComponentName?) { + channel.close() + stopSelf() + } override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { val service = IProfileService.Stub.asInterface(binder) ?: return stopSelf() - launch { - while (isActive) { - val timeout = timeout(1000 * 60L) - - select { - channel.onReceive { - val deferred = CompletableDeferred() - - it.withCallback(object : IStreamCallback.Stub() { - override fun complete() { - deferred.complete(it) - } - - override fun completeExceptionally(reason: String?) { - deferred.completeExceptionally(RemoteException(reason)) - } - - override fun send(data: ParcelableContainer?) {} - }) - - service.enqueueRequest(it) - - queue.add(deferred) - } - if (queue.isNotEmpty()) { - for (task in queue) { - task.onAwait { - queue.remove(task) - } - } - } else { - timeout.onJoin { - stopSelf() - cancel() - } - } - } - - timeout.cancel() - } - } + startProfileProcessor(service) } } override fun onCreate() { super.onCreate() + createNotificationChannels() + + startForeground() + bindService(ProfileProcessService::class.intent, connection, Context.BIND_AUTO_CREATE) } override fun onDestroy() { + super.onDestroy() + unbindService(connection) - super.onDestroy() + stopForeground(true) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) - val request = - intent?.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST) - ?: return START_NOT_STICKY - - channel.offer(request) + when (intent?.action) { + Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST -> { + val request = + intent.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST) + ?: return START_NOT_STICKY + if (request.id != 0L) + channel.offer(request) + } + Intents.INTENT_ACTION_PROFILE_SETUP -> { + resetProfileUpdateAlarm() + } + } return START_NOT_STICKY } @@ -114,6 +96,54 @@ class ProfileBackgroundService : BaseService() { return Binder() } + private fun startProfileProcessor(service: IProfileService) { + launch { + while (isActive) { + val timeout = timeout(1000 * 60L) + + select { + channel.onReceive { + val deferred = CompletableDeferred() + + it.withCallback(object : IStreamCallback.Stub() { + override fun complete() { + deferred.complete(it) + updateUpdateComplete(it.id) + } + + override fun completeExceptionally(reason: String?) { + deferred.complete(it) + updateUpdateFailure(it.id) + } + + override fun send(data: ParcelableContainer?) { + updateUpdating(it.id) + } + }) + + service.enqueueRequest(it) + + queue.add(deferred) + } + if (queue.isNotEmpty()) { + for (task in queue) { + task.onAwait { + queue.remove(task) + } + } + } else { + timeout.onJoin { + stopSelf() + cancel() + } + } + } + + timeout.cancel() + } + } + } + private fun resetProfileUpdateAlarm() { for (entity in profiles.queryProfiles()) { if (entity.updateInterval <= 0) continue @@ -150,49 +180,74 @@ class ProfileBackgroundService : BaseService() { ), NotificationChannel( SERVICE_RESULT_CHANNEL, - getText(R.string.profile_service_result), + getText(R.string.profile_status_channel), NotificationManager.IMPORTANCE_DEFAULT ) ) ) } - private fun createServiceNotification(content: CharSequence): Notification { - return NotificationCompat.Builder(this, SERVICE_STATUS_CHANNEL) + private fun startForeground() { + val notification = NotificationCompat.Builder(this, SERVICE_STATUS_CHANNEL) .setContentTitle(getText(R.string.profile_service_status_title)) - .setContentText(content) .setSmallIcon(R.drawable.ic_updating) .setOnlyAlertOnce(true) - .setProgress(Int.MAX_VALUE, 0, true) .build() + + startForeground(SERVICE_NOTIFICATION_ID_BASE, notification) } - private fun createResultNotification(success: Boolean, content: CharSequence): Notification { - return NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) - .setContentTitle(getText(R.string.profile_process_result)) - .setContentText(content) - .setSmallIcon(if (success) R.drawable.ic_update_completed else R.drawable.ic_update_failure) + private fun updateUpdating(id: Long) { + val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() + val entity = database.queryProfileById(id) ?: return + + val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) + .setContentTitle(getText(R.string.profile_status_title)) + .setContentText(getString(R.string.profile_status_updating, entity.name)) + .setSmallIcon(R.drawable.ic_update_normal) + .setOngoing(true) .build() - } - private fun updateNotificationWaiting() { - startForeground( - SERVICE_NOTIFICATION_ID, - createServiceNotification(getText(R.string.profile_service_status_waiting)) - ) + NotificationManagerCompat.from(this) + .notify(SERVICE_NOTIFICATION_ID_BASE + notificationId, notification) } - private fun updateNotificationUpdating(profileName: String) { - createServiceNotification( - getString( - R.string.profile_service_status_updating, - profileName - ) - ) + private fun updateUpdateComplete(id: Long) { + val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() + val entity = database.queryProfileById(id) + + if (entity == null) { + NotificationManagerCompat.from(this).cancel(notificationId) + return + } + + val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) + .setContentTitle(getText(R.string.profile_status_title)) + .setContentText(getString(R.string.profile_status_update_completed, entity.name)) + .setSmallIcon(R.drawable.ic_update_normal) + .build() + + NotificationManagerCompat.from(this) + .notify(SERVICE_NOTIFICATION_ID_BASE + notificationId, notification) } - private fun notifyResultNotification(success: Boolean, content: CharSequence) { + private fun updateUpdateFailure(id: Long) { + val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() + val entity = database.queryProfileById(id) + + if (entity == null) { + NotificationManagerCompat.from(this).cancel(notificationId) + return + } + + val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) + .setContentTitle(getText(R.string.profile_status_title)) + .setContentText(getString(R.string.profile_status_update_failure, entity.name)) + .setSmallIcon(R.drawable.ic_update_normal) + .setOngoing(true) + .build() + NotificationManagerCompat.from(this) - .notify(RandomUtils.nextInt(), createResultNotification(success, content)) + .notify(SERVICE_NOTIFICATION_ID_BASE + notificationId, notification) } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt index 2cd4ecdae0..4732082b6f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt @@ -59,6 +59,8 @@ class ProfileProcessService : BaseService() { private suspend fun handleRequest(request: ProfileRequest) { try { + request.callback?.send(null) + when (request.action) { ProfileRequest.Action.UPDATE_OR_CREATE -> handleUpdateOrCreate(request) diff --git a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt index 717bc98ba0..b604ccf9b8 100644 --- a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt +++ b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt @@ -74,24 +74,6 @@ class ProfileRequest private constructor(private val bundle: Bundle) : Parcelabl } } - override fun equals(other: Any?): Boolean { - if (other !is ProfileRequest) - return false - - for (key in bundle.keySet()) { - if (bundle.get(key) != other.bundle.get(key)) - return false - } - - return true - } - - override fun hashCode(): Int { - return bundle.keySet() - .joinToString { it + bundle.get(it)?.hashCode() } - .hashCode() - } - override fun writeToParcel(parcel: Parcel, flags: Int) { parcel.writeBundle(bundle) } diff --git a/service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt b/service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt deleted file mode 100644 index 85a77830b2..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/util/DefaultThreadPool.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.kr328.clash.service.util - -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors - -val DefaultThreadPool: ExecutorService = Executors.newCachedThreadPool() \ No newline at end of file diff --git a/service/src/main/res/drawable/ic_update_completed.xml b/service/src/main/res/drawable/ic_update_normal.xml similarity index 100% rename from service/src/main/res/drawable/ic_update_completed.xml rename to service/src/main/res/drawable/ic_update_normal.xml diff --git a/service/src/main/res/values/strings.xml b/service/src/main/res/values/strings.xml index df3547d9cf..d5ff463f74 100644 --- a/service/src/main/res/values/strings.xml +++ b/service/src/main/res/values/strings.xml @@ -14,14 +14,13 @@ "%1$s↑\t%2$s↓" Profile Service Status - Processing Profiles - Waiting Request - Updating %s - Profile Service Result - Process Result - Update %s Completed - Update %s Failure - Profile %s deleted + Profile Updater Running + + Profile Processing Status + Processing Profile + Updating %s + Update %s Failure + Update %s Completed Clash Core Service Tun Device Service From 33fbd378c0349f18cea20ba10b69e0a997fd7188 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 11 Feb 2020 16:20:36 +0800 Subject: [PATCH 079/358] [WIP] UI logic refactor --- app/src/main/AndroidManifest.xml | 10 + .../github/kr328/clash/ApkBrokenActivity.kt | 15 ++ .../com/github/kr328/clash/BaseActivity.kt | 15 +- .../java/com/github/kr328/clash/Constants.kt | 14 + .../kr328/clash/CreateProfileActivity.kt | 129 +++++++++ .../com/github/kr328/clash/MainApplication.kt | 4 +- .../github/kr328/clash/ProfileEditActivity.kt | 250 ++++++++++++++++++ .../github/kr328/clash/ProfilesActivity.kt | 21 +- .../kr328/clash/adapter/ProfileAdapter.kt | 57 ++-- .../com/github/kr328/clash/remote/Calls.kt | 8 +- .../github/kr328/clash/remote/ClashClient.kt | 78 +----- .../kr328/clash/remote/ProfileClient.kt | 21 ++ .../com/github/kr328/clash/remote/Remote.kt | 142 ++++++++++ .../github/kr328/clash/utils/IntervalUtils.kt | 34 +++ app/src/main/res/drawable/ic_boot.xml | 2 +- app/src/main/res/drawable/ic_cloud.xml | 2 +- app/src/main/res/drawable/ic_content.xml | 9 + ...ic_new_profile_url.xml => ic_download.xml} | 2 +- .../{ic_new_profile_file.xml => ic_file.xml} | 2 +- app/src/main/res/drawable/ic_input.xml | 9 + app/src/main/res/drawable/ic_label.xml | 9 + .../main/res/drawable/ic_label_outline.xml | 9 + app/src/main/res/drawable/ic_link.xml | 2 +- app/src/main/res/drawable/ic_logo.xml | 3 +- .../main/res/drawable/ic_settings_color.xml | 2 +- app/src/main/res/drawable/ic_update.xml | 9 + ...rofile.xml => activity_create_profile.xml} | 8 +- app/src/main/res/layout/activity_feedback.xml | 5 +- .../main/res/layout/activity_import_file.xml | 50 ---- .../main/res/layout/activity_import_url.xml | 50 ---- app/src/main/res/layout/activity_logs.xml | 6 +- app/src/main/res/layout/activity_main.xml | 14 +- .../main/res/layout/activity_profile_edit.xml | 54 ++++ app/src/main/res/layout/activity_profiles.xml | 2 +- app/src/main/res/layout/adapter_log.xml | 9 +- .../res/layout/adapter_profile_entity.xml | 10 +- .../res/layout/adapter_profile_footer.xml | 4 +- .../main/res/layout/adapter_proxy_header.xml | 2 +- .../main/res/layout/adapter_url_provider.xml | 36 +++ app/src/main/res/layout/dialog_about.xml | 7 +- .../main/res/layout/view_radio_fat_item.xml | 2 +- app/src/main/res/values-night/colors.xml | 2 +- app/src/main/res/values/colors.xml | 4 +- app/src/main/res/values/strings.xml | 29 ++ app/src/main/res/values/styles.xml | 4 +- app/src/main/res/xml-zh/feedback.xml | 29 -- core/build.gradle | 3 + .../com/github/kr328/clash/core/utils/Log.kt | 41 +-- design/build.gradle | 10 +- .../kr328/clash/design/settings/Base.kt | 36 +++ .../clash/design/settings/SettingsBuilder.kt | 32 +++ .../clash/design/settings/SettingsScreen.kt | 51 ++++ .../kr328/clash/design/settings/TextInput.kt | 112 ++++++++ .../clash/design/view/ColorfulTextCard.kt | 18 +- .../kr328/clash/design/view/SettingsLayout.kt | 27 ++ .../kr328/clash/design/view/TextCard.kt | 1 - .../main/res/drawable/ic_demo_drawable.xml | 9 + .../src/main/res/drawable/ic_edit.xml | 2 +- .../src/main/res/layout/dialog_input_text.xml | 14 + .../res/layout/view_setting_text_input.xml | 38 ++- design/src/main/res/values/strings.xml | 4 +- service/src/main/AndroidManifest.xml | 2 +- .../kr328/clash/service/IClashManager.aidl | 1 - .../kr328/clash/service/IProfileService.aidl | 6 + .../kr328/clash/service/ClashManager.kt | 4 - .../clash/service/ProfileBackgroundService.kt | 2 +- .../clash/service/ProfileProcessService.kt | 166 ------------ .../clash/service/ProfileRequestReceiver.kt | 2 +- .../kr328/clash/service/ProfileService.kt | 187 +++++++++++++ .../service/data/ClashDatabaseMigrations.kt | 4 +- .../clash/service/data/ClashProfileEntity.kt | 6 +- .../service/net/DefaultNetworkChannel.kt | 1 - .../clash/service/transact/ProfileRequest.kt | 15 +- 73 files changed, 1442 insertions(+), 537 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/ApkBrokenActivity.kt create mode 100644 app/src/main/java/com/github/kr328/clash/Constants.kt create mode 100644 app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt create mode 100644 app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt create mode 100644 app/src/main/java/com/github/kr328/clash/remote/Remote.kt create mode 100644 app/src/main/java/com/github/kr328/clash/utils/IntervalUtils.kt create mode 100644 app/src/main/res/drawable/ic_content.xml rename app/src/main/res/drawable/{ic_new_profile_url.xml => ic_download.xml} (89%) rename app/src/main/res/drawable/{ic_new_profile_file.xml => ic_file.xml} (87%) create mode 100644 app/src/main/res/drawable/ic_input.xml create mode 100644 app/src/main/res/drawable/ic_label.xml create mode 100644 app/src/main/res/drawable/ic_label_outline.xml create mode 100644 app/src/main/res/drawable/ic_update.xml rename app/src/main/res/layout/{activity_new_profile.xml => activity_create_profile.xml} (79%) delete mode 100644 app/src/main/res/layout/activity_import_file.xml delete mode 100644 app/src/main/res/layout/activity_import_url.xml create mode 100644 app/src/main/res/layout/activity_profile_edit.xml create mode 100644 app/src/main/res/layout/adapter_url_provider.xml delete mode 100644 app/src/main/res/xml-zh/feedback.xml create mode 100644 design/src/main/java/com/github/kr328/clash/design/settings/Base.kt create mode 100644 design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt create mode 100644 design/src/main/java/com/github/kr328/clash/design/settings/SettingsScreen.kt create mode 100644 design/src/main/java/com/github/kr328/clash/design/settings/TextInput.kt create mode 100644 design/src/main/java/com/github/kr328/clash/design/view/SettingsLayout.kt create mode 100644 design/src/main/res/drawable/ic_demo_drawable.xml rename {app => design}/src/main/res/drawable/ic_edit.xml (89%) create mode 100644 design/src/main/res/layout/dialog_input_text.xml rename app/src/main/res/layout/adapter_form_text.xml => design/src/main/res/layout/view_setting_text_input.xml (56%) delete mode 100644 service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ProfileService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 46ed2e6d9c..1cf97fbf42 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,6 +26,11 @@ + + diff --git a/app/src/main/java/com/github/kr328/clash/ApkBrokenActivity.kt b/app/src/main/java/com/github/kr328/clash/ApkBrokenActivity.kt new file mode 100644 index 0000000000..6e2373b738 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/ApkBrokenActivity.kt @@ -0,0 +1,15 @@ +package com.github.kr328.clash + +class ApkBrokenActivity : BaseActivity() { + + override fun onBackPressed() { + super.onBackPressed() + + finishAffinity() + finish() + } + + override fun shouldDisplayHomeAsUpEnabled(): Boolean { + return false + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 704a89b40e..73ac728b57 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -7,13 +7,16 @@ import android.content.res.Configuration import android.os.Build import android.os.Bundle import android.view.LayoutInflater +import android.view.View import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR import android.view.ViewGroup import android.widget.FrameLayout +import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.service.data.ClashProfileEntity +import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel @@ -44,8 +47,12 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() } } + private var overrideRootView: View? = null + val clashRunning: Boolean get() = Broadcasts.clashRunning + val rootView: View + get() = overrideRootView ?: window.decorView open suspend fun onClashStarted() {} open suspend fun onClashStopped(reason: String?) {} @@ -67,7 +74,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() } } - LayoutInflater.from(this).inflate(layoutResID, base, true) + overrideRootView = LayoutInflater.from(this).inflate(layoutResID, base, true) super.setContentView(base) } @@ -118,6 +125,12 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() recreate() } + protected fun makeSnackbarException(title: String, detail: String) { + Snackbar.make(rootView, title, Snackbar.LENGTH_LONG).setAction(R.string.detail) { + AlertDialog.Builder(this).setTitle(R.string.detail).setMessage(detail).show() + }.show() + } + private fun resetLightNavigationBar() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return diff --git a/app/src/main/java/com/github/kr328/clash/Constants.kt b/app/src/main/java/com/github/kr328/clash/Constants.kt new file mode 100644 index 0000000000..60b172d784 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/Constants.kt @@ -0,0 +1,14 @@ +package com.github.kr328.clash + +object Constants { + const val PREFERENCE_NAME_APP = "app" + const val PREFERENCE_KEY_LAST_INSTALL = "last_install" + + const val URL_PROVIDER_TYPE_FILE = "file" + const val URL_PROVIDER_TYPE_URL = "url" + const val URL_PROVIDER_TYPE_EXTERNAL = "external" + + const val URL_PROVIDER_INTENT_ACTION = "com.github.kr328.clash.action.PROVIDE_URL" + + const val URL_PROVIDER_INTENT_EXTRA_NAME = "name" +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt index 8bc8476917..67fba865ae 100644 --- a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt @@ -1,6 +1,135 @@ package com.github.kr328.clash +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.graphics.drawable.Drawable +import android.net.Uri import android.os.Bundle +import android.provider.Settings +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.TextView +import com.github.kr328.clash.service.util.intent +import kotlinx.android.synthetic.main.activity_create_profile.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class CreateProfileActivity : BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_create_profile) + + setSupportActionBar(toolbar) + + launch { + val providers = queryUrlProviders() + + mainList.adapter = Adapter(this@CreateProfileActivity, providers) + mainList.divider = null + mainList.dividerHeight = 0 + + mainList.setOnItemClickListener { _, _, position, _ -> + val item = providers[position] + + startActivity( + ProfileEditActivity::class.intent + .putExtra("type", item.type) + .putExtra("intent", item.intent) + ) + } + mainList.setOnItemLongClickListener { _, _, position, _ -> + val item = providers[position] + val packageName = item.intent?.component?.packageName + + if (packageName != null) { + val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) + .setData(Uri.fromParts("package", packageName, null)) + + startActivity(intent) + + true + } else { + false + } + } + } + } + + private suspend fun queryUrlProviders(): List = + withContext(Dispatchers.IO) { + val common = listOf( + UrlProvider( + getText(R.string.file), + getText(R.string.import_from_file), + getDrawable(R.drawable.ic_file)!!, + Constants.URL_PROVIDER_TYPE_FILE, + null + ), + UrlProvider( + getText(R.string.url), + getText(R.string.import_from_url), + getDrawable(R.drawable.ic_download)!!, + Constants.URL_PROVIDER_TYPE_URL, + null + ) + ) + + val providers = packageManager.queryIntentActivities( + Intent(Constants.URL_PROVIDER_INTENT_ACTION), + 0 + ).map { + val activity = it.activityInfo + + val name = activity.applicationInfo.loadLabel(packageManager) + val summary = activity.loadLabel(packageManager) + val icon = activity.loadIcon(packageManager) + val type = Constants.URL_PROVIDER_TYPE_EXTERNAL + val intent = Intent(Constants.URL_PROVIDER_INTENT_ACTION) + .setComponent( + ComponentName.createRelative( + activity.packageName, + activity.name + ) + ) + + UrlProvider(name, summary, icon, type, intent) + } + + common + providers + } + + private data class UrlProvider( + val name: CharSequence, + val summary: CharSequence, + val icon: Drawable, + val type: String, + val intent: Intent? + ) + + private class Adapter(private val context: Context, private val providers: List) : + BaseAdapter() { + override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + val provider = providers[position] + val view = convertView ?: LayoutInflater.from(context).inflate( + R.layout.adapter_url_provider, + parent, + false + ) + + view.findViewById(android.R.id.title).text = provider.name + view.findViewById(android.R.id.summary).text = provider.summary + view.findViewById(android.R.id.icon).background = provider.icon + + return view + } + + override fun getItem(position: Int): Any = providers[position] + override fun getItemId(position: Int): Long = providers[position].hashCode().toLong() + override fun getCount(): Int = providers.size + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 583f3aa253..cc00f7f27c 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -5,7 +5,7 @@ import android.content.Context import com.crashlytics.android.Crashlytics import com.github.kr328.clash.core.Global import com.github.kr328.clash.remote.Broadcasts -import com.github.kr328.clash.remote.ClashClient +import com.github.kr328.clash.remote.Remote import com.google.firebase.FirebaseApp import io.fabric.sdk.android.Fabric @@ -27,7 +27,7 @@ class MainApplication : Application() { Fabric.with(this, Crashlytics()) } - ClashClient.init(this) + Remote.init(this) Broadcasts.init(this) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt new file mode 100644 index 0000000000..96449bb27e --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt @@ -0,0 +1,250 @@ +package com.github.kr328.clash + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import android.webkit.MimeTypeMap +import androidx.appcompat.app.AlertDialog +import com.github.kr328.clash.design.settings.TextInput +import com.github.kr328.clash.remote.withProfile +import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.ipc.IStreamCallback +import com.github.kr328.clash.service.ipc.ParcelableContainer +import com.github.kr328.clash.service.transact.ProfileRequest +import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.activity_profile_edit.* +import kotlinx.coroutines.launch +import java.lang.IllegalArgumentException + +class ProfileEditActivity : BaseActivity() { + companion object { + private const val REQUEST_CODE = 10000 + + private const val KEY_NAME = "name" + private const val KEY_URL = "url" + private const val KEY_AUTO_UPDATE = "auto_update" + + private val TYPE_YAML = MimeTypeMap.getSingleton() + .getMimeTypeFromExtension("yaml") ?: "*/*" + } + + private var modified = false + private var processing = false + set(value) { + field = value + + if ( value ) { + saving.visibility = View.VISIBLE + save.visibility = View.INVISIBLE + } + else { + saving.visibility = View.INVISIBLE + save.visibility = View.VISIBLE + } + } + + private val requestCallback = object : IStreamCallback.Stub() { + override fun complete() { + launch { + setResult(Activity.RESULT_OK) + finish() + } + } + + override fun completeExceptionally(reason: String?) { + launch { + makeSnackbarException(getString(R.string.invalid_profile), reason ?: "Unknown") + processing = false + } + } + + override fun send(data: ParcelableContainer?) {} + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_profile_edit) + setSupportActionBar(toolbar) + + settings.build { + textInput( + title = getString(R.string.name), + icon = getDrawable(R.drawable.ic_label_outline), + hint = getString(R.string.profile_name), + id = KEY_NAME + ) { + onTextChanged { + modified = true + } + } + textInput( + title = getString(R.string.url), + icon = getDrawable(R.drawable.ic_content), + hint = getString(R.string.profile_url), + id = KEY_URL + ) { + onOpenInput { + if (!openUrlProvider()) + openDialogInput() + } + onDisplayContent { + it.split("/").last() + } + onTextChanged { + modified = true + } + } + textInput( + title = getString(R.string.auto_update), + icon = getDrawable(R.drawable.ic_update), + hint = getString(R.string.in_minutes), + id = KEY_AUTO_UPDATE + ) { + onDisplayContent { + val interval = it.toString().toIntOrNull() ?: 0 + + if (interval <= 0) + getString(R.string.disabled) + else + getString(R.string.format_minutes, interval) + } + onTextChanged { + val s = it.toString() + + if (s.isNotEmpty() && s.toIntOrNull() == null) { + content = "" + Snackbar.make(rootView, R.string.invalid_interval, Snackbar.LENGTH_LONG) + .show() + } else { + modified = true + } + } + } + } + + settings.screen.restoreState(savedInstanceState) + + save.setOnClickListener { + with(settings.screen) { + val name = requireElement(KEY_NAME).content.toString() + val url = Uri.parse(requireElement(KEY_URL).content.toString()) + val interval = requireElement(KEY_AUTO_UPDATE).content.toString() + .toLongOrNull()?.minus(60) ?: 0 + + if ( name.isBlank() ) { + Snackbar.make(rootView, R.string.empty_name, Snackbar.LENGTH_LONG).show() + return@setOnClickListener + } + + if ( url == null || url == Uri.EMPTY || + (url.scheme != "http" && url.scheme != "https" && url.scheme != "content" )) { + Snackbar.make(rootView, R.string.invalid_url, Snackbar.LENGTH_LONG).show() + return@setOnClickListener + } + + processing = true + + sendProfileRequest(name, url, interval) + } + } + + openUrlProvider() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE) { + if (resultCode != Activity.RESULT_OK || data == null) + return + + data.data?.apply { + settings.screen.requireElement(KEY_URL).content = this.toString() + } + + data.getStringExtra(Constants.URL_PROVIDER_INTENT_EXTRA_NAME)?.also { + settings.screen.requireElement(KEY_NAME).apply { + if (content.isBlank()) + content = it + } + } + } + + super.onActivityResult(requestCode, resultCode, data) + } + + override fun onBackPressed() { + if (!modified) + return super.onBackPressed() + + if (processing) { + Snackbar.make(rootView, R.string.processing, Snackbar.LENGTH_LONG).show() + return + } + + AlertDialog.Builder(this) + .setTitle(R.string.exit_without_save) + .setMessage(R.string.exit_without_save_warning) + .setNegativeButton(R.string.cancel) { _, _ -> } + .setPositiveButton(R.string.ok) { _, _ -> finish() } + .show() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + + settings.screen.saveState(outState) + } + + private fun openUrlProvider(): Boolean { + val type = intent.getStringExtra("type") + val externalIntent = intent.getParcelableExtra("intent") + + when (type) { + Constants.URL_PROVIDER_TYPE_FILE -> + startActivityForResult( + Intent(Intent.ACTION_GET_CONTENT).setType(TYPE_YAML), + REQUEST_CODE + ) + Constants.URL_PROVIDER_TYPE_EXTERNAL -> + startActivityForResult( + externalIntent ?: throw NullPointerException(), + REQUEST_CODE + ) + else -> return false + } + + return true + } + + private fun sendProfileRequest(name: String, url: Uri, interval: Long) { + launch { + val source = intent?.getParcelableExtra("intent")?.toUri(0)?.run(Uri::parse) + val type = when( intent?.getStringExtra("type") ) { + Constants.URL_PROVIDER_TYPE_FILE -> { + ClashProfileEntity.TYPE_FILE + } + Constants.URL_PROVIDER_TYPE_URL -> { + ClashProfileEntity.TYPE_URL + } + Constants.URL_PROVIDER_TYPE_EXTERNAL -> { + ClashProfileEntity.TYPE_EXTERNAL + } + else -> throw IllegalArgumentException() + } + + val request = ProfileRequest() + .action(ProfileRequest.Action.UPDATE_OR_CREATE) + .withName(name) + .withURL(url) + .withUpdateInterval(interval) + .withCallback(requestCallback) + .withType(type) + .withSource(source) + + withProfile { + enqueueRequest(request) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index bd16d19e4b..90993baa3f 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -3,15 +3,16 @@ package com.github.kr328.clash import android.os.Bundle import androidx.recyclerview.widget.LinearLayoutManager import com.github.kr328.clash.adapter.ProfileAdapter -import com.github.kr328.clash.remote.withClash +import com.github.kr328.clash.remote.withProfile import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.util.intent import kotlinx.android.synthetic.main.activity_profiles.* import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch -class ProfilesActivity : BaseActivity() { +class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback { private var backgroundJob: Job? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -20,7 +21,7 @@ class ProfilesActivity : BaseActivity() { setSupportActionBar(toolbar) mainList.layoutManager = LinearLayoutManager(this) - mainList.adapter = ProfileAdapter(this) + mainList.adapter = ProfileAdapter(this, this) } override fun onStart() { @@ -52,11 +53,23 @@ class ProfilesActivity : BaseActivity() { } private suspend fun reloadProfiles() { - val profiles = withClash { + val profiles = withProfile { queryProfiles() } (mainList.adapter as ProfileAdapter) .setEntitiesAsync(profiles.toList()) } + + override fun onProfileClicked(entity: ClashProfileEntity) { + + } + + override fun onMenuClicked(entity: ClashProfileEntity) { + + } + + override fun onNewProfile() { + startActivity(CreateProfileActivity::class.intent) + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt index 83839c8e8c..e5c9018466 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt @@ -10,14 +10,20 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.R import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.utils.IntervalUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import java.util.* -class ProfileAdapter(private val context: Context) : +class ProfileAdapter(private val context: Context, private val callback: Callback) : RecyclerView.Adapter() { - var entities: List = emptyList() - private set + interface Callback { + fun onProfileClicked(entity: ClashProfileEntity) + fun onMenuClicked(entity: ClashProfileEntity) + fun onNewProfile() + } + + private var entities: List = emptyList() class EntityHolder(view: View) : RecyclerView.ViewHolder(view) { val root: View = view.findViewById(R.id.root) @@ -40,7 +46,7 @@ class ProfileAdapter(private val context: Context) : val result = withContext(Dispatchers.Default) { DiffUtil.calculateDiff(object : DiffUtil.Callback() { override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean = - old[oldItemPosition] === new[newItemPosition] + old[oldItemPosition].id == new[newItemPosition].id override fun areContentsTheSame( oldItemPosition: Int, @@ -53,7 +59,7 @@ class ProfileAdapter(private val context: Context) : } withContext(Dispatchers.Main) { - entities = old + entities = new result.dispatchUpdatesTo(this@ProfileAdapter) } } @@ -97,43 +103,36 @@ class ProfileAdapter(private val context: Context) : holder.name.text = current.name holder.type.text = getTypeName(current.type) holder.interval.text = offsetDate(current.lastUpdate) + + holder.root.setOnClickListener { + callback.onProfileClicked(current) + } + holder.menu.setOnClickListener { + callback.onMenuClicked(current) + } } is FooterHolder -> { - + holder.root.setOnClickListener { + callback.onNewProfile() + } } } } private fun getTypeName(type: Int): CharSequence { return when (type) { - ClashProfileEntity.TYPE_LOCAL -> - context.getText(R.string.local) - ClashProfileEntity.TYPE_REMOTE -> - context.getText(R.string.remote) + ClashProfileEntity.TYPE_FILE -> + context.getText(R.string.file) + ClashProfileEntity.TYPE_URL -> + context.getText(R.string.url) + ClashProfileEntity.TYPE_EXTERNAL -> + context.getText(R.string.external) else -> context.getText(R.string.unknown) } } private fun offsetDate(date: Long): CharSequence { - val current = Calendar.getInstance() - val base = Calendar.getInstance().apply { - timeInMillis = date - } - - val year = current.get(Calendar.YEAR) - base.get(Calendar.YEAR) - val month = current.get(Calendar.MONTH) - base.get(Calendar.MONTH) - val day = current.get(Calendar.DAY_OF_YEAR) - base.get(Calendar.DAY_OF_YEAR) - val hour = current.get(Calendar.HOUR) - base.get(Calendar.HOUR) - val minute = current.get(Calendar.MINUTE) - base.get(Calendar.MINUTE) - - return when { - year > 0 -> context.getString(R.string.format_years, year) - month > 0 -> context.getString(R.string.format_months, month) - day > 0 -> context.getString(R.string.format_days, day) - hour > 0 -> context.getString(R.string.format_hours, hour) - minute > 0 -> context.getString(R.string.format_minutes, minute) - else -> context.getText(R.string.recently) - } + return IntervalUtils.intervalString(System.currentTimeMillis() - date) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt index d0b7dffe52..5e477fdc5c 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Calls.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Calls.kt @@ -1,7 +1,13 @@ package com.github.kr328.clash.remote suspend fun withClash(block: suspend ClashClient.() -> T): T { - val client = ClashClient.clashInstanceChannel.receive() + val client = Remote.clash.receive() + + return client.block() +} + +suspend fun withProfile(block: suspend ProfileClient.() -> T): T { + val client = Remote.profile.receive() return client.block() } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt index 8448dabcee..131f73e5f3 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -1,72 +1,15 @@ package com.github.kr328.clash.remote -import android.app.Application -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder import android.os.RemoteException -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.ProcessLifecycleOwner -import com.github.kr328.clash.core.event.LogEvent import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.ProxyGroup -import com.github.kr328.clash.service.ClashManagerService import com.github.kr328.clash.service.IClashManager -import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.ipc.IStreamCallback -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ReceiveChannel +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class ClashClient(private val service: IClashManager) { - companion object { - var clashInstanceChannel = Channel() - - private val connection = object : ServiceConnection { - var instance: ClashClient? = null - var job: Job? = null - - override fun onServiceDisconnected(name: ComponentName?) { - job?.cancel() - instance?.close() - instance = null - } - - override fun onServiceConnected(name: ComponentName?, service: IBinder?) { - if (service != null) - instance = ClashClient(IClashManager.Stub.asInterface(service)) - - job = GlobalScope.launch { - while (isActive) { - val clash = instance ?: return@launch - clashInstanceChannel.send(clash) - } - } - } - } - - fun init(application: Application) { - ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { - override fun onStart(owner: LifecycleOwner) { - application.bindService( - Intent(application, ClashManagerService::class.java), - connection, - Context.BIND_AUTO_CREATE - ) - } - - override fun onStop(owner: LifecycleOwner) { - application.unbindService(connection) - } - }) - } - } - - private val openedChannel: MutableList> = mutableListOf() - suspend fun setSelectProxy(name: String, proxy: String): Boolean = withContext(Dispatchers.IO) { return@withContext service.setSelectProxy(name, proxy) } @@ -89,27 +32,12 @@ class ClashClient(private val service: IClashManager) { service.queryAllProxies() } - suspend fun queryProfiles(): Array = withContext(Dispatchers.IO) { - service.queryAllProfiles() - } - suspend fun queryGeneral(): General = withContext(Dispatchers.IO) { service.queryGeneral() } - suspend fun openLogChannel(): ReceiveChannel = withContext(Dispatchers.IO) { - LogChannel().apply { - service.openLogEvent(createCallback()) - } - } - suspend fun queryBandwidth(): Long = withContext(Dispatchers.IO) { service.queryBandwidth() } - - fun close() { - for (channel in openedChannel) - channel.cancel() - } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt new file mode 100644 index 0000000000..c23d829f72 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt @@ -0,0 +1,21 @@ +package com.github.kr328.clash.remote + +import com.github.kr328.clash.service.IProfileService +import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.transact.ProfileRequest +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ProfileClient(private val service: IProfileService) { + suspend fun queryProfiles(): Array = withContext(Dispatchers.IO) { + service.queryProfiles() + } + + suspend fun queryActiveProfile(): ClashProfileEntity? = withContext(Dispatchers.IO) { + service.queryActiveProfile() + } + + suspend fun enqueueRequest(request: ProfileRequest) = withContext(Dispatchers.IO) { + service.enqueueRequest(request) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Remote.kt b/app/src/main/java/com/github/kr328/clash/remote/Remote.kt new file mode 100644 index 0000000000..974ed64374 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/remote/Remote.kt @@ -0,0 +1,142 @@ +package com.github.kr328.clash.remote + +import android.app.Application +import android.content.ComponentName +import android.content.Context +import android.content.ServiceConnection +import android.os.IBinder +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.core.utils.Log +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 kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import java.util.zip.ZipFile + +object Remote { + var clash = Channel() + var profile = Channel() + + private var clashConnection: ClashConnection? = null + private var profileConnection: ProfileConnection? = null + + class ClashConnection : ServiceConnection { + private var instance: ClashClient? = null + private var sender: Job? = null + + override fun onServiceDisconnected(name: ComponentName?) { + sender?.cancel() + instance = null + sender = null + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + if (service != null) + instance = ClashClient(IClashManager.Stub.asInterface(service)) + + sender = GlobalScope.launch { + while (isActive) { + val client = instance ?: return@launch + clash.send(client) + Log.d("Clash Client sent") + } + } + } + } + + class ProfileConnection : ServiceConnection { + private var instance: ProfileClient? = null + private var sender: Job? = null + + override fun onServiceDisconnected(name: ComponentName?) { + sender?.cancel() + instance = null + sender = null + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + if (service != null) + instance = ProfileClient(IProfileService.Stub.asInterface(service)) + + sender = GlobalScope.launch { + while (isActive) { + val client = instance ?: return@launch + profile.send(client) + Log.d("Profile Client sent") + } + } + } + } + + fun init(application: Application) { + ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { + GlobalScope.launch { + if (!verifyApk(application)) { + application.startActivity(ApkBrokenActivity::class.intent) + return@launch + } + + clashConnection = ClashConnection().apply { + application.bindService( + ClashManagerService::class.intent, + this, + Context.BIND_AUTO_CREATE + ) + } + + profileConnection = ProfileConnection().apply { + application.bindService( + ProfileService::class.intent, + this, + Context.BIND_AUTO_CREATE + ) + } + } + } + + override fun onStop(owner: LifecycleOwner) { + clashConnection?.also(application::unbindService) + profileConnection?.also(application::unbindService) + + clashConnection = null + profileConnection = null + } + }) + } + + 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) + + 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 + + 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/utils/IntervalUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/IntervalUtils.kt new file mode 100644 index 0000000000..a05b12e1ae --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/utils/IntervalUtils.kt @@ -0,0 +1,34 @@ +package com.github.kr328.clash.utils + +import com.github.kr328.clash.R +import com.github.kr328.clash.core.Global + +object IntervalUtils { + private const val MILLIS_SECOND = 1000L + private const val MILLIS_MINUTE = MILLIS_SECOND * 60 + private const val MILLIS_HOUR = MILLIS_MINUTE * 60 + private const val MILLIS_DAY = MILLIS_HOUR * 24 + private const val MILLIS_MONTH = MILLIS_DAY * 30 + private const val MILLIS_YEAR = MILLIS_MONTH * 12 + + fun intervalString(interval: Long): String { + val context = Global.application + + val year = interval / MILLIS_YEAR + val month = interval / MILLIS_MONTH + val day = interval / MILLIS_DAY + val hour = interval / MILLIS_HOUR + val minute = interval / MILLIS_MINUTE + + System.currentTimeMillis() + + return when { + year > 0 -> context.getString(R.string.format_years, year) + month > 0 -> context.getString(R.string.format_months, month) + day > 0 -> context.getString(R.string.format_days, day) + hour > 0 -> context.getString(R.string.format_hours, hour) + minute > 0 -> context.getString(R.string.format_minutes, minute) + else -> context.getString(R.string.recently) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_boot.xml b/app/src/main/res/drawable/ic_boot.xml index 9380e2fd89..cbd73a8b22 100644 --- a/app/src/main/res/drawable/ic_boot.xml +++ b/app/src/main/res/drawable/ic_boot.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_cloud.xml b/app/src/main/res/drawable/ic_cloud.xml index db16c9da0e..b60a4a5b4b 100644 --- a/app/src/main/res/drawable/ic_cloud.xml +++ b/app/src/main/res/drawable/ic_cloud.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_content.xml b/app/src/main/res/drawable/ic_content.xml new file mode 100644 index 0000000000..03648cd123 --- /dev/null +++ b/app/src/main/res/drawable/ic_content.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_new_profile_url.xml b/app/src/main/res/drawable/ic_download.xml similarity index 89% rename from app/src/main/res/drawable/ic_new_profile_url.xml rename to app/src/main/res/drawable/ic_download.xml index aa051b25da..53aeff91da 100644 --- a/app/src/main/res/drawable/ic_new_profile_url.xml +++ b/app/src/main/res/drawable/ic_download.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_new_profile_file.xml b/app/src/main/res/drawable/ic_file.xml similarity index 87% rename from app/src/main/res/drawable/ic_new_profile_file.xml rename to app/src/main/res/drawable/ic_file.xml index 3537de2a3b..1b9f4176ee 100644 --- a/app/src/main/res/drawable/ic_new_profile_file.xml +++ b/app/src/main/res/drawable/ic_file.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_input.xml b/app/src/main/res/drawable/ic_input.xml new file mode 100644 index 0000000000..54d69695a9 --- /dev/null +++ b/app/src/main/res/drawable/ic_input.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_label.xml b/app/src/main/res/drawable/ic_label.xml new file mode 100644 index 0000000000..c1e4bc708b --- /dev/null +++ b/app/src/main/res/drawable/ic_label.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_label_outline.xml b/app/src/main/res/drawable/ic_label_outline.xml new file mode 100644 index 0000000000..9eb92f2736 --- /dev/null +++ b/app/src/main/res/drawable/ic_label_outline.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml index c9698edc79..f615dd387f 100644 --- a/app/src/main/res/drawable/ic_link.xml +++ b/app/src/main/res/drawable/ic_link.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/drawable/ic_logo.xml b/app/src/main/res/drawable/ic_logo.xml index c6fe63f2d5..2d625a11ba 100644 --- a/app/src/main/res/drawable/ic_logo.xml +++ b/app/src/main/res/drawable/ic_logo.xml @@ -1,4 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_settings_color.xml b/app/src/main/res/drawable/ic_settings_color.xml index 9dfdbc9dd4..d092e0420b 100644 --- a/app/src/main/res/drawable/ic_settings_color.xml +++ b/app/src/main/res/drawable/ic_settings_color.xml @@ -4,6 +4,6 @@ android:viewportWidth="24" android:viewportHeight="24"> diff --git a/app/src/main/res/drawable/ic_update.xml b/app/src/main/res/drawable/ic_update.xml new file mode 100644 index 0000000000..835a441e67 --- /dev/null +++ b/app/src/main/res/drawable/ic_update.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_new_profile.xml b/app/src/main/res/layout/activity_create_profile.xml similarity index 79% rename from app/src/main/res/layout/activity_new_profile.xml rename to app/src/main/res/layout/activity_create_profile.xml index 1d22807e3c..26fe9b32de 100644 --- a/app/src/main/res/layout/activity_new_profile.xml +++ b/app/src/main/res/layout/activity_create_profile.xml @@ -9,16 +9,16 @@ android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:background="@color/toolbarColor" /> - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_feedback.xml b/app/src/main/res/layout/activity_feedback.xml index e5bcaf847a..6c3eb2494b 100644 --- a/app/src/main/res/layout/activity_feedback.xml +++ b/app/src/main/res/layout/activity_feedback.xml @@ -1,6 +1,7 @@ + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_import_file.xml b/app/src/main/res/layout/activity_import_file.xml deleted file mode 100644 index 30b1a063d1..0000000000 --- a/app/src/main/res/layout/activity_import_file.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_import_url.xml b/app/src/main/res/layout/activity_import_url.xml deleted file mode 100644 index 3e35a0baf9..0000000000 --- a/app/src/main/res/layout/activity_import_url.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_logs.xml b/app/src/main/res/layout/activity_logs.xml index 5248f5e9fa..0b7623e399 100644 --- a/app/src/main/res/layout/activity_logs.xml +++ b/app/src/main/res/layout/activity_logs.xml @@ -3,6 +3,7 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> + @@ -10,13 +11,12 @@ - + android:layout_height="wrap_content" /> + android:layout_height="match_parent" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index ee6cdfec0a..793fdc7b73 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -3,9 +3,9 @@ xmlns:tools="http://schemas.android.com/tools" xmlns:app="http://schemas.android.com/apk/res-auto" android:scrollbars="none" - android:id="@+id/activity_main_root" android:layout_width="match_parent" android:layout_height="match_parent"> + + + + android:layout_marginStart="15dp" /> + app:summary="@string/tap_to_start" /> + app:summary="@string/direct_mode" /> + app:summary="@string/not_selected" /> + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_profiles.xml b/app/src/main/res/layout/activity_profiles.xml index 6e4fa65799..dfe4a5bcda 100644 --- a/app/src/main/res/layout/activity_profiles.xml +++ b/app/src/main/res/layout/activity_profiles.xml @@ -14,7 +14,7 @@ android:elevation="4dp" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/toolbarColor"/> + android:background="@color/toolbarColor" /> + + android:layout_toStartOf="@id/adapter_log_time" /> + android:textSize="12sp" /> + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_profile_entity.xml b/app/src/main/res/layout/adapter_profile_entity.xml index 65956b6bc3..fbe26224a7 100644 --- a/app/src/main/res/layout/adapter_profile_entity.xml +++ b/app/src/main/res/layout/adapter_profile_entity.xml @@ -24,8 +24,8 @@ android:layout_toEndOf="@id/radio" android:layout_centerVertical="true" android:layout_marginStart="23dp" - android:paddingTop="15dp" - android:paddingBottom="15dp" + android:paddingTop="10dp" + android:paddingBottom="10dp" android:orientation="vertical"> + android:foreground="@drawable/ic_vertex" + android:background="?attr/selectableItemBackgroundBorderless" /> \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_profile_footer.xml b/app/src/main/res/layout/adapter_profile_footer.xml index bf981c4901..f4e4d93911 100644 --- a/app/src/main/res/layout/adapter_profile_footer.xml +++ b/app/src/main/res/layout/adapter_profile_footer.xml @@ -14,7 +14,7 @@ android:layout_marginStart="20dp" android:layout_marginEnd="20dp" android:layout_gravity="center_vertical" - android:background="@drawable/ic_new"/> + android:background="@drawable/ic_new" /> + android:gravity="center_vertical" /> \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_proxy_header.xml b/app/src/main/res/layout/adapter_proxy_header.xml index 2fb16609bc..1db43a2470 100644 --- a/app/src/main/res/layout/adapter_proxy_header.xml +++ b/app/src/main/res/layout/adapter_proxy_header.xml @@ -8,7 +8,7 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_about.xml b/app/src/main/res/layout/dialog_about.xml index 34dfd3829f..d8eccea385 100644 --- a/app/src/main/res/layout/dialog_about.xml +++ b/app/src/main/res/layout/dialog_about.xml @@ -3,25 +3,28 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:padding="20dp"> + + + + android:layout_height="wrap_content" /> \ No newline at end of file diff --git a/app/src/main/res/layout/view_radio_fat_item.xml b/app/src/main/res/layout/view_radio_fat_item.xml index 405b6b5475..6ec7802d5d 100644 --- a/app/src/main/res/layout/view_radio_fat_item.xml +++ b/app/src/main/res/layout/view_radio_fat_item.xml @@ -58,7 +58,7 @@ android:layout_marginBottom="5dp" android:layout_width="1dp" android:layout_height="match_parent" - android:background="@color/lightGray"/> + android:background="@color/lightGray" /> - #FFFFFF + #FFFFFF #121212 #000000 #000000 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f4d0541d52..8d82dff7af 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,14 +1,14 @@ - #1E4376 #FF888888 #FAFAFA #FAFAFA #FAFAFA + #1E4376 + #121212 - #1E4376 #FFFFFF #d3d3d3 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 64cd461290..75496f1a58 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -33,6 +33,35 @@ %d years Create Profile + File + Import from File + URL + Import from URL + External + Import from %s + + Application Broken + + Profile + Name + Source + Profile Name + Profile URL + + Auto Update + Auto Update Interval + In minutes + Invalid Interval + Invalid Profile + Detail + + Exit without Save + All changed will *LOST* + + Disabled + Empty Name + Invalid URL + Processing %s (File) %s (URL) diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 68531ab730..6ea316f9bb 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,9 +2,9 @@ diff --git a/app/src/main/res/xml-zh/feedback.xml b/app/src/main/res/xml-zh/feedback.xml deleted file mode 100644 index 78428e92f4..0000000000 --- a/app/src/main/res/xml-zh/feedback.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/core/build.gradle b/core/build.gradle index b41a763bf4..f09de143c1 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -34,6 +34,9 @@ android { sourceCompatibility = 1.8 targetCompatibility = 1.8 } + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { diff --git a/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt b/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt index 286477f304..d23f4d4fa2 100644 --- a/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt +++ b/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt @@ -3,39 +3,10 @@ package com.github.kr328.clash.core.utils import com.github.kr328.clash.core.Constants.TAG object Log { - var handler: LogHandler = object : LogHandler { - override fun info(message: String, throwable: Throwable?) { - android.util.Log.i(TAG, message, throwable) - } - - override fun warn(message: String, throwable: Throwable?) { - android.util.Log.w(TAG, message, throwable) - } - - override fun error(message: String, throwable: Throwable?) { - android.util.Log.e(TAG, message, throwable) - } - - override fun wtf(message: String, throwable: Throwable?) { - android.util.Log.wtf(TAG, message, throwable) - } - - override fun debug(message: String, throwable: Throwable?) { - android.util.Log.d(TAG, message, throwable) - } - } - - interface LogHandler { - fun info(message: String, throwable: Throwable?) - fun warn(message: String, throwable: Throwable?) - fun error(message: String, throwable: Throwable?) - fun wtf(message: String, throwable: Throwable?) - fun debug(message: String, throwable: Throwable?) - } - - fun i(message: String, throwable: Throwable? = null) = handler.info(message, throwable) - fun w(message: String, throwable: Throwable? = null) = handler.warn(message, throwable) - fun e(message: String, throwable: Throwable? = null) = handler.error(message, throwable) - fun wtf(message: String, throwable: Throwable? = null) = handler.wtf(message, throwable) - fun debug(message: String, throwable: Throwable? = null) = handler.debug(message, throwable) + fun i(message: String, throwable: Throwable? = null) = android.util.Log.i(TAG, message, throwable) + fun w(message: String, throwable: Throwable? = null) = android.util.Log.w(TAG, message, throwable) + fun e(message: String, throwable: Throwable? = null) = android.util.Log.e(TAG, message, throwable) + fun d(message: String, throwable: Throwable? = null) = android.util.Log.d(TAG, message, throwable) + fun v(message: String, throwable: Throwable? = null) = android.util.Log.v(TAG, message, throwable) + fun wtf(message: String, throwable: Throwable? = null) = android.util.Log.wtf(TAG, message, throwable) } diff --git a/design/build.gradle b/design/build.gradle index a061493f9e..c40c3dee20 100644 --- a/design/build.gradle +++ b/design/build.gradle @@ -5,7 +5,6 @@ android { compileSdkVersion 29 buildToolsVersion "29.0.3" - defaultConfig { minSdkVersion 24 targetSdkVersion 29 @@ -15,14 +14,19 @@ android { testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' } - buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } - + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/Base.kt b/design/src/main/java/com/github/kr328/clash/design/settings/Base.kt new file mode 100644 index 0000000000..a834ced149 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/settings/Base.kt @@ -0,0 +1,36 @@ +package com.github.kr328.clash.design.settings + +import android.content.Context +import android.os.Bundle +import android.view.View + +abstract class Base(val screen: SettingsScreen) { + val context: Context + get() = screen.layout.context + + var id: String? = null + + var dependOn: Base? = null + + var isEnabled: Boolean = true + get() = field && (dependOn?.isEnabled ?: true) + set(value) { + field = value + screen.postReapplyAttribute() + } + var isHidden: Boolean = false + get() = field || (dependOn?.isHidden ?: false) + set(value) { + field = value + screen.postReapplyAttribute() + } + + fun reapplyAttribute() { + applyAttribute(isEnabled, isHidden) + } + + abstract val view: View + abstract fun saveState(bundle: Bundle) + abstract fun restoreState(bundle: Bundle) + protected abstract fun applyAttribute(enabled: Boolean, hidden: Boolean) +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt b/design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt new file mode 100644 index 0000000000..3a3f4a2799 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt @@ -0,0 +1,32 @@ +package com.github.kr328.clash.design.settings + +import android.content.Context +import android.graphics.drawable.Drawable + +class SettingsBuilder(val screen: SettingsScreen) { + val context: Context + get() = screen.layout.context + + fun textInput( + title: String = "", + hint: CharSequence = "", + icon: Drawable? = null, + content: String = "", + id: String? = null, + dependOn: String? = null, + setup: TextInput.() -> Unit = {} + ) { + val textInput = TextInput(screen) + + textInput.title = title + textInput.hint = hint + textInput.icon = icon + textInput.content = content + textInput.id = id + textInput.dependOn = dependOn?.run { screen.requireElement(this) } + + setup(textInput) + + screen.addElement(textInput) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/SettingsScreen.kt b/design/src/main/java/com/github/kr328/clash/design/settings/SettingsScreen.kt new file mode 100644 index 0000000000..343afd8743 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/settings/SettingsScreen.kt @@ -0,0 +1,51 @@ +package com.github.kr328.clash.design.settings + +import android.os.Bundle +import android.os.Handler +import com.github.kr328.clash.design.view.SettingsLayout + +class SettingsScreen(val layout: SettingsLayout) { + private val handler = Handler() + val elements = mutableListOf() + + fun clear() { + elements.clear() + layout.removeAllViews() + } + + inline fun getElement(id: String): T? { + return elements.singleOrNull { it.id == id } as T? + } + + inline fun requireElement(id: String): T { + return requireNotNull(getElement(id)) + } + + fun addElement(element: Base) { + layout.addView(element.view) + elements.add(element) + } + + fun postReapplyAttribute() { + handler.post { + elements.forEach { + it.reapplyAttribute() + } + } + } + + fun saveState(bundle: Bundle) { + elements.forEach { + it.saveState(bundle) + } + } + + fun restoreState(bundle: Bundle?) { + if ( bundle == null ) + return + + elements.forEach { + it.restoreState(bundle) + } + } +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/TextInput.kt b/design/src/main/java/com/github/kr328/clash/design/settings/TextInput.kt new file mode 100644 index 0000000000..218b58a40a --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/settings/TextInput.kt @@ -0,0 +1,112 @@ +package com.github.kr328.clash.design.settings + +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.widget.EditText +import android.widget.TextView +import androidx.appcompat.app.AlertDialog +import com.github.kr328.clash.design.R + +class TextInput(screen: SettingsScreen) : Base(screen) { + override val view: View = LayoutInflater.from(context).inflate( + R.layout.view_setting_text_input, + screen.layout, + false + ) + + private val vIcon: View = view.findViewById(android.R.id.icon) + private val vTitle: TextView = view.findViewById(android.R.id.title) + private val vContent: View = view.findViewById(android.R.id.content) + private val vText: TextView = view.findViewById(android.R.id.text1) + + var icon: Drawable? + get() = vIcon.background + set(value) { + vIcon.background = value + } + var title: String + get() = vTitle.text?.toString() ?: "" + set(value) { + vTitle.text = value + } + var content: CharSequence = "" + set(value) { + vText.text = displayContent(value) + field = value + + textChanged.apply { + textChanged = {} + this(value) + textChanged = this + } + } + var hint: CharSequence + get() = vText.hint ?: "" + set(value) { + vText.hint = value + } + + private var openInput: () -> Unit = this::openDialogInput + private var textChanged: (CharSequence) -> Unit = {} + private var displayContent: (CharSequence) -> CharSequence = { it } + + init { + vContent.setOnClickListener { + openInput() + } + } + + fun onOpenInput(block: () -> Unit) { + this.openInput = block + } + + fun onTextChanged(block: (CharSequence) -> Unit) { + this.textChanged = block + } + + fun onDisplayContent(block: (CharSequence) -> CharSequence) { + this.displayContent = block + + // Apply display content transform + content = content + } + + override fun applyAttribute(enabled: Boolean, hidden: Boolean) { + view.isEnabled = enabled + view.visibility = if (hidden) View.GONE else View.INVISIBLE + } + + override fun saveState(bundle: Bundle) { + if ( id == null ) + return + + bundle.putCharSequence(id, content) + } + + override fun restoreState(bundle: Bundle) { + if ( id == null ) + return + + bundle.getCharSequence(id)?.apply { + content = this + } + } + + fun openDialogInput() { + val v = LayoutInflater.from(context) + .inflate(R.layout.dialog_input_text, screen.layout, false) + val c: EditText = v.findViewById(android.R.id.text1) + + c.setText(content) + c.hint = hint + + AlertDialog.Builder(context) + .setTitle(title) + .setView(v) + .setPositiveButton(R.string.ok) { _, _ -> content = c.text?.toString() ?: "" } + .setNegativeButton(R.string.cancel) { _, _ -> } + .show() + } +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt b/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt index ec5127880b..4a90874b3a 100644 --- a/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt +++ b/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt @@ -1,7 +1,6 @@ package com.github.kr328.clash.design.view import android.content.Context -import android.graphics.Color import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater @@ -45,14 +44,15 @@ class ColorfulTextCard @JvmOverloads constructor( } // Custom attrs - context.theme.obtainStyledAttributes(attributeSet, R.styleable.ColorfulTextCard, 0, 0).apply { - try { - iconView.background = getDrawable(R.styleable.ColorfulTextCard_icon) - titleView.text = getString(R.styleable.ColorfulTextCard_title) - summaryView.text = getString(R.styleable.ColorfulTextCard_summary) - } finally { - recycle() + context.theme.obtainStyledAttributes(attributeSet, R.styleable.ColorfulTextCard, 0, 0) + .apply { + try { + iconView.background = getDrawable(R.styleable.ColorfulTextCard_icon) + titleView.text = getString(R.styleable.ColorfulTextCard_title) + summaryView.text = getString(R.styleable.ColorfulTextCard_summary) + } finally { + recycle() + } } - } } } \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/view/SettingsLayout.kt b/design/src/main/java/com/github/kr328/clash/design/view/SettingsLayout.kt new file mode 100644 index 0000000000..c843d09982 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/view/SettingsLayout.kt @@ -0,0 +1,27 @@ +package com.github.kr328.clash.design.view + +import android.animation.LayoutTransition +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.github.kr328.clash.design.settings.SettingsBuilder +import com.github.kr328.clash.design.settings.SettingsScreen + +class SettingsLayout @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attributeSet, defStyleAttr) { + val screen: SettingsScreen = SettingsScreen(this) + + init { + layoutTransition = LayoutTransition() + orientation = VERTICAL + } + + fun build(builder: SettingsBuilder.() -> Unit) { + screen.clear() + + SettingsBuilder(screen).apply(builder) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt b/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt index d8aa390b89..ce6b706845 100644 --- a/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt +++ b/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt @@ -1,7 +1,6 @@ package com.github.kr328.clash.design.view import android.content.Context -import android.graphics.Color import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.LayoutInflater diff --git a/design/src/main/res/drawable/ic_demo_drawable.xml b/design/src/main/res/drawable/ic_demo_drawable.xml new file mode 100644 index 0000000000..a4f5ea0e71 --- /dev/null +++ b/design/src/main/res/drawable/ic_demo_drawable.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_edit.xml b/design/src/main/res/drawable/ic_edit.xml similarity index 89% rename from app/src/main/res/drawable/ic_edit.xml rename to design/src/main/res/drawable/ic_edit.xml index 2ab2fb7533..3f39f1cbbb 100644 --- a/app/src/main/res/drawable/ic_edit.xml +++ b/design/src/main/res/drawable/ic_edit.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/design/src/main/res/layout/dialog_input_text.xml b/design/src/main/res/layout/dialog_input_text.xml new file mode 100644 index 0000000000..2402e54ba6 --- /dev/null +++ b/design/src/main/res/layout/dialog_input_text.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_form_text.xml b/design/src/main/res/layout/view_setting_text_input.xml similarity index 56% rename from app/src/main/res/layout/adapter_form_text.xml rename to design/src/main/res/layout/view_setting_text_input.xml index 33bd3e152f..880790e239 100644 --- a/app/src/main/res/layout/adapter_form_text.xml +++ b/design/src/main/res/layout/view_setting_text_input.xml @@ -1,54 +1,50 @@ - + + android:singleLine="true" + android:ellipsize="end" + android:textAppearance="@style/TextAppearance.AppCompat.Medium" /> + android:background="?attr/colorOnSurface" /> diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index b4e079e762..1e5e6c15da 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -1,3 +1,5 @@ - Design + Demo String + OK + Cancel diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index 9bff042148..6c5291f93d 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -26,7 +26,7 @@ android:exported="false" android:process=":background" /> diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index f323ca08dc..00bc3330d3 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -13,7 +13,6 @@ interface IClashManager { ProxyGroup[] queryAllProxies(); General queryGeneral(); long queryBandwidth(); - ClashProfileEntity[] queryAllProfiles(); // Events void openLogEvent(IStreamCallback callback); diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl index 0123d0647e..509899d68f 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl @@ -1,7 +1,13 @@ package com.github.kr328.clash.service; import com.github.kr328.clash.service.transact.ProfileRequest; +import com.github.kr328.clash.service.data.ClashProfileEntity; interface IProfileService { + // process void enqueueRequest(in ProfileRequest request); + + // query + ClashProfileEntity[] queryProfiles(); + ClashProfileEntity queryActiveProfile(); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index 69152382c2..b4ce19b501 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -34,10 +34,6 @@ class ClashManager(private val context: Context) : IClashManager.Stub() { return true } - override fun queryAllProfiles(): Array { - return ClashDatabase.getInstance(context).openClashProfileDao().queryProfiles() - } - override fun queryBandwidth(): Long { val data = Clash.queryBandwidth() diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt index 0d0ecd4295..71db50bf7b 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt @@ -62,7 +62,7 @@ class ProfileBackgroundService : BaseService() { startForeground() - bindService(ProfileProcessService::class.intent, connection, Context.BIND_AUTO_CREATE) + bindService(ProfileService::class.intent, connection, Context.BIND_AUTO_CREATE) } override fun onDestroy() { diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt deleted file mode 100644 index 4732082b6f..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessService.kt +++ /dev/null @@ -1,166 +0,0 @@ -package com.github.kr328.clash.service - -import android.app.AlarmManager -import android.app.PendingIntent -import android.content.Intent -import android.net.Uri -import android.os.IBinder -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.service.data.ClashDatabase -import com.github.kr328.clash.service.data.ClashProfileDao -import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.service.transact.ProfileRequest -import com.github.kr328.clash.service.util.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout -import java.io.File -import java.io.FileNotFoundException -import java.util.* - -class ProfileProcessService : BaseService() { - private val service = this - private val queue: MutableMap> = Hashtable() - - private val profiles: ClashProfileDao by lazy { - ClashDatabase.getInstance(service).openClashProfileDao() - } - - override fun onBind(intent: Intent?): IBinder? { - return object : IProfileService.Stub() { - override fun enqueueRequest(request: ProfileRequest?) { - service.enqueueRequest(request ?: return) - } - } - } - - private fun createChannelForRequests(): Channel { - return Channel(Channel.UNLIMITED).also { - launch { - while (isActive) { - val request = withTimeout(1000 * 30) { - it.receive() - } - - handleRequest(request) - } - } - } - } - - private fun enqueueRequest(request: ProfileRequest) { - launch { - queue.computeIfAbsent(request.id) { - createChannelForRequests() - }.send(request) - } - } - - private suspend fun handleRequest(request: ProfileRequest) { - try { - request.callback?.send(null) - - when (request.action) { - ProfileRequest.Action.UPDATE_OR_CREATE -> - handleUpdateOrCreate(request) - ProfileRequest.Action.REMOVE -> - removeProfile(request) - } - - request.callback?.complete() - - broadcastProfileChanged(this) - } catch (e: Exception) { - request.callback?.completeExceptionally(e.message) - } - } - - private suspend fun handleUpdateOrCreate(request: ProfileRequest) { - val id = request.id - - val entity: ClashProfileEntity = - if (id == 0L) { - ClashProfileEntity( - requireNotNull(request.name), - requireNotNull(request.type), - requireNotNull(request.url), - RandomUtils.fileName(profileDir, ".yaml"), - RandomUtils.fileName(clashDir), - false, - 0, - request.interval.takeIf { it >= 0 } ?: 0 - ) - } else { - val e = profiles.queryProfileById(id) ?: return - - e.copy( - name = request.name ?: e.name, - uri = request.url ?: e.uri, - updateInterval = request.interval.takeIf { it >= 0 } ?: e.updateInterval - ) - } - - val url = Uri.parse(entity.uri) - - if (url == null || url == Uri.EMPTY) - throw IllegalArgumentException("Invalid url $url") - - downloadProfile(url, profileDir.resolve(entity.file), clashDir.resolve(entity.base)) - - val newEntity = entity.copy(lastUpdate = System.currentTimeMillis()) - - val newId = if (entity.id == 0L) - profiles.getId(profiles.addProfile(newEntity)) - else - profiles.updateProfile(newEntity).run { entity.id } - - if (entity.updateInterval > 0) { - val nextRequest = - ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE).withId(newId) - - requireNotNull(getSystemService(AlarmManager::class.java)).set( - AlarmManager.RTC, - entity.lastUpdate + entity.updateInterval, - PendingIntent.getBroadcast( - this, - RandomUtils.nextInt(), - Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) - .setComponent(ProfileRequestReceiver::class.componentName) - .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, nextRequest), - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - } - } - - private fun removeProfile(request: ProfileRequest) { - val entity = profiles.queryProfileById(request.id) ?: return - - clashDir.resolve(entity.base).deleteRecursively() - profileDir.resolve(entity.file).delete() - - profiles.removeProfile(entity.id) - } - - private suspend fun downloadProfile(source: Uri, target: File, baseDir: File) { - try { - target.parentFile?.mkdirs() - baseDir.mkdirs() - - if (source.scheme == "content" || source.scheme == "file") { - val fd = contentResolver.openFileDescriptor(source, "r") - ?: throw FileNotFoundException("Unable to open file $source") - - Clash.downloadProfile(fd.fd, target, baseDir).await() - } else { - Clash.downloadProfile(source.toString(), target, baseDir).await() - } - } catch (e: Exception) { - target.delete() - baseDir.deleteRecursively() - - throw e - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt index 610159797f..d275397bed 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileRequestReceiver.kt @@ -11,7 +11,7 @@ class ProfileRequestReceiver : BroadcastReceiver() { if (intent?.action != Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST || context == null) return - intent.component = ProfileProcessService::class.componentName + intent.component = ProfileService::class.componentName if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) context.startForegroundService(intent) diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt new file mode 100644 index 0000000000..cb46b3a632 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt @@ -0,0 +1,187 @@ +package com.github.kr328.clash.service + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Intent +import android.net.Uri +import android.os.IBinder +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.data.ClashProfileDao +import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.transact.ProfileRequest +import com.github.kr328.clash.service.util.* +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import java.io.File +import java.io.FileNotFoundException +import java.util.* + +class ProfileService : BaseService() { + private val service = this + private val queue: MutableMap> = Hashtable() + + private val profiles: ClashProfileDao by lazy { + ClashDatabase.getInstance(service).openClashProfileDao() + } + + override fun onBind(intent: Intent?): IBinder? { + return object : IProfileService.Stub() { + override fun enqueueRequest(request: ProfileRequest?) { + service.enqueueRequest(request ?: return) + } + + override fun queryActiveProfile(): ClashProfileEntity? { + return profiles.queryActiveProfile() + } + + override fun queryProfiles(): Array { + return profiles.queryProfiles() + } + } + } + + private fun createChannelForRequests(id: Long): Channel { + return Channel(Channel.UNLIMITED).also { + launch { + try { + while (isActive) { + Log.d("Coroutine for $id launched") + + val request = withTimeout(1000 * 30) { + it.receive() + } + + Log.d("Handling $id") + handleRequest(request) + } + } + finally { + Log.d("Coroutine for $id exited") + + queue.remove(id) + } + } + } + } + + private fun enqueueRequest(request: ProfileRequest) { + launch { + queue.computeIfAbsent(request.id) { + createChannelForRequests(it) + }.send(request) + } + } + + private suspend fun handleRequest(request: ProfileRequest) { + try { + request.callback?.send(null) + + when (request.action) { + ProfileRequest.Action.UPDATE_OR_CREATE -> + handleUpdateOrCreate(request) + ProfileRequest.Action.REMOVE -> + removeProfile(request) + } + + request.callback?.complete() + + broadcastProfileChanged(this) + } catch (e: Exception) { + request.callback?.completeExceptionally(e.message) + } + } + + private suspend fun handleUpdateOrCreate(request: ProfileRequest) = + withContext(Dispatchers.IO) { + val id = request.id + + val entity: ClashProfileEntity = + if (id == 0L) { + ClashProfileEntity( + requireNotNull(request.name), + requireNotNull(request.type), + requireNotNull(request.url).toString(), + request.source?.toString(), + RandomUtils.fileName(profileDir, ".yaml"), + RandomUtils.fileName(clashDir), + false, + 0, + request.interval.takeIf { it >= 0 } ?: 0 + ) + } else { + val e = profiles.queryProfileById(id) ?: return@withContext + + e.copy( + name = request.name ?: e.name, + uri = request.url?.toString() ?: e.uri, + updateInterval = request.interval.takeIf { it >= 0 } ?: e.updateInterval + ) + } + + val url = Uri.parse(entity.uri) + + if (url == null || url == Uri.EMPTY) + throw IllegalArgumentException("Invalid url $url") + + Log.d("Profile ${entity.name} downloading") + + downloadProfile(url, profileDir.resolve(entity.file), clashDir.resolve(entity.base)) + + val newEntity = entity.copy(lastUpdate = System.currentTimeMillis()) + + val newId = if (entity.id == 0L) + profiles.getId(profiles.addProfile(newEntity)) + else + profiles.updateProfile(newEntity).run { entity.id } + + if (entity.updateInterval > 0) { + val nextRequest = + ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE).withId(newId) + + requireNotNull(getSystemService(AlarmManager::class.java)).set( + AlarmManager.RTC, + entity.lastUpdate + entity.updateInterval, + PendingIntent.getBroadcast( + service, + RandomUtils.nextInt(), + Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) + .setComponent(ProfileRequestReceiver::class.componentName) + .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, nextRequest), + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + } + } + + private suspend fun removeProfile(request: ProfileRequest) = withContext(Dispatchers.IO) { + val entity = profiles.queryProfileById(request.id) ?: return@withContext + + clashDir.resolve(entity.base).deleteRecursively() + profileDir.resolve(entity.file).delete() + + profiles.removeProfile(entity.id) + } + + private suspend fun downloadProfile(source: Uri, target: File, baseDir: File) { + try { + target.parentFile?.mkdirs() + baseDir.mkdirs() + + if (source.scheme == "content" || source.scheme == "file") { + val fd = contentResolver.openFileDescriptor(source, "r") + ?: throw FileNotFoundException("Unable to open file $source") + + Clash.downloadProfile(fd.fd, target, baseDir).await() + } else { + Clash.downloadProfile(source.toString(), target, baseDir).await() + } + } catch (e: Exception) { + target.delete() + baseDir.deleteRecursively() + + throw e + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt index ecd28776bb..6e0f6da4e3 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashDatabaseMigrations.kt @@ -42,8 +42,8 @@ object ClashDatabaseMigrations { // new val type = when { - token.startsWith("url") -> ClashProfileEntity.TYPE_REMOTE - token.startsWith("file") -> ClashProfileEntity.TYPE_LOCAL + token.startsWith("url") -> ClashProfileEntity.TYPE_URL + token.startsWith("file") -> ClashProfileEntity.TYPE_FILE else -> ClashProfileEntity.TYPE_UNKNOWN } val uri = token.removePrefix("url|").removePrefix("file|") diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index 461b19e4de..180cec9f3d 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -14,6 +14,7 @@ data class ClashProfileEntity( @ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "type") val type: Int, @ColumnInfo(name = "uri") val uri: String, + @ColumnInfo(name = "source") val source: String?, @ColumnInfo(name = "file") val file: String, @ColumnInfo(name = "base") val base: String, @ColumnInfo(name = "active") val active: Boolean, @@ -30,8 +31,9 @@ data class ClashProfileEntity( } companion object { - const val TYPE_LOCAL = 1 - const val TYPE_REMOTE = 2 + const val TYPE_FILE = 1 + const val TYPE_URL = 2 + const val TYPE_EXTERNAL = 3 const val TYPE_UNKNOWN = -1 @JvmField diff --git a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt index 3d737c90ec..9e261ffb29 100644 --- a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt +++ b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt @@ -7,7 +7,6 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import com.github.kr328.clash.core.utils.Log -import com.github.kr328.clash.core.utils.Log.handler import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.channels.Channel diff --git a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt index b604ccf9b8..331bad8f3a 100644 --- a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt +++ b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt @@ -25,8 +25,10 @@ class ProfileRequest private constructor(private val bundle: Bundle) : Parcelabl get() = bundle.getLong(KEY_ID, 0) val name: String? get() = bundle.getString(KEY_NAME) - val url: String? - get() = bundle.getString(KEY_URL) + val url: Uri? + get() = bundle.getParcelable(KEY_URL) + val source: Uri? + get() = bundle.getParcelable(KEY_SOURCE) val interval: Long get() = bundle.getLong(KEY_UPDATE_INTERVAL, -1) val callback: IStreamCallback? @@ -58,7 +60,13 @@ class ProfileRequest private constructor(private val bundle: Bundle) : Parcelabl fun withURL(url: Uri): ProfileRequest { return apply { - bundle.putString(KEY_URL, url.toString()) + bundle.putParcelable(KEY_URL, url) + } + } + + fun withSource(source: Uri?): ProfileRequest { + return apply { + bundle.putParcelable(KEY_SOURCE, source) } } @@ -87,6 +95,7 @@ class ProfileRequest private constructor(private val bundle: Bundle) : Parcelabl private const val KEY_ID = "id" private const val KEY_NAME = "name" private const val KEY_URL = "url" + private const val KEY_SOURCE = "source" private const val KEY_TYPE = "type" private const val KEY_CALLBACK = "callback" private const val KEY_UPDATE_INTERVAL = "update_interval" From c9d3b8e836f06014f7c81dc0ae9bbd7b6ca943f0 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Wed, 12 Feb 2020 00:14:09 +0800 Subject: [PATCH 080/358] [WIP] UI logic refactor --- .../com/github/kr328/clash/BaseActivity.kt | 3 +- .../github/kr328/clash/ProfileEditActivity.kt | 11 +-- .../github/kr328/clash/ProfilesActivity.kt | 65 ++++++++++++- .../kr328/clash/remote/ProfileClient.kt | 4 + .../com/github/kr328/clash/remote/Remote.kt | 13 ++- .../main/res/drawable/ic_delete_colorful.xml | 9 ++ app/src/main/res/drawable/ic_delete_sweep.xml | 9 ++ app/src/main/res/drawable/ic_properties.xml | 9 ++ app/src/main/res/values/strings.xml | 6 ++ core/src/main/golang/bridge/init.go | 5 + core/src/main/golang/clash | 2 +- core/src/main/golang/profile/download.go | 16 +--- core/src/main/golang/profile/load.go | 15 +-- .../java/com/github/kr328/clash/core/Clash.kt | 1 + .../kr328/clash/design/settings/Option.kt | 58 ++++++++++++ .../clash/design/settings/SettingsBuilder.kt | 21 +++++ .../main/res/layout/view_setting_option.xml | 42 +++++++++ .../kr328/clash/service/IProfileService.aidl | 3 + .../kr328/clash/service/ProfileProcessor.kt | 74 +++++++++++++++ .../kr328/clash/service/ProfileService.kt | 93 ++++++++----------- .../clash/service/transact/ProfileRequest.kt | 2 +- 21 files changed, 367 insertions(+), 94 deletions(-) create mode 100644 app/src/main/res/drawable/ic_delete_colorful.xml create mode 100644 app/src/main/res/drawable/ic_delete_sweep.xml create mode 100644 app/src/main/res/drawable/ic_properties.xml create mode 100644 design/src/main/java/com/github/kr328/clash/design/settings/Option.kt create mode 100644 design/src/main/res/layout/view_setting_option.xml create mode 100644 service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 73ac728b57..08e7db3cb4 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -14,6 +14,7 @@ import android.widget.FrameLayout import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar +import androidx.coordinatorlayout.widget.CoordinatorLayout import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.service.data.ClashProfileEntity import com.google.android.material.snackbar.Snackbar @@ -59,7 +60,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() open suspend fun onClashProfileChanged(active: ClashProfileEntity?) {} override fun setContentView(layoutResID: Int) { - val base = FrameLayout(this).apply { + val base = CoordinatorLayout(this).apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT diff --git a/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt index 96449bb27e..677413fb5d 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt @@ -55,7 +55,7 @@ class ProfileEditActivity : BaseActivity() { override fun completeExceptionally(reason: String?) { launch { - makeSnackbarException(getString(R.string.invalid_profile), reason ?: "Unknown") + makeSnackbarException(getString(R.string.download_failure), reason ?: "Unknown") processing = false } } @@ -221,15 +221,12 @@ class ProfileEditActivity : BaseActivity() { launch { val source = intent?.getParcelableExtra("intent")?.toUri(0)?.run(Uri::parse) val type = when( intent?.getStringExtra("type") ) { - Constants.URL_PROVIDER_TYPE_FILE -> { + Constants.URL_PROVIDER_TYPE_FILE -> ClashProfileEntity.TYPE_FILE - } - Constants.URL_PROVIDER_TYPE_URL -> { + Constants.URL_PROVIDER_TYPE_URL -> ClashProfileEntity.TYPE_URL - } - Constants.URL_PROVIDER_TYPE_EXTERNAL -> { + Constants.URL_PROVIDER_TYPE_EXTERNAL -> ClashProfileEntity.TYPE_EXTERNAL - } else -> throw IllegalArgumentException() } diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 90993baa3f..f48c1c87f8 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -1,11 +1,17 @@ package com.github.kr328.clash import android.os.Bundle +import android.util.TypedValue +import android.view.ViewGroup.LayoutParams +import androidx.annotation.ColorInt import androidx.recyclerview.widget.LinearLayoutManager import com.github.kr328.clash.adapter.ProfileAdapter +import com.github.kr328.clash.design.view.SettingsLayout import com.github.kr328.clash.remote.withProfile +import com.github.kr328.clash.service.ProfileBackgroundService import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.util.intent +import com.google.android.material.bottomsheet.BottomSheetDialog import kotlinx.android.synthetic.main.activity_profiles.* import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -62,14 +68,71 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback { } override fun onProfileClicked(entity: ClashProfileEntity) { - + launch { + withProfile { + setActiveProfile(entity.id) + } + } } override fun onMenuClicked(entity: ClashProfileEntity) { + val dialog = BottomSheetDialog(this) + val menu = SettingsLayout(this).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + } + @ColorInt + val errorColor = TypedValue().run { + theme.resolveAttribute(R.attr.colorError, this, true) + data + } + + menu.build { + if (entity.type != ClashProfileEntity.TYPE_FILE) { + option( + title = getString(R.string.update), + icon = getDrawable(R.drawable.ic_update) + ) { + + } + } else { + option( + title = getString(R.string.edit), + icon = getDrawable(R.drawable.ic_edit) + ) { + + } + } + option( + title = getString(R.string.properties), + icon = getDrawable(R.drawable.ic_properties) + ) { + + } + option( + title = getString(R.string.clear_cache), + icon = getDrawable(R.drawable.ic_delete_sweep) + ) { + } + option( + title = getString(R.string.delete), + icon = getDrawable(R.drawable.ic_delete_colorful) + ) { + textColor = errorColor + } + } + + dialog.setContentView(menu) + dialog.show() } override fun onNewProfile() { startActivity(CreateProfileActivity::class.intent) } + + private fun sendDelete(entity: ClashProfileEntity) { + launch { + + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt index c23d829f72..661042bf66 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt @@ -18,4 +18,8 @@ class ProfileClient(private val service: IProfileService) { suspend fun enqueueRequest(request: ProfileRequest) = withContext(Dispatchers.IO) { service.enqueueRequest(request) } + + suspend fun setActiveProfile(id: Long) = withContext(Dispatchers.IO) { + service.setActiveProfile(id) + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Remote.kt b/app/src/main/java/com/github/kr328/clash/remote/Remote.kt index 974ed64374..5658bb61aa 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Remote.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Remote.kt @@ -11,7 +11,6 @@ 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.core.utils.Log import com.github.kr328.clash.service.ClashManagerService import com.github.kr328.clash.service.IClashManager import com.github.kr328.clash.service.IProfileService @@ -46,7 +45,6 @@ object Remote { while (isActive) { val client = instance ?: return@launch clash.send(client) - Log.d("Clash Client sent") } } } @@ -70,7 +68,6 @@ object Remote { while (isActive) { val client = instance ?: return@launch profile.send(client) - Log.d("Profile Client sent") } } } @@ -104,8 +101,14 @@ object Remote { } override fun onStop(owner: LifecycleOwner) { - clashConnection?.also(application::unbindService) - profileConnection?.also(application::unbindService) + clashConnection?.also { + application.unbindService(it) + it.onServiceDisconnected(null) + } + profileConnection?.also { + application.unbindService(it) + it.onServiceDisconnected(null) + } clashConnection = null profileConnection = null diff --git a/app/src/main/res/drawable/ic_delete_colorful.xml b/app/src/main/res/drawable/ic_delete_colorful.xml new file mode 100644 index 0000000000..bb6fdbbb13 --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_colorful.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_delete_sweep.xml b/app/src/main/res/drawable/ic_delete_sweep.xml new file mode 100644 index 0000000000..17b0eea87a --- /dev/null +++ b/app/src/main/res/drawable/ic_delete_sweep.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_properties.xml b/app/src/main/res/drawable/ic_properties.xml new file mode 100644 index 0000000000..5de6536319 --- /dev/null +++ b/app/src/main/res/drawable/ic_properties.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 75496f1a58..b693fadbf7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -53,7 +53,13 @@ In minutes Invalid Interval Invalid Profile + Download Failure Detail + Update + Edit + Delete + Clear Cache + Properties Exit without Save All changed will *LOST* diff --git a/core/src/main/golang/bridge/init.go b/core/src/main/golang/bridge/init.go index 321b174dcf..71dc8aadc6 100644 --- a/core/src/main/golang/bridge/init.go +++ b/core/src/main/golang/bridge/init.go @@ -2,6 +2,7 @@ package bridge import ( "github.com/Dreamacro/clash/component/mmdb" + C "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/tunnel" "github.com/kr328/cfa/profile" ) @@ -10,6 +11,10 @@ func LoadMMDB(data []byte) { mmdb.LoadFromBytes(data) } +func SetHome(homeDir string) { + C.SetHomeDir(homeDir) +} + func Reset() { profile.LoadDefault() tunnel.DefaultManager.ResetStatistic() diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index 102408d736..8971f2c8a5 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit 102408d736d602f6ec1091caaf0adcbb4f702adf +Subproject commit 8971f2c8a570a9fc15df6dccbd3331d04f775e7f diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go index f24e389d83..33280dab39 100644 --- a/core/src/main/golang/profile/download.go +++ b/core/src/main/golang/profile/download.go @@ -71,11 +71,7 @@ func ReadAndCheck(fd int, output, baseDir string) error { } func SaveAndCheck(data []byte, output, baseDir string) error { - original := constant.Path.HomeDir() - constant.SetHomeDir(baseDir) - defer constant.SetHomeDir(original) - - _, err := parseConfig(data) + _, err := parseConfig(data, baseDir) if err != nil { return err } @@ -84,16 +80,12 @@ func SaveAndCheck(data []byte, output, baseDir string) error { } func MoveAndCheck(source, target, baseDir string) error { - original := constant.Path.HomeDir() - constant.SetHomeDir(baseDir) - defer constant.SetHomeDir(original) - buf, err := ioutil.ReadFile(source) if err != nil { return err } - _, err = parseConfig(buf) + _, err = parseConfig(buf, baseDir) if err != nil { return err } @@ -107,7 +99,7 @@ func MoveAndCheck(source, target, baseDir string) error { return nil } -func parseConfig(data []byte) (*config.Config, error) { +func parseConfig(data []byte, baseDir string) (*config.Config, error) { raw, err := config.UnmarshalRawConfig(data) if err != nil { return nil, err @@ -116,5 +108,5 @@ func parseConfig(data []byte) (*config.Config, error) { raw.ExternalUI = "" raw.ExternalController = "" - return config.ParseRawConfig(raw) + return config.ParseRawConfig(raw, baseDir) } diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index e12d0f6241..4da826a23d 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -6,7 +6,6 @@ import ( "github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/config" - "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/hub/executor" "github.com/kr328/cfa/tun" @@ -48,23 +47,11 @@ func LoadFromFile(path, baseDir string) error { return err } - rawCfg, err := config.UnmarshalRawConfig(data) + cfg, err := parseConfig(data, baseDir) if err != nil { return err } - rawCfg.ExternalController = "" - rawCfg.ExternalUI = "" - - fallbackBaseDir := constant.Path.HomeDir() - constant.SetHomeDir(baseDir) - - cfg, err := config.ParseRawConfig(rawCfg) - if err != nil { - constant.SetHomeDir(fallbackBaseDir) - return err - } - executor.ApplyConfig(cfg, true) if dns.DefaultResolver == nil && cfg.DNS.Enable { diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 22e7b28f42..6cdf45c814 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -30,6 +30,7 @@ object Clash { .use(InputStream::readBytes) Bridge.loadMMDB(bytes) + Bridge.setHome(context.cacheDir.absolutePath) } fun start() { diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/Option.kt b/design/src/main/java/com/github/kr328/clash/design/settings/Option.kt new file mode 100644 index 0000000000..a66a2363ed --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/settings/Option.kt @@ -0,0 +1,58 @@ +package com.github.kr328.clash.design.settings + +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.widget.TextView +import com.github.kr328.clash.design.R + +class Option(screen: SettingsScreen): 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) + private val vSummary: TextView = view.findViewById(android.R.id.summary) + + private var click: () -> Unit = {} + + var icon: Drawable? + get() = vIcon.background + set(value) { vIcon.background = value } + var title: CharSequence + get() = vTitle.text + set(value) { vTitle.text = value } + var summary: CharSequence + get() = vSummary.text + set(value) { + vSummary.text = value + if ( value.isEmpty() ) + vSummary.visibility = View.GONE + else + vSummary.visibility = View.VISIBLE + } + var textColor: Int + get() = vTitle.textColors.defaultColor + set(value) { + vTitle.setTextColor(value) + vSummary.setTextColor(value) + } + + init { + view.setOnClickListener { + click() + } + } + + fun onClick(block: () -> Unit) { + this.click = block + } + + override fun applyAttribute(enabled: Boolean, hidden: Boolean) { + view.isEnabled = enabled + view.visibility = if ( hidden ) View.GONE else View.VISIBLE + } + + override fun saveState(bundle: Bundle) {} + override fun restoreState(bundle: Bundle) {} +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt b/design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt index 3a3f4a2799..fea0493be1 100644 --- a/design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt +++ b/design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt @@ -29,4 +29,25 @@ class SettingsBuilder(val screen: SettingsScreen) { screen.addElement(textInput) } + + fun option( + title: String = "", + summary: String = "", + icon: Drawable? = null, + id: String? = null, + dependOn: String? = null, + setup: Option.() -> Unit = {} + ) { + val option = Option(screen) + + option.title = title + option.summary = summary + option.icon = icon + option.id = id + option.dependOn = dependOn?.run { screen.requireElement(this) } + + setup(option) + + screen.addElement(option) + } } \ No newline at end of file diff --git a/design/src/main/res/layout/view_setting_option.xml b/design/src/main/res/layout/view_setting_option.xml new file mode 100644 index 0000000000..36b5a7c3a4 --- /dev/null +++ b/design/src/main/res/layout/view_setting_option.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl index 509899d68f..e96139209f 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl @@ -10,4 +10,7 @@ interface IProfileService { // query ClashProfileEntity[] queryProfiles(); ClashProfileEntity queryActiveProfile(); + + // set + void setActiveProfile(long id); } diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt new file mode 100644 index 0000000000..0046e147ab --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt @@ -0,0 +1,74 @@ +package com.github.kr328.clash.service + +import android.content.Context +import android.net.Uri +import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.util.clashDir +import com.github.kr328.clash.service.util.profileDir +import java.io.File +import java.io.FileNotFoundException + +class ProfileProcessor(private val context: Context) { + suspend fun createOrUpdate(entity: ClashProfileEntity): Long { + val database = ClashDatabase.getInstance(context).openClashProfileDao() + + val uri = Uri.parse(entity.uri) + if (uri == null || uri == Uri.EMPTY) + throw IllegalArgumentException("Invalid uri $uri") + + downloadProfile( + uri, + context.clashDir.resolve(entity.file), + context.profileDir.resolve(entity.base) + ) + + val newEntity = entity.copy(lastUpdate = System.currentTimeMillis()) + + return if ( newEntity.id == 0L ) + database.addProfile(newEntity).let { database.getId(it) } + else + database.updateProfile(newEntity).let { newEntity.id } + } + + fun remove(id: Long) { + val database = ClashDatabase.getInstance(context).openClashProfileDao() + val entity = database.queryProfileById(id) ?: return + + context.profileDir.resolve(entity.file).delete() + context.clashDir.resolve(entity.base).deleteRecursively() + + database.removeProfile(id) + } + + fun clear(id: Long) { + val database = ClashDatabase.getInstance(context).openClashProfileDao() + val entity = database.queryProfileById(id) ?: return + + context.profileDir.resolve(entity.base).listFiles()?.forEach { + it.deleteRecursively() + } + } + + private suspend fun downloadProfile(source: Uri, target: File, baseDir: File) { + try { + target.parentFile?.mkdirs() + baseDir.mkdirs() + + if (source.scheme == "content" || source.scheme == "file") { + val fd = context.contentResolver.openFileDescriptor(source, "r") + ?: throw FileNotFoundException("Unable to open file $source") + + Clash.downloadProfile(fd.fd, target, baseDir).await() + } else { + Clash.downloadProfile(source.toString(), target, baseDir).await() + } + } catch (e: Exception) { + target.delete() + baseDir.deleteRecursively() + + throw e + } + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt index cb46b3a632..e5081a0b2e 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt @@ -3,28 +3,24 @@ package com.github.kr328.clash.service import android.app.AlarmManager import android.app.PendingIntent import android.content.Intent -import android.net.Uri import android.os.IBinder -import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.Global import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.data.ClashDatabase -import com.github.kr328.clash.service.data.ClashProfileDao import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.transact.ProfileRequest import com.github.kr328.clash.service.util.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel -import java.io.File -import java.io.FileNotFoundException import java.util.* class ProfileService : BaseService() { private val service = this private val queue: MutableMap> = Hashtable() + private val pending = mutableListOf() - private val profiles: ClashProfileDao by lazy { - ClashDatabase.getInstance(service).openClashProfileDao() - } + private val profiles = ClashDatabase.getInstance(Global.application).openClashProfileDao() + private val processor = ProfileProcessor(this) override fun onBind(intent: Intent?): IBinder? { return object : IProfileService.Stub() { @@ -39,16 +35,38 @@ class ProfileService : BaseService() { override fun queryProfiles(): Array { return profiles.queryProfiles() } + + override fun setActiveProfile(id: Long) { + profiles.setActiveProfile(id) + + broadcastProfileChanged(service) + } + } + } + + override fun onCreate() { + super.onCreate() + + Log.d("ProfileService.onCreate") + } + + override fun onDestroy() { + super.onDestroy() + + pending.forEach { + it.callback?.completeExceptionally("Canceled") } + + Log.d("ProfileService.onDestroy") } private fun createChannelForRequests(id: Long): Channel { return Channel(Channel.UNLIMITED).also { launch { try { - while (isActive) { - Log.d("Coroutine for $id launched") + Log.d("Coroutine for $id launched") + while (isActive) { val request = withTimeout(1000 * 30) { it.receive() } @@ -56,8 +74,7 @@ class ProfileService : BaseService() { Log.d("Handling $id") handleRequest(request) } - } - finally { + } finally { Log.d("Coroutine for $id exited") queue.remove(id) @@ -68,6 +85,10 @@ class ProfileService : BaseService() { private fun enqueueRequest(request: ProfileRequest) { launch { + Log.d("Request $request enqueue") + + pending.add(request) + queue.computeIfAbsent(request.id) { createChannelForRequests(it) }.send(request) @@ -83,6 +104,8 @@ class ProfileService : BaseService() { handleUpdateOrCreate(request) ProfileRequest.Action.REMOVE -> removeProfile(request) + ProfileRequest.Action.CLEAR -> + clearProfile(request) } request.callback?.complete() @@ -90,6 +113,8 @@ class ProfileService : BaseService() { broadcastProfileChanged(this) } catch (e: Exception) { request.callback?.completeExceptionally(e.message) + } finally { + pending.remove(request) } } @@ -120,21 +145,7 @@ class ProfileService : BaseService() { ) } - val url = Uri.parse(entity.uri) - - if (url == null || url == Uri.EMPTY) - throw IllegalArgumentException("Invalid url $url") - - Log.d("Profile ${entity.name} downloading") - - downloadProfile(url, profileDir.resolve(entity.file), clashDir.resolve(entity.base)) - - val newEntity = entity.copy(lastUpdate = System.currentTimeMillis()) - - val newId = if (entity.id == 0L) - profiles.getId(profiles.addProfile(newEntity)) - else - profiles.updateProfile(newEntity).run { entity.id } + val newId = processor.createOrUpdate(entity) if (entity.updateInterval > 0) { val nextRequest = @@ -156,32 +167,10 @@ class ProfileService : BaseService() { } private suspend fun removeProfile(request: ProfileRequest) = withContext(Dispatchers.IO) { - val entity = profiles.queryProfileById(request.id) ?: return@withContext - - clashDir.resolve(entity.base).deleteRecursively() - profileDir.resolve(entity.file).delete() - - profiles.removeProfile(entity.id) + processor.remove(request.id) } - private suspend fun downloadProfile(source: Uri, target: File, baseDir: File) { - try { - target.parentFile?.mkdirs() - baseDir.mkdirs() - - if (source.scheme == "content" || source.scheme == "file") { - val fd = contentResolver.openFileDescriptor(source, "r") - ?: throw FileNotFoundException("Unable to open file $source") - - Clash.downloadProfile(fd.fd, target, baseDir).await() - } else { - Clash.downloadProfile(source.toString(), target, baseDir).await() - } - } catch (e: Exception) { - target.delete() - baseDir.deleteRecursively() - - throw e - } + private suspend fun clearProfile(request: ProfileRequest) = withContext(Dispatchers.IO) { + processor.clear(request.id) } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt index 331bad8f3a..36fd65156c 100644 --- a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt +++ b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt @@ -14,7 +14,7 @@ class ProfileRequest private constructor(private val bundle: Bundle) : Parcelabl ) enum class Action { - UPDATE_OR_CREATE, REMOVE + UPDATE_OR_CREATE, REMOVE, CLEAR } val action: Action From 05f2a12bafd4e34296cb780319c0041584a7952f Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Thu, 13 Feb 2020 16:34:13 +0800 Subject: [PATCH 081/358] refactor file storage logic --- .gitignore | 1 + app/build.gradle | 27 ++- .../com/github/kr328/clash/BaseActivity.kt | 4 +- .../kr328/clash/CreateProfileActivity.kt | 17 +- .../com/github/kr328/clash/MainApplication.kt | 20 ++- .../github/kr328/clash/ProfileEditActivity.kt | 54 ++++-- .../github/kr328/clash/ProfilesActivity.kt | 161 ++++++++++++------ .../github/kr328/clash/remote/Broadcasts.kt | 3 + .../kr328/clash/remote/ProfileClient.kt | 8 + .../github/kr328/clash/weight/ProfilesMenu.kt | 110 ++++++++++++ app/src/main/res/drawable/ic_clear.xml | 9 + app/src/main/res/drawable/ic_copy.xml | 9 + app/src/main/res/drawable/ic_save.xml | 2 +- .../main/res/layout/activity_profile_edit.xml | 4 +- app/src/main/res/values/strings.xml | 7 + build.gradle | 7 +- .../java/com/github/kr328/clash/core/Clash.kt | 1 + .../clash/design/{settings => common}/Base.kt | 4 +- .../CommonUiBuilder.kt} | 4 +- .../CommonUiScreen.kt} | 6 +- .../design/{settings => common}/Option.kt | 4 +- .../design/{settings => common}/TextInput.kt | 7 +- .../kr328/clash/design/view/CommonUiLayout.kt | 25 +++ .../kr328/clash/design/view/SettingsLayout.kt | 27 --- service/build.gradle | 1 + service/src/main/AndroidManifest.xml | 14 +- .../kr328/clash/service/IProfileService.aidl | 5 +- .../kr328/clash/service/ClashService.kt | 4 +- .../github/kr328/clash/service/Constants.kt | 2 + .../clash/service/ProfileBackgroundService.kt | 24 ++- .../kr328/clash/service/ProfileProcessor.kt | 42 +++-- .../kr328/clash/service/ProfileService.kt | 96 +++++++++-- .../clash/service/data/ClashProfileDao.kt | 19 ++- .../clash/service/data/ClashProfileEntity.kt | 6 +- .../service/data/ClashProfileProxyDao.kt | 6 +- .../clash/service/util/BroadcastUtils.kt | 16 +- .../kr328/clash/service/util/FileUtils.kt | 13 +- service/src/main/res/xml/profile_provider.xml | 9 + 38 files changed, 570 insertions(+), 208 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/weight/ProfilesMenu.kt create mode 100644 app/src/main/res/drawable/ic_clear.xml create mode 100644 app/src/main/res/drawable/ic_copy.xml rename design/src/main/java/com/github/kr328/clash/design/{settings => common}/Base.kt (89%) rename design/src/main/java/com/github/kr328/clash/design/{settings/SettingsBuilder.kt => common/CommonUiBuilder.kt} (92%) rename design/src/main/java/com/github/kr328/clash/design/{settings/SettingsScreen.kt => common/CommonUiScreen.kt} (86%) rename design/src/main/java/com/github/kr328/clash/design/{settings => common}/Option.kt (94%) rename design/src/main/java/com/github/kr328/clash/design/{settings => common}/TextInput.kt (93%) create mode 100644 design/src/main/java/com/github/kr328/clash/design/view/CommonUiLayout.kt delete mode 100644 design/src/main/java/com/github/kr328/clash/design/view/SettingsLayout.kt create mode 100644 service/src/main/res/xml/profile_provider.xml diff --git a/.gitignore b/.gitignore index bf25eae7c5..a324a27cde 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ .gradle build/ /app/release/ +/captures # Ignore Gradle GUI config gradle-app.setting diff --git a/app/build.gradle b/app/build.gradle index 86242d0445..1d48ac9430 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -3,8 +3,6 @@ apply plugin: 'kotlinx-serialization' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' -apply plugin: 'com.google.gms.google-services' -apply plugin: 'io.fabric' android { compileSdkVersion 29 @@ -14,7 +12,7 @@ android { minSdkVersion 24 targetSdkVersion 29 versionCode 10035 - versionName "1.0.35-alpha" + versionName "1.1.0" } buildTypes { release { @@ -45,7 +43,7 @@ dependencies { implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.core:core-ktx:1.3.0-alpha01' - implementation 'androidx.fragment:fragment-ktx:1.2.0' + implementation 'androidx.fragment:fragment-ktx:1.2.1' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation "androidx.room:room-runtime:$room_version" @@ -53,6 +51,23 @@ dependencies { implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" implementation "com.google.android.material:material:1.2.0-alpha04" - implementation 'com.google.firebase:firebase-analytics:17.2.2' - implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' + implementation "com.microsoft.appcenter:appcenter-analytics:$app_center_version" + implementation "com.microsoft.appcenter:appcenter-crashes:$app_center_version" } + +task injectAppCenterKey() { + doFirst { + Properties properties = new Properties() + properties.load(rootProject.file('local.properties').newDataInputStream()) + + def key = properties.getProperty("appcenter.key", "") + + android.buildTypes.each { + it.buildConfigField 'String', 'APP_CENTER_KEY', "\"$key\"" + } + } +} + +afterEvaluate { + preBuild.dependsOn(injectAppCenterKey) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 08e7db3cb4..272f6801a8 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -126,9 +126,9 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() recreate() } - protected fun makeSnackbarException(title: String, detail: String) { + protected fun makeSnackbarException(title: String, detail: String?) { Snackbar.make(rootView, title, Snackbar.LENGTH_LONG).setAction(R.string.detail) { - AlertDialog.Builder(this).setTitle(R.string.detail).setMessage(detail).show() + AlertDialog.Builder(this).setTitle(R.string.detail).setMessage(detail ?: "Unknown").show() }.show() } diff --git a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt index 67fba865ae..5ae1d55e2c 100644 --- a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash +import android.app.Activity import android.content.ComponentName import android.content.Context import android.content.Intent @@ -19,6 +20,10 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class CreateProfileActivity : BaseActivity() { + companion object { + const val REQUEST_CODE = 20000 + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -36,10 +41,11 @@ class CreateProfileActivity : BaseActivity() { mainList.setOnItemClickListener { _, _, position, _ -> val item = providers[position] - startActivity( + startActivityForResult( ProfileEditActivity::class.intent .putExtra("type", item.type) - .putExtra("intent", item.intent) + .putExtra("intent", item.intent), + REQUEST_CODE ) } mainList.setOnItemLongClickListener { _, _, position, _ -> @@ -60,6 +66,13 @@ class CreateProfileActivity : BaseActivity() { } } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if ( requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK ) + return finish() + + super.onActivityResult(requestCode, resultCode, data) + } + private suspend fun queryUrlProviders(): List = withContext(Dispatchers.IO) { val common = listOf( diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index cc00f7f27c..853a8de106 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,15 +2,15 @@ package com.github.kr328.clash import android.app.Application import android.content.Context -import com.crashlytics.android.Crashlytics import com.github.kr328.clash.core.Global import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.remote.Remote -import com.google.firebase.FirebaseApp -import io.fabric.sdk.android.Fabric +import com.microsoft.appcenter.AppCenter +import com.microsoft.appcenter.analytics.Analytics +import com.microsoft.appcenter.crashes.Crashes @Suppress("unused") -class MainApplication : Application() { +class MainApplication: Application() { override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) @@ -20,11 +20,13 @@ class MainApplication : Application() { override fun onCreate() { super.onCreate() - runCatching { - FirebaseApp.initializeApp(this) - } - runCatching { - Fabric.with(this, Crashlytics()) + // Initialize AppCenter + if ( BuildConfig.APP_CENTER_KEY.isNotEmpty() && !BuildConfig.DEBUG ) { + AppCenter.start( + this, + BuildConfig.APP_CENTER_KEY, + Analytics::class.java, Crashes::class.java + ) } Remote.init(this) diff --git a/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt index 677413fb5d..5c316ca4a1 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt @@ -7,7 +7,7 @@ import android.os.Bundle import android.view.View import android.webkit.MimeTypeMap import androidx.appcompat.app.AlertDialog -import com.github.kr328.clash.design.settings.TextInput +import com.github.kr328.clash.design.common.TextInput import com.github.kr328.clash.remote.withProfile import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.ipc.IStreamCallback @@ -16,6 +16,7 @@ import com.github.kr328.clash.service.transact.ProfileRequest import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_profile_edit.* import kotlinx.coroutines.launch +import java.lang.Exception import java.lang.IllegalArgumentException class ProfileEditActivity : BaseActivity() { @@ -73,6 +74,7 @@ class ProfileEditActivity : BaseActivity() { title = getString(R.string.name), icon = getDrawable(R.drawable.ic_label_outline), hint = getString(R.string.profile_name), + content = intent.getStringExtra("name") ?: "", id = KEY_NAME ) { onTextChanged { @@ -83,6 +85,7 @@ class ProfileEditActivity : BaseActivity() { title = getString(R.string.url), icon = getDrawable(R.drawable.ic_content), hint = getString(R.string.profile_url), + content = intent.getStringExtra("url") ?: "", id = KEY_URL ) { onOpenInput { @@ -100,7 +103,8 @@ class ProfileEditActivity : BaseActivity() { title = getString(R.string.auto_update), icon = getDrawable(R.drawable.ic_update), hint = getString(R.string.in_minutes), - id = KEY_AUTO_UPDATE + id = KEY_AUTO_UPDATE, + content = intent.getStringExtra("interval") ?: "" ) { onDisplayContent { val interval = it.toString().toIntOrNull() ?: 0 @@ -121,6 +125,9 @@ class ProfileEditActivity : BaseActivity() { modified = true } } + + if ( intent.getStringExtra("type") == Constants.URL_PROVIDER_TYPE_FILE ) + isHidden = true } } @@ -150,7 +157,18 @@ class ProfileEditActivity : BaseActivity() { } } - openUrlProvider() + when (intent.extras?.getLong("id", -1L)) { + -1L -> { + openUrlProvider() + setTitle(R.string.new_profile) + } + 0L -> { + setTitle(R.string.new_profile) + } + else -> { + setTitle(R.string.edit_profile) + } + } } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -200,18 +218,23 @@ class ProfileEditActivity : BaseActivity() { val type = intent.getStringExtra("type") val externalIntent = intent.getParcelableExtra("intent") - when (type) { - Constants.URL_PROVIDER_TYPE_FILE -> - startActivityForResult( - Intent(Intent.ACTION_GET_CONTENT).setType(TYPE_YAML), - REQUEST_CODE - ) - Constants.URL_PROVIDER_TYPE_EXTERNAL -> - startActivityForResult( - externalIntent ?: throw NullPointerException(), - REQUEST_CODE - ) - else -> return false + try { + when (type) { + Constants.URL_PROVIDER_TYPE_FILE -> + startActivityForResult( + Intent(Intent.ACTION_GET_CONTENT).setType(TYPE_YAML), + REQUEST_CODE + ) + Constants.URL_PROVIDER_TYPE_EXTERNAL -> + startActivityForResult( + externalIntent ?: throw NullPointerException(), + REQUEST_CODE + ) + else -> return false + } + } + catch (e: Exception) { + makeSnackbarException(getString(R.string.start_url_provider_failure), e.message) } return true @@ -232,6 +255,7 @@ class ProfileEditActivity : BaseActivity() { val request = ProfileRequest() .action(ProfileRequest.Action.UPDATE_OR_CREATE) + .withId(intent.getLongExtra("id", 0)) .withName(name) .withURL(url) .withUpdateInterval(interval) diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index f48c1c87f8..fff1a4cb66 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -1,25 +1,33 @@ package com.github.kr328.clash +import android.content.Intent +import android.net.Uri import android.os.Bundle -import android.util.TypedValue -import android.view.ViewGroup.LayoutParams -import androidx.annotation.ColorInt import androidx.recyclerview.widget.LinearLayoutManager import com.github.kr328.clash.adapter.ProfileAdapter -import com.github.kr328.clash.design.view.SettingsLayout +import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.remote.withProfile -import com.github.kr328.clash.service.ProfileBackgroundService import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.transact.ProfileRequest import com.github.kr328.clash.service.util.intent +import com.github.kr328.clash.weight.ProfilesMenu import com.google.android.material.bottomsheet.BottomSheetDialog import kotlinx.android.synthetic.main.activity_profiles.* import kotlinx.coroutines.Job import kotlinx.coroutines.delay import kotlinx.coroutines.isActive import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import java.util.* + +class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.Callback { + companion object { + private const val EDITOR_REQUEST_CODE = 30000 + } -class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback { private var backgroundJob: Job? = null + private val reloadMutex = Mutex() + private val editorStack = Stack() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -52,19 +60,42 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback { backgroundJob = null } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == EDITOR_REQUEST_CODE) { + launch { + val uri = editorStack.pop() + + withProfile { + commitProfileEditUri(uri) + } + } + + return + } + + super.onActivityResult(requestCode, resultCode, data) + } + override suspend fun onClashProfileChanged(active: ClashProfileEntity?) { super.onClashProfileChanged(active) + Log.d("Broadcast received") + reloadProfiles() } private suspend fun reloadProfiles() { + if (!reloadMutex.tryLock()) + return + val profiles = withProfile { queryProfiles() } (mainList.adapter as ProfileAdapter) .setEntitiesAsync(profiles.toList()) + + reloadMutex.unlock() } override fun onProfileClicked(entity: ClashProfileEntity) { @@ -76,63 +107,91 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback { } override fun onMenuClicked(entity: ClashProfileEntity) { - val dialog = BottomSheetDialog(this) - val menu = SettingsLayout(this).apply { - layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT) + ProfilesMenu(this, entity, this).show() + } + + override fun onNewProfile() { + startActivity(CreateProfileActivity::class.intent) + } + + private fun deleteProfile(entity: ClashProfileEntity) = launch { + val request = ProfileRequest().action(ProfileRequest.Action.REMOVE).withId(entity.id) + + withProfile { + enqueueRequest(request) } - @ColorInt - val errorColor = TypedValue().run { - theme.resolveAttribute(R.attr.colorError, this, true) - data + } + + private fun resetProviders(entity: ClashProfileEntity) = launch { + val request = ProfileRequest().action(ProfileRequest.Action.CLEAR).withId(entity.id) + + withProfile { + enqueueRequest(request) } + } - menu.build { - if (entity.type != ClashProfileEntity.TYPE_FILE) { - option( - title = getString(R.string.update), - icon = getDrawable(R.drawable.ic_update) - ) { + private fun openPropertiesEditor(entity: ClashProfileEntity, duplicate: Boolean) { + val type = when (entity.type) { + ClashProfileEntity.TYPE_FILE -> + Constants.URL_PROVIDER_TYPE_FILE + ClashProfileEntity.TYPE_URL -> + Constants.URL_PROVIDER_TYPE_URL + ClashProfileEntity.TYPE_EXTERNAL -> + Constants.URL_PROVIDER_TYPE_EXTERNAL + else -> throw IllegalArgumentException("Invalid type ${entity.type}") + } + val intent = entity.source?.run { Intent.parseUri(this, 0) } + val name = entity.name + val uri = entity.uri + val interval = entity.updateInterval.toString() + + val editor = ProfileEditActivity::class.intent + .putExtra("id", if (duplicate) 0 else entity.id) + .putExtra("type", type) + .putExtra("intent", intent) + .putExtra("name", name) + .putExtra("url", uri) + .putExtra("interval", interval) + + startActivity(editor) + } - } - } else { - option( - title = getString(R.string.edit), - icon = getDrawable(R.drawable.ic_edit) - ) { + private fun openEditor(entity: ClashProfileEntity) = launch { + val uri = withProfile { + requestProfileEditUri(entity.id) + } ?: return@launch - } - } - option( - title = getString(R.string.properties), - icon = getDrawable(R.drawable.ic_properties) - ) { + editorStack.push(uri) - } - option( - title = getString(R.string.clear_cache), - icon = getDrawable(R.drawable.ic_delete_sweep) - ) { + startActivityForResult( + Intent(Intent.ACTION_VIEW) + .setDataAndType(Uri.parse(uri), "text/plain") + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION), + EDITOR_REQUEST_CODE + ) + } - } - option( - title = getString(R.string.delete), - icon = getDrawable(R.drawable.ic_delete_colorful) - ) { - textColor = errorColor - } - } + override fun onOpenEditor(entity: ClashProfileEntity) { + openEditor(entity) + } + + override fun onUpdate(entity: ClashProfileEntity) { - dialog.setContentView(menu) - dialog.show() } - override fun onNewProfile() { - startActivity(CreateProfileActivity::class.intent) + override fun onOpenProperties(entity: ClashProfileEntity) { + openPropertiesEditor(entity, false) } - private fun sendDelete(entity: ClashProfileEntity) { - launch { + override fun onDuplicate(entity: ClashProfileEntity) { + openPropertiesEditor(entity, true) + } - } + override fun onResetProvider(entity: ClashProfileEntity) { + resetProviders(entity) + } + + override fun onDelete(entity: ClashProfileEntity) { + deleteProfile(entity) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt index 379c3a7e87..5340e90b27 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt @@ -25,6 +25,9 @@ object Broadcasts { private val receivers = mutableListOf() private val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { + if ( intent?.`package` != context?.packageName ) + return + when (intent?.action) { Intents.INTENT_ACTION_CLASH_STARTED -> receivers.forEach { diff --git a/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt index 661042bf66..bf70528e2d 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ProfileClient.kt @@ -22,4 +22,12 @@ class ProfileClient(private val service: IProfileService) { suspend fun setActiveProfile(id: Long) = withContext(Dispatchers.IO) { service.setActiveProfile(id) } + + suspend fun requestProfileEditUri(id: Long): String? = withContext(Dispatchers.IO) { + service.requestProfileEditUri(id) + } + + suspend fun commitProfileEditUri(uri: String) = withContext(Dispatchers.IO) { + service.commitProfileEditUri(uri) + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/weight/ProfilesMenu.kt b/app/src/main/java/com/github/kr328/clash/weight/ProfilesMenu.kt new file mode 100644 index 0000000000..1763960122 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/weight/ProfilesMenu.kt @@ -0,0 +1,110 @@ +package com.github.kr328.clash.weight + +import android.content.Context +import android.util.TypedValue +import android.view.ViewGroup +import androidx.annotation.ColorInt +import com.github.kr328.clash.R +import com.github.kr328.clash.design.view.CommonUiLayout +import com.github.kr328.clash.service.data.ClashProfileEntity +import com.google.android.material.bottomsheet.BottomSheetDialog + +class ProfilesMenu( + context: Context, + private val entity: ClashProfileEntity, + private val callback: Callback +) : BottomSheetDialog(context) { + interface Callback { + fun onOpenEditor(entity: ClashProfileEntity) + fun onUpdate(entity: ClashProfileEntity) + fun onOpenProperties(entity: ClashProfileEntity) + fun onDuplicate(entity: ClashProfileEntity) + fun onResetProvider(entity: ClashProfileEntity) + fun onDelete(entity: ClashProfileEntity) + } + + init { + val menu = CommonUiLayout(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + @ColorInt + val errorColor = TypedValue().run { + context.theme.resolveAttribute(R.attr.colorError, this, true) + data + } + + menu.build { + if (entity.type != ClashProfileEntity.TYPE_FILE) { + option( + title = context.getString(R.string.update), + icon = context.getDrawable(R.drawable.ic_update) + ) { + onClick { + callback.onUpdate(entity) + + dismiss() + } + } + } else { + option( + title = context.getString(R.string.edit), + icon = context.getDrawable(R.drawable.ic_edit) + ) { + onClick { + callback.onOpenEditor(entity) + + dismiss() + } + } + } + option( + title = context.getString(R.string.properties), + icon = context.getDrawable(R.drawable.ic_properties) + ) { + onClick { + callback.onOpenProperties(entity) + + dismiss() + } + } + option( + title = context.getString(R.string.duplicate), + icon = context.getDrawable(R.drawable.ic_copy) + ) { + onClick { + callback.onDuplicate(entity) + + dismiss() + } + } + option( + title = context.getString(R.string.reset_provider), + icon = context.getDrawable(R.drawable.ic_clear) + ) { + onClick { + callback.onResetProvider(entity) + + dismiss() + } + } + option( + title = context.getString(R.string.delete), + icon = context.getDrawable(R.drawable.ic_delete_colorful) + ) { + textColor = errorColor + + onClick { + callback.onDelete(entity) + + dismiss() + } + } + } + + dismissWithAnimation = true + setContentView(menu) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_clear.xml b/app/src/main/res/drawable/ic_clear.xml new file mode 100644 index 0000000000..1fc03e2b10 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_copy.xml b/app/src/main/res/drawable/ic_copy.xml new file mode 100644 index 0000000000..98c8be9cc7 --- /dev/null +++ b/app/src/main/res/drawable/ic_copy.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml index 620bc91334..faa65b0a82 100644 --- a/app/src/main/res/drawable/ic_save.xml +++ b/app/src/main/res/drawable/ic_save.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/layout/activity_profile_edit.xml b/app/src/main/res/layout/activity_profile_edit.xml index 83bb412cb2..36ceba467d 100644 --- a/app/src/main/res/layout/activity_profile_edit.xml +++ b/app/src/main/res/layout/activity_profile_edit.xml @@ -36,7 +36,7 @@ android:layout_gravity="center" android:layout_width="30dp" android:layout_height="30dp" - android:indeterminate="true"/> + android:indeterminate="true" /> @@ -45,7 +45,7 @@ android:layout_width="match_parent" android:layout_height="match_parent"> - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b693fadbf7..519b0403d2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -60,6 +60,8 @@ Delete Clear Cache Properties + Duplicate + Reset Providers Exit without Save All changed will *LOST* @@ -69,6 +71,11 @@ Invalid URL Processing + New Profile + Edit Profile + + Start URL Provider Failure + %s (File) %s (URL) Remove diff --git a/build.gradle b/build.gradle index e205f9c6ca..91c249ffb7 100644 --- a/build.gradle +++ b/build.gradle @@ -5,21 +5,16 @@ buildscript { kotlin_version = '1.3.61' kotlin_coroutine_version = '1.3.3' room_version = '2.2.3' + app_center_version = '2.5.1' } repositories { google() jcenter() - - maven { - url 'https://maven.fabric.io/public' - } } dependencies { classpath 'com.android.tools.build:gradle:3.5.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" - classpath 'com.google.gms:google-services:4.3.3' - classpath 'io.fabric.tools:gradle:1.31.2' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 6cdf45c814..46e83aa5c3 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -31,6 +31,7 @@ object Clash { Bridge.loadMMDB(bytes) Bridge.setHome(context.cacheDir.absolutePath) + Bridge.reset() } fun start() { diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/Base.kt b/design/src/main/java/com/github/kr328/clash/design/common/Base.kt similarity index 89% rename from design/src/main/java/com/github/kr328/clash/design/settings/Base.kt rename to design/src/main/java/com/github/kr328/clash/design/common/Base.kt index a834ced149..e801ccbec1 100644 --- a/design/src/main/java/com/github/kr328/clash/design/settings/Base.kt +++ b/design/src/main/java/com/github/kr328/clash/design/common/Base.kt @@ -1,10 +1,10 @@ -package com.github.kr328.clash.design.settings +package com.github.kr328.clash.design.common import android.content.Context import android.os.Bundle import android.view.View -abstract class Base(val screen: SettingsScreen) { +abstract class Base(val screen: CommonUiScreen) { val context: Context get() = screen.layout.context diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt b/design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt similarity index 92% rename from design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt rename to design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt index fea0493be1..1642ccb4ae 100644 --- a/design/src/main/java/com/github/kr328/clash/design/settings/SettingsBuilder.kt +++ b/design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt @@ -1,9 +1,9 @@ -package com.github.kr328.clash.design.settings +package com.github.kr328.clash.design.common import android.content.Context import android.graphics.drawable.Drawable -class SettingsBuilder(val screen: SettingsScreen) { +class CommonUiBuilder(val screen: CommonUiScreen) { val context: Context get() = screen.layout.context diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/SettingsScreen.kt b/design/src/main/java/com/github/kr328/clash/design/common/CommonUiScreen.kt similarity index 86% rename from design/src/main/java/com/github/kr328/clash/design/settings/SettingsScreen.kt rename to design/src/main/java/com/github/kr328/clash/design/common/CommonUiScreen.kt index 343afd8743..37af3656ba 100644 --- a/design/src/main/java/com/github/kr328/clash/design/settings/SettingsScreen.kt +++ b/design/src/main/java/com/github/kr328/clash/design/common/CommonUiScreen.kt @@ -1,10 +1,10 @@ -package com.github.kr328.clash.design.settings +package com.github.kr328.clash.design.common import android.os.Bundle import android.os.Handler -import com.github.kr328.clash.design.view.SettingsLayout +import com.github.kr328.clash.design.view.CommonUiLayout -class SettingsScreen(val layout: SettingsLayout) { +class CommonUiScreen(val layout: CommonUiLayout) { private val handler = Handler() val elements = mutableListOf() diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/Option.kt b/design/src/main/java/com/github/kr328/clash/design/common/Option.kt similarity index 94% rename from design/src/main/java/com/github/kr328/clash/design/settings/Option.kt rename to design/src/main/java/com/github/kr328/clash/design/common/Option.kt index a66a2363ed..90f3661b04 100644 --- a/design/src/main/java/com/github/kr328/clash/design/settings/Option.kt +++ b/design/src/main/java/com/github/kr328/clash/design/common/Option.kt @@ -1,4 +1,4 @@ -package com.github.kr328.clash.design.settings +package com.github.kr328.clash.design.common import android.graphics.drawable.Drawable import android.os.Bundle @@ -7,7 +7,7 @@ import android.view.View import android.widget.TextView import com.github.kr328.clash.design.R -class Option(screen: SettingsScreen): Base(screen) { +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) diff --git a/design/src/main/java/com/github/kr328/clash/design/settings/TextInput.kt b/design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt similarity index 93% rename from design/src/main/java/com/github/kr328/clash/design/settings/TextInput.kt rename to design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt index 218b58a40a..2872c68b68 100644 --- a/design/src/main/java/com/github/kr328/clash/design/settings/TextInput.kt +++ b/design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt @@ -1,7 +1,8 @@ -package com.github.kr328.clash.design.settings +package com.github.kr328.clash.design.common import android.graphics.drawable.Drawable import android.os.Bundle +import android.util.Log import android.view.LayoutInflater import android.view.View import android.widget.EditText @@ -9,7 +10,7 @@ import android.widget.TextView import androidx.appcompat.app.AlertDialog import com.github.kr328.clash.design.R -class TextInput(screen: SettingsScreen) : Base(screen) { +class TextInput(screen: CommonUiScreen) : Base(screen) { override val view: View = LayoutInflater.from(context).inflate( R.layout.view_setting_text_input, screen.layout, @@ -75,7 +76,7 @@ class TextInput(screen: SettingsScreen) : Base(screen) { override fun applyAttribute(enabled: Boolean, hidden: Boolean) { view.isEnabled = enabled - view.visibility = if (hidden) View.GONE else View.INVISIBLE + view.visibility = if (hidden) View.GONE else View.VISIBLE } override fun saveState(bundle: Bundle) { diff --git a/design/src/main/java/com/github/kr328/clash/design/view/CommonUiLayout.kt b/design/src/main/java/com/github/kr328/clash/design/view/CommonUiLayout.kt new file mode 100644 index 0000000000..a48f6e4fe1 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/view/CommonUiLayout.kt @@ -0,0 +1,25 @@ +package com.github.kr328.clash.design.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import com.github.kr328.clash.design.common.CommonUiBuilder +import com.github.kr328.clash.design.common.CommonUiScreen + +class CommonUiLayout @JvmOverloads constructor( + context: Context, + attributeSet: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attributeSet, defStyleAttr) { + val screen: CommonUiScreen = CommonUiScreen(this) + + init { + orientation = VERTICAL + } + + fun build(builder: CommonUiBuilder.() -> Unit) { + screen.clear() + + CommonUiBuilder(screen).apply(builder) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/view/SettingsLayout.kt b/design/src/main/java/com/github/kr328/clash/design/view/SettingsLayout.kt deleted file mode 100644 index c843d09982..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/view/SettingsLayout.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.kr328.clash.design.view - -import android.animation.LayoutTransition -import android.content.Context -import android.util.AttributeSet -import android.widget.LinearLayout -import com.github.kr328.clash.design.settings.SettingsBuilder -import com.github.kr328.clash.design.settings.SettingsScreen - -class SettingsLayout @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyleAttr: Int = 0 -) : LinearLayout(context, attributeSet, defStyleAttr) { - val screen: SettingsScreen = SettingsScreen(this) - - init { - layoutTransition = LayoutTransition() - orientation = VERTICAL - } - - fun build(builder: SettingsBuilder.() -> Unit) { - screen.clear() - - SettingsBuilder(screen).apply(builder) - } -} \ No newline at end of file diff --git a/service/build.gradle b/service/build.gradle index 1d0f15cc3c..a53f87b778 100644 --- a/service/build.gradle +++ b/service/build.gradle @@ -41,5 +41,6 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutine_version" implementation "androidx.room:room-runtime:$room_version" + implementation "androidx.room:room-ktx:$room_version" implementation 'androidx.core:core-ktx:1.3.0-alpha01' } \ No newline at end of file diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index 6c5291f93d..8d8c318e11 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -30,13 +30,25 @@ android:label="@string/clash_profile_service_label" android:exported="false" android:process=":background" /> - + + + + + diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl index e96139209f..e1970101b3 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl @@ -4,13 +4,12 @@ import com.github.kr328.clash.service.transact.ProfileRequest; import com.github.kr328.clash.service.data.ClashProfileEntity; interface IProfileService { - // process void enqueueRequest(in ProfileRequest request); + String requestProfileEditUri(long id); + void commitProfileEditUri(String uri); - // query ClashProfileEntity[] queryProfiles(); ClashProfileEntity queryActiveProfile(); - // set void setActiveProfile(long id); } 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 1cbaf91ba9..a9eb065398 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 @@ -84,8 +84,8 @@ class ClashService : BaseService() { try { Clash.loadProfile( - profileDir.resolve(active.file), - clashDir.resolve(active.base) + resolveProfile(active.id), + resolveBase(active.id) ).await() notification.setProfile(active.name) diff --git a/service/src/main/java/com/github/kr328/clash/service/Constants.kt b/service/src/main/java/com/github/kr328/clash/service/Constants.kt index a0a16428c0..852cf12eec 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Constants.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Constants.kt @@ -3,4 +3,6 @@ package com.github.kr328.clash.service object Constants { const val CLASH_DIR = "clash" const val PROFILES_DIR = "profiles" + + const val PROFILE_PROVIDER_SUFFIX = ".profiles" } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt index 71db50bf7b..04a48b9697 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt @@ -85,7 +85,9 @@ class ProfileBackgroundService : BaseService() { channel.offer(request) } Intents.INTENT_ACTION_PROFILE_SETUP -> { - resetProfileUpdateAlarm() + launch { + resetProfileUpdateAlarm() + } } } @@ -108,16 +110,22 @@ class ProfileBackgroundService : BaseService() { it.withCallback(object : IStreamCallback.Stub() { override fun complete() { deferred.complete(it) - updateUpdateComplete(it.id) + launch { + updateUpdateComplete(it.id) + } } override fun completeExceptionally(reason: String?) { deferred.complete(it) - updateUpdateFailure(it.id) + launch { + updateUpdateFailure(it.id) + } } override fun send(data: ParcelableContainer?) { - updateUpdating(it.id) + launch { + updateUpdating(it.id) + } } }) @@ -144,7 +152,7 @@ class ProfileBackgroundService : BaseService() { } } - private fun resetProfileUpdateAlarm() { + private suspend fun resetProfileUpdateAlarm() { for (entity in profiles.queryProfiles()) { if (entity.updateInterval <= 0) continue @@ -197,7 +205,7 @@ class ProfileBackgroundService : BaseService() { startForeground(SERVICE_NOTIFICATION_ID_BASE, notification) } - private fun updateUpdating(id: Long) { + private suspend fun updateUpdating(id: Long) { val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() val entity = database.queryProfileById(id) ?: return @@ -212,7 +220,7 @@ class ProfileBackgroundService : BaseService() { .notify(SERVICE_NOTIFICATION_ID_BASE + notificationId, notification) } - private fun updateUpdateComplete(id: Long) { + private suspend fun updateUpdateComplete(id: Long) { val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() val entity = database.queryProfileById(id) @@ -231,7 +239,7 @@ class ProfileBackgroundService : BaseService() { .notify(SERVICE_NOTIFICATION_ID_BASE + notificationId, notification) } - private fun updateUpdateFailure(id: Long) { + private suspend fun updateUpdateFailure(id: Long) { val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() val entity = database.queryProfileById(id) diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt index 0046e147ab..a097441012 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt @@ -2,16 +2,17 @@ package com.github.kr328.clash.service import android.content.Context import android.net.Uri +import androidx.core.content.FileProvider import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.service.util.clashDir -import com.github.kr328.clash.service.util.profileDir +import com.github.kr328.clash.service.util.resolveBase +import com.github.kr328.clash.service.util.resolveProfile import java.io.File import java.io.FileNotFoundException class ProfileProcessor(private val context: Context) { - suspend fun createOrUpdate(entity: ClashProfileEntity): Long { + suspend fun createOrUpdate(entity: ClashProfileEntity, newRecord: Boolean) { val database = ClashDatabase.getInstance(context).openClashProfileDao() val uri = Uri.parse(entity.uri) @@ -20,33 +21,39 @@ class ProfileProcessor(private val context: Context) { downloadProfile( uri, - context.clashDir.resolve(entity.file), - context.profileDir.resolve(entity.base) + resolveProfile(entity.id), + resolveBase(entity.id) ) - val newEntity = entity.copy(lastUpdate = System.currentTimeMillis()) + val newEntity = if (entity.type == ClashProfileEntity.TYPE_FILE) + entity.copy( + lastUpdate = System.currentTimeMillis(), + uri = FileProvider.getUriForFile( + context, + "${context.packageName}${Constants.PROFILE_PROVIDER_SUFFIX}", + resolveProfile(entity.id) + ).toString() + ) + else + entity.copy(lastUpdate = System.currentTimeMillis()) - return if ( newEntity.id == 0L ) - database.addProfile(newEntity).let { database.getId(it) } + if (newRecord) + database.addProfile(newEntity) else - database.updateProfile(newEntity).let { newEntity.id } + database.updateProfile(newEntity) } - fun remove(id: Long) { + suspend fun remove(id: Long) { val database = ClashDatabase.getInstance(context).openClashProfileDao() - val entity = database.queryProfileById(id) ?: return - context.profileDir.resolve(entity.file).delete() - context.clashDir.resolve(entity.base).deleteRecursively() + resolveProfile(id).delete() + resolveBase(id).deleteRecursively() database.removeProfile(id) } fun clear(id: Long) { - val database = ClashDatabase.getInstance(context).openClashProfileDao() - val entity = database.queryProfileById(id) ?: return - - context.profileDir.resolve(entity.base).listFiles()?.forEach { + resolveBase(id).listFiles()?.forEach { it.deleteRecursively() } } @@ -67,7 +74,6 @@ class ProfileProcessor(private val context: Context) { } catch (e: Exception) { target.delete() baseDir.deleteRecursively() - throw e } } diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt index e5081a0b2e..5707a597d9 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt @@ -3,15 +3,21 @@ package com.github.kr328.clash.service import android.app.AlarmManager import android.app.PendingIntent import android.content.Intent +import android.net.Uri +import android.os.Build import android.os.IBinder +import androidx.core.content.FileProvider import com.github.kr328.clash.core.Global import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.ipc.IStreamCallback +import com.github.kr328.clash.service.ipc.ParcelableContainer import com.github.kr328.clash.service.transact.ProfileRequest import com.github.kr328.clash.service.util.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel +import java.io.File import java.util.* class ProfileService : BaseService() { @@ -29,17 +35,77 @@ class ProfileService : BaseService() { } override fun queryActiveProfile(): ClashProfileEntity? { - return profiles.queryActiveProfile() + return runBlocking { + profiles.queryActiveProfile() + } } override fun queryProfiles(): Array { - return profiles.queryProfiles() + return runBlocking { + profiles.queryProfiles() + } } override fun setActiveProfile(id: Long) { - profiles.setActiveProfile(id) + launch { + profiles.setActiveProfile(id) + + broadcastProfileChanged(service) + } + } + + override fun requestProfileEditUri(id: Long): String? { + return runBlocking { + val entity = profiles.queryProfileById(id) ?: return@runBlocking null + + val baseDir = cacheDir.resolve("profiles").apply { mkdirs() } - broadcastProfileChanged(service) + val fileName = RandomUtils.fileName(baseDir, ".yaml") + + val file = resolveProfile(entity.id).copyTo(baseDir.resolve(fileName)) + + val url = FileProvider.getUriForFile( + service, + "$packageName${Constants.PROFILE_PROVIDER_SUFFIX}", + file + ).toString() + + Log.d("Generated template file $file") + + "$url?id=${entity.id}&fileName=$fileName" + } + } + + override fun commitProfileEditUri(uri: String?) { + val u = Uri.parse(uri) + + if (u == null || u == Uri.EMPTY) + return + + val id = u.getQueryParameter("id")?.toLongOrNull() ?: return + val fileName = u.getQueryParameter("fileName") ?: return + + val request = ProfileRequest().withId(id).withURL(u) + .withCallback(object : IStreamCallback.Stub() { + override fun complete() { + cacheDir.resolve("profiles/$fileName").delete() + } + + override fun completeExceptionally(reason: String?) { + cacheDir.resolve("profiles/$fileName").delete() + } + + override fun send(data: ParcelableContainer?) { + + } + }) + val i = ProfileBackgroundService::class.intent + .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, request) + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + startForegroundService(i) + else + startService(i) } } } @@ -84,15 +150,13 @@ class ProfileService : BaseService() { } private fun enqueueRequest(request: ProfileRequest) { - launch { - Log.d("Request $request enqueue") + Log.d("Request $request enqueue") - pending.add(request) + pending.add(request) - queue.computeIfAbsent(request.id) { - createChannelForRequests(it) - }.send(request) - } + queue.computeIfAbsent(request.id) { + createChannelForRequests(it) + }.offer(request) } private suspend fun handleRequest(request: ProfileRequest) { @@ -112,6 +176,7 @@ class ProfileService : BaseService() { broadcastProfileChanged(this) } catch (e: Exception) { + Log.w("handleRequest", e) request.callback?.completeExceptionally(e.message) } finally { pending.remove(request) @@ -129,11 +194,10 @@ class ProfileService : BaseService() { requireNotNull(request.type), requireNotNull(request.url).toString(), request.source?.toString(), - RandomUtils.fileName(profileDir, ".yaml"), - RandomUtils.fileName(clashDir), false, 0, - request.interval.takeIf { it >= 0 } ?: 0 + request.interval.takeIf { it >= 0 } ?: 0, + profiles.generateNewId() ) } else { val e = profiles.queryProfileById(id) ?: return@withContext @@ -145,11 +209,11 @@ class ProfileService : BaseService() { ) } - val newId = processor.createOrUpdate(entity) + processor.createOrUpdate(entity, id == 0L) if (entity.updateInterval > 0) { val nextRequest = - ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE).withId(newId) + ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE).withId(entity.id) requireNotNull(getSystemService(AlarmManager::class.java)).set( AlarmManager.RTC, diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt index 8219106f2f..ff8220241f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileDao.kt @@ -5,26 +5,29 @@ import androidx.room.* @Dao interface ClashProfileDao { @Query("UPDATE profiles SET active = CASE WHEN id = :id THEN 1 ELSE 0 END") - fun setActiveProfile(id: Long) + suspend fun setActiveProfile(id: Long) @Query("SELECT * FROM profiles WHERE active = 1 LIMIT 1") - fun queryActiveProfile(): ClashProfileEntity? + suspend fun queryActiveProfile(): ClashProfileEntity? @Query("SELECT * FROM profiles") - fun queryProfiles(): Array + suspend fun queryProfiles(): Array @Query("SELECT * FROM profiles WHERE id = :id") - fun queryProfileById(id: Long): ClashProfileEntity? + suspend fun queryProfileById(id: Long): ClashProfileEntity? @Insert(onConflict = OnConflictStrategy.ABORT) - fun addProfile(profile: ClashProfileEntity): Long + suspend fun addProfile(profile: ClashProfileEntity): Long @Update(onConflict = OnConflictStrategy.ABORT) - fun updateProfile(profile: ClashProfileEntity) + suspend fun updateProfile(profile: ClashProfileEntity) @Query("DELETE FROM profiles WHERE id = :id") - fun removeProfile(id: Long) + suspend fun removeProfile(id: Long) @Query("SELECT id FROM profiles WHERE rowId = :rowId") - fun getId(rowId: Long): Long + suspend fun getId(rowId: Long): Long + + @Query("SELECT IfNull(MAX(id) + 1, 0) AS id FROM profiles") + suspend fun generateNewId(): Long } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index 180cec9f3d..b103f19677 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -8,19 +8,17 @@ import androidx.room.PrimaryKey import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable -@Entity(tableName = "profiles") +@Entity(tableName = "profiles", primaryKeys = ["id"]) @Serializable data class ClashProfileEntity( @ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "type") val type: Int, @ColumnInfo(name = "uri") val uri: String, @ColumnInfo(name = "source") val source: String?, - @ColumnInfo(name = "file") val file: String, - @ColumnInfo(name = "base") val base: String, @ColumnInfo(name = "active") val active: Boolean, @ColumnInfo(name = "last_update") val lastUpdate: Long, @ColumnInfo(name = "update_interval") val updateInterval: Long, - @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Long = 0 + @ColumnInfo(name = "id") val id: Long ) : Parcelable { override fun writeToParcel(parcel: Parcel, flags: Int) { Parcels.dump(serializer(), this, parcel) diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyDao.kt index cfd9ce43c1..900b0a5456 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyDao.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyDao.kt @@ -8,11 +8,11 @@ import androidx.room.Query @Dao interface ClashProfileProxyDao { @Insert(onConflict = OnConflictStrategy.REPLACE) - fun setSelectedForProfile(item: ClashProfileProxyEntity) + suspend fun setSelectedForProfile(item: ClashProfileProxyEntity) @Query("SELECT * FROM profile_select_proxies WHERE profile_id = :id") - fun querySelectedForProfile(id: Int): List + suspend fun querySelectedForProfile(id: Int): List @Query("DELETE FROM profile_select_proxies WHERE profile_id = :id AND proxy in (:selected)") - fun removeSelectedForProfile(id: Int, selected: List) + suspend fun removeSelectedForProfile(id: Int, selected: List) } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt index 76676b57d8..b8a00d607c 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt @@ -2,22 +2,24 @@ package com.github.kr328.clash.service.util import android.content.Context import android.content.Intent +import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.Intents import com.github.kr328.clash.service.data.ClashDatabase +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext fun Context.sendBroadcastSelf(intent: Intent) { this.sendBroadcast(intent.setPackage(this.packageName)) } -fun broadcastProfileChanged(context: Context) { +suspend fun broadcastProfileChanged(context: Context) { val active = ClashDatabase.getInstance(context).openClashProfileDao().queryActiveProfile() + val intent = Intent(Intents.INTENT_ACTION_PROFILE_CHANGED) + .putExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE, active) - context.sendBroadcastSelf( - Intent(Intents.INTENT_ACTION_PROFILE_CHANGED).putExtra( - Intents.INTENT_EXTRA_PROFILE_ACTIVE, - active - ) - ) + context.sendBroadcastSelf(intent) + + Log.d("Broadcasting $intent") } fun broadcastClashStarted(context: Context) { diff --git a/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt index b6d3618142..aae0245b57 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt @@ -1,10 +1,13 @@ package com.github.kr328.clash.service.util -import android.content.Context +import com.github.kr328.clash.core.Global import com.github.kr328.clash.service.Constants import java.io.File -val Context.profileDir: File - get() = this.filesDir.resolve(Constants.PROFILES_DIR) -val Context.clashDir: File - get() = this.filesDir.resolve(Constants.CLASH_DIR) \ No newline at end of file +fun resolveProfile(id: Long): File { + return Global.application.filesDir.resolve(Constants.PROFILES_DIR).resolve("$id.yaml") +} + +fun resolveBase(id: Long): File { + return Global.application.filesDir.resolve(Constants.CLASH_DIR).resolve(id.toString()) +} \ No newline at end of file diff --git a/service/src/main/res/xml/profile_provider.xml b/service/src/main/res/xml/profile_provider.xml new file mode 100644 index 0000000000..c88acec066 --- /dev/null +++ b/service/src/main/res/xml/profile_provider.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file From 88e0425736fd5687f025f92d7bab047d71c8e59e Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Fri, 14 Feb 2020 20:17:58 +0800 Subject: [PATCH 082/358] [WIP] UI refactor --- app/src/main/AndroidManifest.xml | 5 + .../com/github/kr328/clash/BaseActivity.kt | 4 +- .../kr328/clash/CreateProfileActivity.kt | 2 +- .../com/github/kr328/clash/MainActivity.kt | 98 ++++++-- .../com/github/kr328/clash/MainApplication.kt | 4 +- .../github/kr328/clash/ProfileEditActivity.kt | 29 ++- .../github/kr328/clash/ProfilesActivity.kt | 21 +- .../com/github/kr328/clash/ProxiesActivity.kt | 102 +++++++++ .../clash/adapter/AbstractProxyAdapter.kt | 11 + .../kr328/clash/adapter/GridProxyAdapter.kt | 216 ++++++++++++++++++ .../kr328/clash/adapter/ProfileAdapter.kt | 1 - .../github/kr328/clash/remote/Broadcasts.kt | 28 ++- .../github/kr328/clash/remote/ClashClient.kt | 4 +- .../com/github/kr328/clash/remote/Remote.kt | 8 + .../github/kr328/clash/utils/IntervalUtils.kt | 16 +- .../github/kr328/clash/utils/ProxySorter.kt | 91 ++++++++ .../main/res/color/proxies_chip_colors.xml | 5 + .../res/color/proxies_chip_text_colors.xml | 5 + app/src/main/res/drawable/ic_filter.xml | 9 + .../{ic_url_test.xml => ic_flash.xml} | 2 +- app/src/main/res/layout/activity_main.xml | 5 +- app/src/main/res/layout/activity_proxies.xml | 33 ++- ..._proxy_item.xml => adapter_grid_proxy.xml} | 16 +- ...eader.xml => adapter_grid_proxy_group.xml} | 7 +- app/src/main/res/values-night/colors.xml | 3 +- app/src/main/res/values/colors.xml | 6 +- app/src/main/res/values/strings.xml | 1 + core/src/main/golang/bridge/profiles.go | 12 - core/src/main/golang/bridge/tun.go | 4 +- core/src/main/golang/profile/download.go | 20 -- core/src/main/golang/profile/load.go | 3 + core/src/main/golang/tun/tun.go | 13 +- .../kr328/clash/core/model/ProxyGroup.kt | 20 +- .../kr328/clash/core/model/ProxyGroupList.kt | 27 +++ .../clash/core/serialization/MergedParcels.kt | 13 +- service/src/main/AndroidManifest.xml | 5 + .../github/kr328/clash/core/model/Packet.aidl | 2 +- .../kr328/clash/service/IClashManager.aidl | 2 +- .../kr328/clash/service/ClashManager.kt | 14 +- .../kr328/clash/service/ClashNotification.kt | 106 +++++---- .../kr328/clash/service/ClashService.kt | 16 +- .../github/kr328/clash/service/Constants.kt | 1 + .../clash/service/ProfileBackgroundService.kt | 109 ++++----- .../kr328/clash/service/ProfileService.kt | 21 +- .../clash/service/ServiceStatusProvider.kt | 60 +++++ .../github/kr328/clash/service/Settings.kt | 3 +- .../github/kr328/clash/service/TunService.kt | 8 +- .../clash/service/data/ClashProfileEntity.kt | 1 - .../service/net/DefaultNetworkChannel.kt | 4 +- .../clash/service/transact/ProfileRequest.kt | 2 +- .../clash/service/util/BroadcastUtils.kt | 2 - .../kr328/clash/service/util/ServiceUtils.kt | 13 ++ .../kr328/clash/service/util/Timeout.kt | 5 +- .../src/main/res/drawable/ic_notification.xml | 14 +- 54 files changed, 947 insertions(+), 285 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt create mode 100644 app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt create mode 100644 app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt create mode 100644 app/src/main/java/com/github/kr328/clash/utils/ProxySorter.kt create mode 100644 app/src/main/res/color/proxies_chip_colors.xml create mode 100644 app/src/main/res/color/proxies_chip_text_colors.xml create mode 100644 app/src/main/res/drawable/ic_filter.xml rename app/src/main/res/drawable/{ic_url_test.xml => ic_flash.xml} (84%) rename app/src/main/res/layout/{adapter_proxy_item.xml => adapter_grid_proxy.xml} (76%) rename app/src/main/res/layout/{adapter_proxy_header.xml => adapter_grid_proxy_group.xml} (80%) create mode 100644 core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/ServiceStatusProvider.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/util/ServiceUtils.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 1cf97fbf42..339579f5a5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -46,5 +46,10 @@ android:label="@string/profile" android:exported="false" android:configChanges="uiMode" /> + diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 272f6801a8..8fe474c54b 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -10,7 +10,6 @@ import android.view.LayoutInflater import android.view.View import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR import android.view.ViewGroup -import android.widget.FrameLayout import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar @@ -128,7 +127,8 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() protected fun makeSnackbarException(title: String, detail: String?) { Snackbar.make(rootView, title, Snackbar.LENGTH_LONG).setAction(R.string.detail) { - AlertDialog.Builder(this).setTitle(R.string.detail).setMessage(detail ?: "Unknown").show() + AlertDialog.Builder(this).setTitle(R.string.detail).setMessage(detail ?: "Unknown") + .show() }.show() } diff --git a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt index 5ae1d55e2c..66e9466e06 100644 --- a/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/CreateProfileActivity.kt @@ -67,7 +67,7 @@ class CreateProfileActivity : BaseActivity() { } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if ( requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK ) + if (requestCode == REQUEST_CODE && resultCode == Activity.RESULT_OK) return finish() super.onActivityResult(requestCode, resultCode, data) diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 7aabe436e4..10879c0e5d 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -1,9 +1,15 @@ package com.github.kr328.clash +import android.app.Activity import android.content.Intent +import android.net.VpnService import android.os.Bundle +import android.view.View import com.github.kr328.clash.core.utils.asBytesString import com.github.kr328.clash.remote.withClash +import com.github.kr328.clash.service.ClashService +import com.github.kr328.clash.service.util.intent +import com.github.kr328.clash.service.util.startForegroundServiceCompat import kotlinx.android.synthetic.main.activity_main.* import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -11,6 +17,10 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch class MainActivity : BaseActivity() { + companion object { + private const val REQUEST_CODE = 40000 + } + private var bandwidthJob: Job? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -18,26 +28,31 @@ class MainActivity : BaseActivity() { setContentView(R.layout.activity_main) + status.setOnClickListener { + if (clashRunning) { + stopService(ClashService::class.intent) + } else { + val vpnRequest = VpnService.prepare(this) + if (vpnRequest == null) + startForegroundServiceCompat(ClashService::class.intent) + else + startActivityForResult(vpnRequest, REQUEST_CODE) + } + } + + proxies.setOnClickListener { + startActivity(ProxiesActivity::class.intent) + } + profiles.setOnClickListener { - startActivity(Intent(this, ProfilesActivity::class.java)) + startActivity(ProfilesActivity::class.intent) } } override fun onStart() { super.onStart() - launch { - if (clashRunning) { - status.icon = getDrawable(R.drawable.ic_started) - status.title = getText(R.string.running) - status.summary = getString( - R.string.format_traffic_forwarded, - 0L.asBytesString() - ) - - startBandwidthPolling() - } - } + updateClashStatus() } override fun onStop() { @@ -46,12 +61,25 @@ class MainActivity : BaseActivity() { stopBandwidthPolling() } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) + startForegroundServiceCompat(ClashService::class.intent) + return + } + + super.onActivityResult(requestCode, resultCode, data) + } + override suspend fun onClashStarted() { - startBandwidthPolling() + updateClashStatus() } override suspend fun onClashStopped(reason: String?) { - stopBandwidthPolling() + updateClashStatus() + + if (reason != null) + makeSnackbarException(getString(R.string.clash_start_failure), reason) } private fun startBandwidthPolling() { @@ -60,15 +88,18 @@ class MainActivity : BaseActivity() { bandwidthJob = launch { withClash { - while (clashRunning && isActive) { - val bandwidth = queryBandwidth() - status.summary = getString( - R.string.format_traffic_forwarded, - bandwidth.asBytesString() - ) - delay(1000) + try { + while (clashRunning && isActive) { + val bandwidth = queryBandwidth() + status.summary = getString( + R.string.format_traffic_forwarded, + bandwidth.asBytesString() + ) + delay(1000) + } + } finally { + bandwidthJob = null } - bandwidthJob = null } } } @@ -76,4 +107,25 @@ class MainActivity : BaseActivity() { private fun stopBandwidthPolling() { bandwidthJob?.cancel() } + + private fun updateClashStatus() { + if (clashRunning) { + startBandwidthPolling() + + status.setCardBackgroundColor(getColor(R.color.primaryCardColorStarted)) + status.icon = getDrawable(R.drawable.ic_started) + status.title = getText(R.string.running) + + proxies.visibility = View.VISIBLE + } else { + stopBandwidthPolling() + + status.setCardBackgroundColor(getColor(R.color.primaryCardColorStopped)) + status.icon = getDrawable(R.drawable.ic_stopped) + status.title = getText(R.string.stopped) + status.summary = getText(R.string.tap_to_start) + + proxies.visibility = View.GONE + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 853a8de106..dd869e040f 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -10,7 +10,7 @@ import com.microsoft.appcenter.analytics.Analytics import com.microsoft.appcenter.crashes.Crashes @Suppress("unused") -class MainApplication: Application() { +class MainApplication : Application() { override fun attachBaseContext(base: Context?) { super.attachBaseContext(base) @@ -21,7 +21,7 @@ class MainApplication: Application() { super.onCreate() // Initialize AppCenter - if ( BuildConfig.APP_CENTER_KEY.isNotEmpty() && !BuildConfig.DEBUG ) { + if (BuildConfig.APP_CENTER_KEY.isNotEmpty() && !BuildConfig.DEBUG) { AppCenter.start( this, BuildConfig.APP_CENTER_KEY, diff --git a/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt index 5c316ca4a1..277fd26f8e 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfileEditActivity.kt @@ -16,8 +16,6 @@ import com.github.kr328.clash.service.transact.ProfileRequest import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_profile_edit.* import kotlinx.coroutines.launch -import java.lang.Exception -import java.lang.IllegalArgumentException class ProfileEditActivity : BaseActivity() { companion object { @@ -36,11 +34,10 @@ class ProfileEditActivity : BaseActivity() { set(value) { field = value - if ( value ) { + if (value) { saving.visibility = View.VISIBLE save.visibility = View.INVISIBLE - } - else { + } else { saving.visibility = View.INVISIBLE save.visibility = View.VISIBLE } @@ -126,7 +123,7 @@ class ProfileEditActivity : BaseActivity() { } } - if ( intent.getStringExtra("type") == Constants.URL_PROVIDER_TYPE_FILE ) + if (intent.getStringExtra("type") == Constants.URL_PROVIDER_TYPE_FILE) isHidden = true } } @@ -140,13 +137,14 @@ class ProfileEditActivity : BaseActivity() { val interval = requireElement(KEY_AUTO_UPDATE).content.toString() .toLongOrNull()?.minus(60) ?: 0 - if ( name.isBlank() ) { + if (name.isBlank()) { Snackbar.make(rootView, R.string.empty_name, Snackbar.LENGTH_LONG).show() return@setOnClickListener } - if ( url == null || url == Uri.EMPTY || - (url.scheme != "http" && url.scheme != "https" && url.scheme != "content" )) { + if (url == null || url == Uri.EMPTY || + (url.scheme != "http" && url.scheme != "https" && url.scheme != "content") + ) { Snackbar.make(rootView, R.string.invalid_url, Snackbar.LENGTH_LONG).show() return@setOnClickListener } @@ -157,12 +155,12 @@ class ProfileEditActivity : BaseActivity() { } } - when (intent.extras?.getLong("id", -1L)) { - -1L -> { + when (intent.extras?.getLong("id", Long.MIN_VALUE)) { + Long.MIN_VALUE -> { openUrlProvider() setTitle(R.string.new_profile) } - 0L -> { + -1L -> { setTitle(R.string.new_profile) } else -> { @@ -232,8 +230,7 @@ class ProfileEditActivity : BaseActivity() { ) else -> return false } - } - catch (e: Exception) { + } catch (e: Exception) { makeSnackbarException(getString(R.string.start_url_provider_failure), e.message) } @@ -243,7 +240,7 @@ class ProfileEditActivity : BaseActivity() { private fun sendProfileRequest(name: String, url: Uri, interval: Long) { launch { val source = intent?.getParcelableExtra("intent")?.toUri(0)?.run(Uri::parse) - val type = when( intent?.getStringExtra("type") ) { + val type = when (intent?.getStringExtra("type")) { Constants.URL_PROVIDER_TYPE_FILE -> ClashProfileEntity.TYPE_FILE Constants.URL_PROVIDER_TYPE_URL -> @@ -255,7 +252,7 @@ class ProfileEditActivity : BaseActivity() { val request = ProfileRequest() .action(ProfileRequest.Action.UPDATE_OR_CREATE) - .withId(intent.getLongExtra("id", 0)) + .withId(intent.getLongExtra("id", -1L)) .withName(name) .withURL(url) .withUpdateInterval(interval) diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index fff1a4cb66..0519e17219 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -7,11 +7,14 @@ import androidx.recyclerview.widget.LinearLayoutManager import com.github.kr328.clash.adapter.ProfileAdapter import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.remote.withProfile +import com.github.kr328.clash.service.Intents +import com.github.kr328.clash.service.ProfileBackgroundService import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.transact.ProfileRequest +import com.github.kr328.clash.service.util.componentName import com.github.kr328.clash.service.util.intent +import com.github.kr328.clash.service.util.startForegroundServiceCompat import com.github.kr328.clash.weight.ProfilesMenu -import com.google.android.material.bottomsheet.BottomSheetDialog import kotlinx.android.synthetic.main.activity_profiles.* import kotlinx.coroutines.Job import kotlinx.coroutines.delay @@ -146,7 +149,7 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C val interval = entity.updateInterval.toString() val editor = ProfileEditActivity::class.intent - .putExtra("id", if (duplicate) 0 else entity.id) + .putExtra("id", if (duplicate) -1L else entity.id) .putExtra("type", type) .putExtra("intent", intent) .putExtra("name", name) @@ -171,12 +174,24 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C ) } + private fun startUpdate(entity: ClashProfileEntity) { + val request = ProfileRequest() + .action(ProfileRequest.Action.UPDATE_OR_CREATE) + .withId(entity.id) + + val intent = Intent(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) + .setComponent(ProfileBackgroundService::class.componentName) + .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, request) + + startForegroundServiceCompat(intent) + } + override fun onOpenEditor(entity: ClashProfileEntity) { openEditor(entity) } override fun onUpdate(entity: ClashProfileEntity) { - + startUpdate(entity) } override fun onOpenProperties(entity: ClashProfileEntity) { diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt new file mode 100644 index 0000000000..b4c1e6b5ed --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -0,0 +1,102 @@ +package com.github.kr328.clash + +import android.os.Bundle +import androidx.core.view.isEmpty +import com.github.kr328.clash.adapter.AbstractProxyAdapter +import com.github.kr328.clash.adapter.GridProxyAdapter +import com.github.kr328.clash.core.model.ProxyGroup +import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.remote.withClash +import com.github.kr328.clash.utils.ProxySorter +import com.google.android.material.chip.Chip +import kotlinx.android.synthetic.main.activity_proxies.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class ProxiesActivity : BaseActivity() { + private val activity: ProxiesActivity + get() = this + private var maskedGroup: MutableSet = mutableSetOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_proxies) + setSupportActionBar(toolbar) + + GridProxyAdapter(this).apply { + mainList.layoutManager = layoutManager + mainList.adapter = this + + onSelectProxyListener = { group, name -> + withClash { + setSelectProxy(group, name) + } + } + } + } + + override fun onStart() { + super.onStart() + + refreshList() + } + + override suspend fun onClashStarted() { + finish() + } + + private fun refreshList(scrollTo: String? = null) { + launch { + val proxies = withClash { + queryAllProxyGroups() + } + + val sorter = ProxySorter(ProxySorter.Order.DEFAULT, ProxySorter.Order.DEFAULT) + + val filtered = withContext(Dispatchers.Default) { + sorter.sort(proxies.toList()) + } + + if (chipGroup.isEmpty()) { + filtered.map(ProxyGroup::name).forEach { + val chip = Chip(activity).apply { + text = it + + chipBackgroundColor = getColorStateList(R.color.proxies_chip_colors) + rippleColor = getColorStateList(R.color.proxies_chip_colors) + setTextColor(getColorStateList(R.color.proxies_chip_text_colors)) + checkedIcon = null + + isCheckable = true + isClickable = true + isFocusable = true + isChecked = !maskedGroup.contains(it) + + setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + maskedGroup.remove(it) + } else { + maskedGroup.add(it) + } + + refreshList(it) + } + } + + chipGroup.addView(chip) + } + } + + (mainList.adapter!! as AbstractProxyAdapter).apply { + root = filtered.filter { !maskedGroup.contains(it.name) } + + applyChange() + + if (scrollTo != null) { + scrollToGroup(scrollTo) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt new file mode 100644 index 0000000000..35b9fa7c53 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt @@ -0,0 +1,11 @@ +package com.github.kr328.clash.adapter + +import com.github.kr328.clash.core.model.ProxyGroup + +interface AbstractProxyAdapter { + var root: List + var onSelectProxyListener: suspend (String, String) -> Unit + + suspend fun applyChange() + suspend fun scrollToGroup(name: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt new file mode 100644 index 0000000000..3615a6439e --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt @@ -0,0 +1,216 @@ +package com.github.kr328.clash.adapter + +import android.graphics.Color +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.ProxiesActivity +import com.github.kr328.clash.R +import com.github.kr328.clash.core.model.Proxy +import com.github.kr328.clash.core.model.ProxyGroup +import com.github.kr328.clash.core.utils.Log +import com.google.android.material.card.MaterialCardView +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) : + RecyclerView.Adapter(), AbstractProxyAdapter { + private interface RenderInfo { + val name: String + } + + private data class ProxyGroupInfo(override val name: String) : RenderInfo + private data class ProxyInfo( + override val name: String, + val type: Proxy.Type, + val group: String, + val selectable: Boolean, + val delay: Short, + val active: Boolean + ) : RenderInfo + + private var renderList = emptyList() + @ColorInt + private val colorSurface: Int + @ColorInt + private val colorOnSurface: Int + + init { + val typedValue = TypedValue() + + context.theme.resolveAttribute(R.attr.colorSurface, typedValue, true) + colorSurface = typedValue.data + + context.theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true) + colorOnSurface = typedValue.data + } + + val layoutManager = GridLayoutManager(context, spanCount).apply { + spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { + override fun getSpanSize(position: Int): Int { + return when (renderList[position]) { + is ProxyGroupInfo -> spanCount + is ProxyInfo -> 1 + else -> throw IllegalArgumentException() + } + } + } + } + + override var root = listOf() + override var onSelectProxyListener: suspend (String, String) -> Unit = { _, _ -> } + + private class ProxyGroupHeader(view: View) : RecyclerView.ViewHolder(view) { + val name: TextView = view.findViewById(R.id.name) + val urlTest: View = view.findViewById(R.id.urlTest) + } + + private class ProxyItem(view: View) : RecyclerView.ViewHolder(view) { + val root: MaterialCardView = view.findViewById(R.id.root) + val name: TextView = view.findViewById(R.id.name) + val type: TextView = view.findViewById(R.id.type) + val delay: TextView = view.findViewById(R.id.delay) + } + + override suspend fun applyChange() = withContext(Dispatchers.Default) { + val newRenderList = root + .flatMap { + listOf(ProxyGroupInfo(it.name)) + it.proxies.map { p -> + ProxyInfo( + p.name, + p.type, + it.name, + it.type == Proxy.Type.SELECT, + p.delay.toShort(), + it.current == p.name + ) + } + } + + val oldRenderList = renderList + + val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldRenderList[oldItemPosition]::class == newRenderList[newItemPosition]::class && + oldRenderList[oldItemPosition].name == newRenderList[newItemPosition].name + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldRenderList[oldItemPosition] == newRenderList[newItemPosition] + + override fun getOldListSize(): Int = oldRenderList.size + override fun getNewListSize(): Int = newRenderList.size + }) + + withContext(Dispatchers.Main) { + renderList = newRenderList + result.dispatchUpdatesTo(this@GridProxyAdapter) + } + } + + override suspend fun scrollToGroup(name: String) { + val position = withContext(Dispatchers.Default) { + renderList.mapIndexed { index, p -> + if ( p is ProxyGroupInfo && p.name == name ) + index + else + -1 + }.singleOrNull { it >= 0 } + } ?: return + + layoutManager.scrollToPositionWithOffset(position, 0) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val layoutInflater = LayoutInflater.from(context) + + return when (viewType) { + 1 -> ProxyGroupHeader( + layoutInflater + .inflate(R.layout.adapter_grid_proxy_group, parent, false) + ) + 2 -> ProxyItem( + layoutInflater + .inflate(R.layout.adapter_grid_proxy, parent, false) + ) + else -> throw IllegalArgumentException() + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + when (holder) { + is ProxyGroupHeader -> { + val current = renderList[position] as ProxyGroupInfo + + holder.name.text = current.name + holder.urlTest.setOnClickListener { + + } + } + is ProxyItem -> { + val current = renderList[position] as ProxyInfo + + holder.name.text = current.name + holder.type.text = current.type.toString() + + + if (current.delay > 0) + holder.delay.text = current.delay.toString() + else + holder.delay.text = "N/A" + + if (current.active) { + holder.name.setTextColor(Color.WHITE) + holder.type.setTextColor(Color.WHITE) + holder.delay.setTextColor(Color.WHITE) + holder.root.setCardBackgroundColor(context.getColor(R.color.primaryCardColorStarted)) + } else { + holder.name.setTextColor(colorOnSurface) + holder.type.setTextColor(colorOnSurface) + holder.delay.setTextColor(colorOnSurface) + holder.root.setCardBackgroundColor(colorSurface) + } + + if (current.selectable) { + holder.root.setOnClickListener { + root = root.map { + if (it.name == current.group) { + it.copy(current = current.name) + } else { + it + } + } + + context.launch { + applyChange() + + onSelectProxyListener(current.group, current.name) + } + } + holder.root.isClickable = true + holder.root.isFocusable = true + } else { + holder.root.setOnClickListener(null) + holder.root.isClickable = false + holder.root.isFocusable = false + } + } + } + } + + override fun getItemCount(): Int = renderList.size + override fun getItemViewType(position: Int): Int { + return when (renderList[position]) { + is ProxyGroupInfo -> 1 + is ProxyInfo -> 2 + else -> throw IllegalArgumentException() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt index e5c9018466..4b3ae46201 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProfileAdapter.kt @@ -13,7 +13,6 @@ import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.utils.IntervalUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -import java.util.* class ProfileAdapter(private val context: Context, private val callback: Callback) : RecyclerView.Adapter() { diff --git a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt index 5340e90b27..400c492340 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt @@ -8,10 +8,10 @@ import android.content.IntentFilter import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.ProcessLifecycleOwner -import com.github.kr328.clash.service.ClashService +import com.github.kr328.clash.service.Constants import com.github.kr328.clash.service.Intents +import com.github.kr328.clash.service.ServiceStatusProvider import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.service.util.componentName object Broadcasts { interface Receiver { @@ -25,18 +25,24 @@ object Broadcasts { private val receivers = mutableListOf() private val broadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { - if ( intent?.`package` != context?.packageName ) + if (intent?.`package` != context?.packageName) return when (intent?.action) { - Intents.INTENT_ACTION_CLASH_STARTED -> + Intents.INTENT_ACTION_CLASH_STARTED -> { + clashRunning = true + receivers.forEach { it.onStarted() } - Intents.INTENT_ACTION_CLASH_STOPPED -> + } + Intents.INTENT_ACTION_CLASH_STOPPED -> { + clashRunning = false + receivers.forEach { it.onStopped(intent.getStringExtra(Intents.INTENT_EXTRA_CLASH_STOP_REASON)) } + } Intents.INTENT_ACTION_PROFILE_CHANGED -> receivers.forEach { it.onProfileChanged(intent.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE)) @@ -62,10 +68,14 @@ object Broadcasts { addAction(Intents.INTENT_ACTION_CLASH_STARTED) }) - clashRunning = broadcastReceiver.peekService( - application, - Intent().setComponent(ClashService::class.componentName) - ) != null + val pong = application.contentResolver.call( + "${application.packageName}${Constants.STATUS_PROVIDER_SUFFIX}", + ServiceStatusProvider.METHOD_PING_CLASH_SERVICE, + null, + null + ) + + clashRunning = pong != null } override fun onStop(owner: LifecycleOwner) { diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt index 131f73e5f3..ceb1d6be3f 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -28,8 +28,8 @@ class ClashClient(private val service: IClashManager) { } }.await() - suspend fun queryAllProxyGroups(): Array = withContext(Dispatchers.IO) { - service.queryAllProxies() + suspend fun queryAllProxyGroups(): List = withContext(Dispatchers.IO) { + service.queryAllProxies().list } suspend fun queryGeneral(): General = withContext(Dispatchers.IO) { diff --git a/app/src/main/java/com/github/kr328/clash/remote/Remote.kt b/app/src/main/java/com/github/kr328/clash/remote/Remote.kt index 5658bb61aa..61419b3ecc 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Remote.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Remote.kt @@ -41,6 +41,10 @@ object Remote { if (service != null) instance = ClashClient(IClashManager.Stub.asInterface(service)) + service?.linkToDeath({ + onServiceDisconnected(null) + }, 0) + sender = GlobalScope.launch { while (isActive) { val client = instance ?: return@launch @@ -64,6 +68,10 @@ object Remote { if (service != null) instance = ProfileClient(IProfileService.Stub.asInterface(service)) + service?.linkToDeath({ + onServiceDisconnected(null) + }, 0) + sender = GlobalScope.launch { while (isActive) { val client = instance ?: return@launch diff --git a/app/src/main/java/com/github/kr328/clash/utils/IntervalUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/IntervalUtils.kt index a05b12e1ae..ac2d1fd4ee 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/IntervalUtils.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/IntervalUtils.kt @@ -6,18 +6,18 @@ import com.github.kr328.clash.core.Global object IntervalUtils { private const val MILLIS_SECOND = 1000L private const val MILLIS_MINUTE = MILLIS_SECOND * 60 - private const val MILLIS_HOUR = MILLIS_MINUTE * 60 - private const val MILLIS_DAY = MILLIS_HOUR * 24 - private const val MILLIS_MONTH = MILLIS_DAY * 30 - private const val MILLIS_YEAR = MILLIS_MONTH * 12 + private const val MILLIS_HOUR = MILLIS_MINUTE * 60 + private const val MILLIS_DAY = MILLIS_HOUR * 24 + private const val MILLIS_MONTH = MILLIS_DAY * 30 + private const val MILLIS_YEAR = MILLIS_MONTH * 12 fun intervalString(interval: Long): String { val context = Global.application - val year = interval / MILLIS_YEAR - val month = interval / MILLIS_MONTH - val day = interval / MILLIS_DAY - val hour = interval / MILLIS_HOUR + val year = interval / MILLIS_YEAR + val month = interval / MILLIS_MONTH + val day = interval / MILLIS_DAY + val hour = interval / MILLIS_HOUR val minute = interval / MILLIS_MINUTE System.currentTimeMillis() diff --git a/app/src/main/java/com/github/kr328/clash/utils/ProxySorter.kt b/app/src/main/java/com/github/kr328/clash/utils/ProxySorter.kt new file mode 100644 index 0000000000..f6ba14fe8b --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/utils/ProxySorter.kt @@ -0,0 +1,91 @@ +package com.github.kr328.clash.utils + +import com.github.kr328.clash.core.model.Proxy +import com.github.kr328.clash.core.model.ProxyGroup + +class ProxySorter(val groupOrder: Order, val proxyOrder: Order) { + enum class Order { + DEFAULT, DELAY_INCREASE, DELAY_DECREASE, NAME_INCREASE, NAME_DECREASE + } + + fun sort(proxyGroup: List): List { + val global = proxyGroup.singleOrNull { + it.name == "GLOBAL" + } + + val sortedGroup = when (groupOrder) { + Order.DEFAULT -> groupSortWithDefault(global, proxyGroup) + Order.DELAY_INCREASE -> groupSortWithDelay(true, proxyGroup) + Order.DELAY_DECREASE -> groupSortWithDelay(false, proxyGroup) + Order.NAME_INCREASE -> groupSortWithName(true, proxyGroup) + Order.NAME_DECREASE -> groupSortWithName(false, proxyGroup) + } + + return sortedGroup.map { + val sortedProxy = when (proxyOrder) { + Order.DEFAULT -> it.proxies + Order.DELAY_INCREASE -> proxySortWithDelay(true, it.proxies) + Order.DELAY_DECREASE -> proxySortWithDelay(false, it.proxies) + Order.NAME_INCREASE -> proxySortWithName(true, it.proxies) + Order.NAME_DECREASE -> proxySortWithName(false, it.proxies) + } + + it.copy(proxies = sortedProxy) + } + } + + private fun groupSortWithDefault( + global: ProxyGroup?, + proxyGroup: List + ): List { + if (global == null) return proxyGroup + + val orderMap = global.proxies.mapIndexed { index, proxy -> + proxy.name to index + }.toMap() + + return proxyGroup.sortedBy { + orderMap[it.name] ?: Int.MAX_VALUE + } + } + + private fun groupSortWithName( + increase: Boolean, + proxyGroup: List + ): List { + return if (increase) + proxyGroup.sortedBy { it.name } + else + proxyGroup.sortedByDescending { it.name } + } + + private fun groupSortWithDelay( + increase: Boolean, + proxyGroup: List + ): List { + return if (increase) + proxyGroup.sortedBy { it.delay } + else + proxyGroup.sortedByDescending { it.delay } + } + + private fun proxySortWithName( + increase: Boolean, + proxies: List + ): List { + return if (increase) + proxies.sortedBy { it.name } + else + proxies.sortedByDescending { it.name } + } + + private fun proxySortWithDelay( + increase: Boolean, + proxies: List + ): List { + return if (increase) + proxies.sortedBy { it.delay } + else + proxies.sortedByDescending { it.delay } + } +} \ No newline at end of file diff --git a/app/src/main/res/color/proxies_chip_colors.xml b/app/src/main/res/color/proxies_chip_colors.xml new file mode 100644 index 0000000000..4f053273a6 --- /dev/null +++ b/app/src/main/res/color/proxies_chip_colors.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/proxies_chip_text_colors.xml b/app/src/main/res/color/proxies_chip_text_colors.xml new file mode 100644 index 0000000000..5e2e624e74 --- /dev/null +++ b/app/src/main/res/color/proxies_chip_text_colors.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml new file mode 100644 index 0000000000..72a680cfd9 --- /dev/null +++ b/app/src/main/res/drawable/ic_filter.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_url_test.xml b/app/src/main/res/drawable/ic_flash.xml similarity index 84% rename from app/src/main/res/drawable/ic_url_test.xml rename to app/src/main/res/drawable/ic_flash.xml index 4efdc7f724..8a5fa36650 100644 --- a/app/src/main/res/drawable/ic_url_test.xml +++ b/app/src/main/res/drawable/ic_flash.xml @@ -4,6 +4,6 @@ android:viewportWidth="24.0" android:viewportHeight="24.0"> diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 793fdc7b73..28241a4db6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,7 +4,8 @@ xmlns:app="http://schemas.android.com/apk/res-auto" android:scrollbars="none" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + android:animateLayoutChanges="true"> diff --git a/app/src/main/res/layout/activity_proxies.xml b/app/src/main/res/layout/activity_proxies.xml index 789158fb7e..70d6b385bb 100644 --- a/app/src/main/res/layout/activity_proxies.xml +++ b/app/src/main/res/layout/activity_proxies.xml @@ -11,21 +11,32 @@ android:layout_height="wrap_content"> - + android:layout_height="wrap_content" + android:scrollbars="none" + android:overScrollMode="never"> - - + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_proxy_item.xml b/app/src/main/res/layout/adapter_grid_proxy.xml similarity index 76% rename from app/src/main/res/layout/adapter_proxy_item.xml rename to app/src/main/res/layout/adapter_grid_proxy.xml index 9877acaf33..4323479b5f 100644 --- a/app/src/main/res/layout/adapter_proxy_item.xml +++ b/app/src/main/res/layout/adapter_grid_proxy.xml @@ -5,7 +5,7 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> - + android:background="?attr/selectableItemBackgroundBorderless" + android:foreground="@drawable/ic_flash"/> diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index bb57d3b3b8..9f8ad0b812 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -1,8 +1,9 @@ #FFFFFF - #121212 + #121212 #000000 #000000 #121212 + #242424 \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 8d82dff7af..e53bc91e7c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,11 +1,13 @@ - #FF888888 + #1E4376 #FAFAFA #FAFAFA #FAFAFA + #EDEDED - #1E4376 + #FF888888 + #1E4376 #121212 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 519b0403d2..dbcda1822f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,6 +73,7 @@ New Profile Edit Profile + Clash Start Failure Start URL Provider Failure diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go index a0c339d931..2df452af99 100644 --- a/core/src/main/golang/bridge/profiles.go +++ b/core/src/main/golang/bridge/profiles.go @@ -22,18 +22,6 @@ func ReadProfileAndCheck(fd int, output, baseDir string, callback DoneCallback) }() } -func SaveProfileAndCheck(data []byte, output, baseDir string, callback DoneCallback) { - go func() { - call(profile.SaveAndCheck(data, output, baseDir), callback) - }() -} - -func MoveProfileAndCheck(source, target, baseDir string, callback DoneCallback) { - go func() { - call(profile.MoveAndCheck(source, target, baseDir), callback) - }() -} - func call(err error, callback DoneCallback) { if err != nil { callback.DoneWithError(err) diff --git a/core/src/main/golang/bridge/tun.go b/core/src/main/golang/bridge/tun.go index ad566e87f2..770474ecb1 100644 --- a/core/src/main/golang/bridge/tun.go +++ b/core/src/main/golang/bridge/tun.go @@ -17,11 +17,11 @@ func StartTunDevice(fd, mtu int, dns string, cb TunCallback) error { } func StopTunDevice() { + tun.StopTunDevice() + if c := callback; c != nil { c.OnStop() } callback = nil - - tun.StopTunDevice() } diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go index 33280dab39..b62c5a3d66 100644 --- a/core/src/main/golang/profile/download.go +++ b/core/src/main/golang/profile/download.go @@ -79,26 +79,6 @@ func SaveAndCheck(data []byte, output, baseDir string) error { return ioutil.WriteFile(output, data, defaultFileMode) } -func MoveAndCheck(source, target, baseDir string) error { - buf, err := ioutil.ReadFile(source) - if err != nil { - return err - } - - _, err = parseConfig(buf, baseDir) - if err != nil { - return err - } - - if err := ioutil.WriteFile(target, buf, defaultFileMode); err != nil { - return err - } - - os.Remove(source) - - return nil -} - func parseConfig(data []byte, baseDir string) (*config.Config, error) { raw, err := config.UnmarshalRawConfig(data) if err != nil { diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index 4da826a23d..8c3ac58f2f 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -8,6 +8,7 @@ import ( "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/hub/executor" + "github.com/Dreamacro/clash/log" "github.com/kr328/cfa/tun" ) @@ -98,5 +99,7 @@ func LoadFromFile(path, baseDir string) error { tun.ResetDnsRedirect() + log.Infoln("Profile " + path + " loaded") + return nil } diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go index b36baa3af0..9f85a21594 100644 --- a/core/src/main/golang/tun/tun.go +++ b/core/src/main/golang/tun/tun.go @@ -2,6 +2,7 @@ package tun import ( "strconv" + "sync" "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/log" @@ -10,8 +11,12 @@ import ( var tunInstance *tun.TunAdapter var dnsAddress string +var mutex sync.Mutex func StartTunDevice(fd, mtu int, dns string) error { + mutex.Lock() + defer mutex.Unlock() + if tunInstance != nil { return nil } @@ -22,7 +27,7 @@ func StartTunDevice(fd, mtu int, dns string) error { } tunInstance = &t - dnsAddress = dns + ":53" + dnsAddress = dns ResetDnsRedirect() @@ -32,12 +37,18 @@ func StartTunDevice(fd, mtu int, dns string) error { } func StopTunDevice() { + mutex.Lock() + defer mutex.Unlock() + t := tunInstance if t == nil { return } (*t).Close() + tunInstance = nil + + log.Infoln("Android tun stopped") } func ResetDnsRedirect() { diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt index d7a64d0201..da51f1eea8 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt @@ -12,22 +12,4 @@ data class ProxyGroup( val delay: Long, val current: String, val proxies: List -) : Parcelable { - override fun writeToParcel(parcel: Parcel, flags: Int) { - MergedParcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ProxyGroup { - return MergedParcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt new file mode 100644 index 0000000000..3f09873270 --- /dev/null +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt @@ -0,0 +1,27 @@ +package com.github.kr328.clash.core.model + +import android.os.Parcel +import android.os.Parcelable +import com.github.kr328.clash.core.serialization.MergedParcels +import kotlinx.serialization.Serializable + +@Serializable +data class ProxyGroupList(val list: List): Parcelable { + override fun writeToParcel(parcel: Parcel, flags: Int) { + MergedParcels.dump(serializer(), this, parcel) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ProxyGroupList { + return MergedParcels.load(serializer(), parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt index e265a78105..a60977499e 100644 --- a/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt +++ b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt @@ -1,6 +1,7 @@ package com.github.kr328.clash.core.serialization import android.os.Parcel +import com.github.kr328.clash.core.utils.Log import kotlinx.serialization.* import kotlinx.serialization.modules.EmptyModule import kotlinx.serialization.modules.SerialModule @@ -17,6 +18,8 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) { parcel.writeStringList(encoder.getStringList()) parcel.appendFrom(data, 0, data.dataSize()) + + Log.i("Send ${parcel.dataSize()} bytes") } finally { data.recycle() } @@ -24,6 +27,7 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) { fun load(deserializer: DeserializationStrategy, parcel: Parcel): T { val strings = mutableListOf().apply { parcel.readStringList(this) } + return deserializer.deserialize(ParcelsDecoder(strings, parcel)) } @@ -142,6 +146,7 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) { val index = strings.computeIfAbsent(value) { stringIndex++ } + parcel.writeInt(index) } } @@ -254,8 +259,12 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) { parcel.readInt().toShort() override fun decodeUnit() {} - override fun decodeString() = - strings[parcel.readInt()] + override fun decodeString(): String { + val index = parcel.readInt() + + return strings[index] + } + } } diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml index 8d8c318e11..65c474bc4c 100644 --- a/service/src/main/AndroidManifest.xml +++ b/service/src/main/AndroidManifest.xml @@ -50,5 +50,10 @@ android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/profile_provider" /> + diff --git a/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl b/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl index 5477ae380f..2cdcbd7e7a 100644 --- a/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl @@ -1,4 +1,4 @@ package com.github.kr328.clash.core.model; -parcelable ProxyGroup; +parcelable ProxyGroupList; parcelable General; \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index 00bc3330d3..09dff42e63 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -10,7 +10,7 @@ interface IClashManager { void startHealthCheck(String group, IStreamCallback callback); // Query - ProxyGroup[] queryAllProxies(); + ProxyGroupList queryAllProxies(); General queryGeneral(); long queryBandwidth(); diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index b4ce19b501..ce55c46f08 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -5,16 +5,16 @@ 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.ProxyGroup -import com.github.kr328.clash.service.data.ClashDatabase -import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.core.model.ProxyGroupList +import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.ipc.IStreamCallback import com.github.kr328.clash.service.ipc.ParcelableContainer -class ClashManager(private val context: Context) : IClashManager.Stub() { +class ClashManager(context: Context) : IClashManager.Stub() { private val settings = context.getSharedPreferences("service", Context.MODE_PRIVATE) - override fun queryAllProxies(): Array { - return Clash.queryProxyGroups().toTypedArray() + override fun queryAllProxies(): ProxyGroupList { + return ProxyGroupList(Clash.queryProxyGroups()) } override fun queryGeneral(): General { @@ -65,7 +65,7 @@ class ClashManager(private val context: Context) : IClashManager.Stub() { } } - override fun getSetting(key: String?): String { - return settings.getString(key, "")!! + override fun getSetting(key: String?): String? { + return settings.getString(key, null) } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 8eb2d5c512..0de773aa75 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -1,6 +1,9 @@ package com.github.kr328.clash.service -import android.app.* +import android.app.Notification +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent import android.content.BroadcastReceiver import android.content.Context import android.content.Intent @@ -12,14 +15,11 @@ import androidx.core.app.NotificationManagerCompat import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.utils.asBytesString import com.github.kr328.clash.core.utils.asSpeedString -import com.github.kr328.clash.service.util.ticker import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.selects.select -import kotlin.coroutines.CoroutineContext -import kotlin.coroutines.EmptyCoroutineContext -class ClashNotification(private val context: Service) : CoroutineScope { +class ClashNotification(private val context: ClashService, enableRefresh: Boolean) : + CoroutineScope by context { companion object { private const val CLASH_STATUS_NOTIFICATION_CHANNEL = "clash_status_channel" private const val CLASH_STATUS_NOTIFICATION_ID = 413 @@ -45,6 +45,7 @@ class ClashNotification(private val context: Service) : CoroutineScope { ) ) private val screenChannel: Channel = Channel(Channel.CONFLATED) + private val tickerChannel: Channel = Channel() private var profile = "None" private val observer = object : BroadcastReceiver() { @@ -59,65 +60,71 @@ class ClashNotification(private val context: Service) : CoroutineScope { } init { + createNotificationChannel() + runBlocking { update() } - launch { - withContext(Dispatchers.IO) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - NotificationManagerCompat.from(context) - .createNotificationChannel( - NotificationChannel( - CLASH_STATUS_NOTIFICATION_CHANNEL, - context.getString(R.string.clash_service_status_channel), - NotificationManager.IMPORTANCE_LOW - ) - ) - } - } - - while (isActive) { + if (enableRefresh) { + launch { val powerManager = requireNotNull(context.getSystemService(PowerManager::class.java)) - val tickerChannel = Channel(Channel.CONFLATED) - var tickerJob = if (powerManager.isInteractive) - ticker(1000, tickerChannel) - else - EmptyCoroutineContext - - select { - screenChannel.onReceive { - tickerJob.cancel() - - if (it) { - tickerJob = ticker(1000, tickerChannel) + + screenChannel.send(powerManager.isInteractive) + + var tickerJob: Job? = null + + launch { + while (isActive) { + tickerJob = if (screenChannel.receive()) { + tickerJob?.cancel() + startTicker() + } else { + tickerJob?.cancel() + null } } - tickerChannel.onReceive { + } + + launch { + while (isActive) { + tickerChannel.receive() + update() } } + + context.registerReceiver(observer, IntentFilter().apply { + addAction(Intent.ACTION_SCREEN_ON) + addAction(Intent.ACTION_SCREEN_OFF) + }) } } + } - context.registerReceiver(observer, IntentFilter().apply { - addAction(Intent.ACTION_SCREEN_ON) - addAction(Intent.ACTION_SCREEN_OFF) - }) + private fun startTicker(): Job { + return launch { + while (isActive) { + tickerChannel.send(Unit) + + delay(1000) + } + } } fun destroy() { - cancel() - context.unregisterReceiver(observer) context.stopForeground(true) } fun setProfile(profile: String) { - this.profile = profile - } + launch { + this@ClashNotification.profile = profile + update() + } + } private suspend fun update() { val notification = withContext(Dispatchers.Default) { @@ -144,12 +151,21 @@ class ClashNotification(private val context: Service) : CoroutineScope { context.getString( R.string.clash_notification_content, bandwidth.upload.asBytesString(), - traffic.download.asBytesString() + bandwidth.download.asBytesString() ) ) .build() } - override val coroutineContext: CoroutineContext - get() = SupervisorJob() + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + return + NotificationManagerCompat.from(context).createNotificationChannel( + NotificationChannel( + CLASH_STATUS_NOTIFICATION_CHANNEL, + context.getText(R.string.clash_service_status_channel), + NotificationManager.IMPORTANCE_LOW + ) + ) + } } \ No newline at end of file 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 a9eb065398..df6b93c474 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 @@ -16,14 +16,19 @@ class ClashService : BaseService() { companion object { const val INTENT_EXTRA_START_TUN = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.start.tun" + + var isServiceRunning = false } private val service = this - private val notification by lazy { ClashNotification(service) } + private lateinit var notification: ClashNotification private var stopReason: String? = null private val reloadChannel = Channel(Channel.CONFLATED) + private val settings: Settings by lazy { Settings(ClashManager(this)) } private val profileObserver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.`package` != packageName) + return reloadChannel.offer(Unit) } } @@ -31,6 +36,8 @@ class ClashService : BaseService() { override fun onCreate() { super.onCreate() + notification = ClashNotification(service, settings.get(Settings.NOTIFICATION_REFRESH)) + launch { while (isActive) { reloadChannel.receive() @@ -43,6 +50,11 @@ class ClashService : BaseService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { super.onStartCommand(intent, flags, startId) + if (isServiceRunning) + return START_NOT_STICKY + + isServiceRunning = true + Clash.start() broadcastClashStarted(this) @@ -75,6 +87,8 @@ class ClashService : BaseService() { unregisterReceiver(profileObserver) + isServiceRunning = false + super.onDestroy() } diff --git a/service/src/main/java/com/github/kr328/clash/service/Constants.kt b/service/src/main/java/com/github/kr328/clash/service/Constants.kt index 852cf12eec..c6229ebc88 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Constants.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Constants.kt @@ -5,4 +5,5 @@ object Constants { const val PROFILES_DIR = "profiles" const val PROFILE_PROVIDER_SUFFIX = ".profiles" + const val STATUS_PROVIDER_SUFFIX = ".status" } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt index 04a48b9697..302d145f38 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt @@ -36,7 +36,6 @@ class ProfileBackgroundService : BaseService() { private const val SERVICE_NOTIFICATION_ID_BASE = 10000 } - private val database by lazy { ClashDatabase.getInstance(this).openClashProfileDao() } private val channel = Channel(2) private val queue = mutableListOf>() private val profiles: ClashProfileDao by lazy { @@ -81,8 +80,7 @@ class ProfileBackgroundService : BaseService() { val request = intent.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST) ?: return START_NOT_STICKY - if (request.id != 0L) - channel.offer(request) + channel.offer(request) } Intents.INTENT_ACTION_PROFILE_SETUP -> { launch { @@ -98,57 +96,62 @@ class ProfileBackgroundService : BaseService() { return Binder() } - private fun startProfileProcessor(service: IProfileService) { - launch { - while (isActive) { - val timeout = timeout(1000 * 60L) - - select { - channel.onReceive { - val deferred = CompletableDeferred() - - it.withCallback(object : IStreamCallback.Stub() { - override fun complete() { - deferred.complete(it) - launch { - updateUpdateComplete(it.id) - } - } + private fun startProfileProcessor(service: IProfileService) = launch { + while (isActive) { + val timeout = timeout(1000 * 30L) + + select { + channel.onReceive { + val deferred = CompletableDeferred() + val originalCallback = it.callback + + it.withCallback(object : IStreamCallback.Stub() { + override fun complete() { + originalCallback?.complete() + deferred.complete(it) - override fun completeExceptionally(reason: String?) { - deferred.complete(it) - launch { - updateUpdateFailure(it.id) - } + launch { + updateUpdateComplete(it.id) } + } + + override fun completeExceptionally(reason: String?) { + originalCallback?.completeExceptionally(reason) + deferred.complete(it) - override fun send(data: ParcelableContainer?) { - launch { - updateUpdating(it.id) - } + launch { + updateUpdateFailure(it.id, reason ?: "Unknown") } - }) + } - service.enqueueRequest(it) + override fun send(data: ParcelableContainer?) { + originalCallback?.send(data) - queue.add(deferred) - } - if (queue.isNotEmpty()) { - for (task in queue) { - task.onAwait { - queue.remove(task) + launch { + updateUpdating(it.id) } } - } else { - timeout.onJoin { - stopSelf() - cancel() + }) + + service.enqueueRequest(it) + + queue.add(deferred) + } + if (queue.isNotEmpty()) { + for (task in queue) { + task.onAwait { + queue.remove(task) } } + } else { + timeout.onJoin { + stopSelf() + cancel() + } } - - timeout.cancel() } + + timeout.cancel() } } @@ -206,13 +209,14 @@ class ProfileBackgroundService : BaseService() { } private suspend fun updateUpdating(id: Long) { - val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() - val entity = database.queryProfileById(id) ?: return + val notificationId = ((id + 1) % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() + val entity = profiles.queryProfileById(id) ?: return val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) .setContentTitle(getText(R.string.profile_status_title)) .setContentText(getString(R.string.profile_status_updating, entity.name)) .setSmallIcon(R.drawable.ic_update_normal) + .setOnlyAlertOnce(true) .setOngoing(true) .build() @@ -221,8 +225,8 @@ class ProfileBackgroundService : BaseService() { } private suspend fun updateUpdateComplete(id: Long) { - val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() - val entity = database.queryProfileById(id) + val notificationId = ((id + 1) % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() + val entity = profiles.queryProfileById(id) if (entity == null) { NotificationManagerCompat.from(this).cancel(notificationId) @@ -233,15 +237,16 @@ class ProfileBackgroundService : BaseService() { .setContentTitle(getText(R.string.profile_status_title)) .setContentText(getString(R.string.profile_status_update_completed, entity.name)) .setSmallIcon(R.drawable.ic_update_normal) + .setOnlyAlertOnce(true) .build() NotificationManagerCompat.from(this) .notify(SERVICE_NOTIFICATION_ID_BASE + notificationId, notification) } - private suspend fun updateUpdateFailure(id: Long) { - val notificationId = (id % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() - val entity = database.queryProfileById(id) + private suspend fun updateUpdateFailure(id: Long, reason: String) { + val notificationId = ((id + 1) % (Int.MAX_VALUE - SERVICE_NOTIFICATION_ID_BASE)).toInt() + val entity = profiles.queryProfileById(id) if (entity == null) { NotificationManagerCompat.from(this).cancel(notificationId) @@ -249,10 +254,10 @@ class ProfileBackgroundService : BaseService() { } val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) - .setContentTitle(getText(R.string.profile_status_title)) - .setContentText(getString(R.string.profile_status_update_failure, entity.name)) + .setContentTitle(getString(R.string.profile_status_update_failure, entity.name)) + .setContentText(reason) .setSmallIcon(R.drawable.ic_update_normal) - .setOngoing(true) + .setOnlyAlertOnce(true) .build() NotificationManagerCompat.from(this) diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt index 5707a597d9..398e8d38a2 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt @@ -4,7 +4,6 @@ import android.app.AlarmManager import android.app.PendingIntent import android.content.Intent import android.net.Uri -import android.os.Build import android.os.IBinder import androidx.core.content.FileProvider import com.github.kr328.clash.core.Global @@ -17,7 +16,6 @@ import com.github.kr328.clash.service.transact.ProfileRequest import com.github.kr328.clash.service.util.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel -import java.io.File import java.util.* class ProfileService : BaseService() { @@ -70,8 +68,6 @@ class ProfileService : BaseService() { file ).toString() - Log.d("Generated template file $file") - "$url?id=${entity.id}&fileName=$fileName" } } @@ -85,7 +81,9 @@ class ProfileService : BaseService() { val id = u.getQueryParameter("id")?.toLongOrNull() ?: return val fileName = u.getQueryParameter("fileName") ?: return - val request = ProfileRequest().withId(id).withURL(u) + val request = ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE) + .withId(id) + .withURL(u) .withCallback(object : IStreamCallback.Stub() { override fun complete() { cacheDir.resolve("profiles/$fileName").delete() @@ -100,12 +98,10 @@ class ProfileService : BaseService() { } }) val i = ProfileBackgroundService::class.intent + .setAction(Intents.INTENT_ACTION_PROFILE_ENQUEUE_REQUEST) .putExtra(Intents.INTENT_EXTRA_PROFILE_REQUEST, request) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) - startForegroundService(i) - else - startService(i) + startForegroundServiceCompat(i) } } } @@ -188,7 +184,7 @@ class ProfileService : BaseService() { val id = request.id val entity: ClashProfileEntity = - if (id == 0L) { + if (id == -1L) { ClashProfileEntity( requireNotNull(request.name), requireNotNull(request.type), @@ -209,11 +205,12 @@ class ProfileService : BaseService() { ) } - processor.createOrUpdate(entity, id == 0L) + processor.createOrUpdate(entity, id == -1L) if (entity.updateInterval > 0) { val nextRequest = - ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE).withId(entity.id) + ProfileRequest().action(ProfileRequest.Action.UPDATE_OR_CREATE) + .withId(entity.id) requireNotNull(getSystemService(AlarmManager::class.java)).set( AlarmManager.RTC, diff --git a/service/src/main/java/com/github/kr328/clash/service/ServiceStatusProvider.kt b/service/src/main/java/com/github/kr328/clash/service/ServiceStatusProvider.kt new file mode 100644 index 0000000000..c380fa7668 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/ServiceStatusProvider.kt @@ -0,0 +1,60 @@ +package com.github.kr328.clash.service + +import android.content.ContentProvider +import android.content.ContentValues +import android.database.Cursor +import android.net.Uri +import android.os.Bundle + +class ServiceStatusProvider : ContentProvider() { + companion object { + const val METHOD_PING_CLASH_SERVICE = "pingClashService" + } + + override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { + return when (method) { + METHOD_PING_CLASH_SERVICE -> { + return if (ClashService.isServiceRunning) + Bundle() + else + null + } + else -> super.call(method, arg, extras) + } + } + + override fun insert(uri: Uri, values: ContentValues?): Uri? { + throw IllegalArgumentException("Stub!") + } + + override fun query( + uri: Uri, + projection: Array?, + selection: String?, + selectionArgs: Array?, + sortOrder: String? + ): Cursor? { + throw IllegalArgumentException("Stub!") + } + + override fun update( + uri: Uri, + values: ContentValues?, + selection: String?, + selectionArgs: Array? + ): Int { + throw IllegalArgumentException("Stub!") + } + + override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { + throw IllegalArgumentException("Stub!") + } + + override fun getType(uri: Uri): String? { + throw IllegalArgumentException("Stub!") + } + + override fun onCreate(): Boolean { + return true + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/Settings.kt b/service/src/main/java/com/github/kr328/clash/service/Settings.kt index 3ff4d24151..d63411611c 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Settings.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Settings.kt @@ -10,6 +10,7 @@ class Settings(private val clashManager: IClashManager) { val ACCESS_CONTROL_MODE = StringSetting("access_control_mode", ACCESS_CONTROL_MODE_ALL) val ACCESS_CONTROL_PACKAGES = PackageListSetting("access_control_packages", emptyList()) val DNS_HIJACKING = BooleanSetting("dns_hijacking", true) + val NOTIFICATION_REFRESH = BooleanSetting("notification_refresh", true) } fun put(key: String, value: String) { @@ -56,7 +57,7 @@ class Settings(private val clashManager: IClashManager) { } } - class StringSetting(override val key: String, val def: String) : Setting { + class StringSetting(override val key: String, private val def: String) : Setting { override fun parseValue(value: String?): String { return value ?: def } 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 160cff0bfc..ed565c2366 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 @@ -38,9 +38,13 @@ class TunService : VpnService(), CoroutineScope by MainScope() { else "$PRIVATE_VLAN_DNS:53" + Log.i("TunService.startTun ${fd.fd}") + Clash.startTunDevice(fd.fd, VPN_MTU, dnsAddress) { stopSelf() } + + fd.close() } override fun onCreate() { @@ -68,6 +72,8 @@ class TunService : VpnService(), CoroutineScope by MainScope() { defaultNetworkChannel.unregister() + Log.i("TunService.onDestroy") + super.onDestroy() } @@ -102,7 +108,6 @@ class TunService : VpnService(), CoroutineScope by MainScope() { addDisallowedApplication(packageName) } Settings.ACCESS_CONTROL_MODE_WHITELIST -> { - addAllowedApplication(packageName) for (app in settings.get(Settings.ACCESS_CONTROL_PACKAGES).toSet() - resources.getStringArray(R.array.default_disallow_application) - setOf(packageName)) { @@ -124,6 +129,7 @@ class TunService : VpnService(), CoroutineScope by MainScope() { } addDisallowedApplication(packageName) } + else -> throw IllegalArgumentException("Invalid mode") } return this diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt index b103f19677..64384e5476 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileEntity.kt @@ -4,7 +4,6 @@ import android.os.Parcel import android.os.Parcelable import androidx.room.ColumnInfo import androidx.room.Entity -import androidx.room.PrimaryKey import com.github.kr328.clash.core.serialization.Parcels import kotlinx.serialization.Serializable diff --git a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt index 9e261ffb29..a2eb7075c6 100644 --- a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt +++ b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt @@ -13,7 +13,7 @@ import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class DefaultNetworkChannel(val context: Context, scope: CoroutineScope): +class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) : CoroutineScope by scope, Channel by Channel(Channel.CONFLATED) { private val connectivity = context.getSystemService(ConnectivityManager::class.java)!! private val callback = object : ConnectivityManager.NetworkCallback() { @@ -22,11 +22,13 @@ class DefaultNetworkChannel(val context: Context, scope: CoroutineScope): send(rebuildNetworkList()) } } + override fun onLost(network: Network) { launch { send(rebuildNetworkList()) } } + override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities diff --git a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt index 36fd65156c..ea07aa4d65 100644 --- a/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt +++ b/service/src/main/java/com/github/kr328/clash/service/transact/ProfileRequest.kt @@ -22,7 +22,7 @@ class ProfileRequest private constructor(private val bundle: Bundle) : Parcelabl val type: Int get() = bundle.getInt(KEY_TYPE, -1) val id: Long - get() = bundle.getLong(KEY_ID, 0) + get() = bundle.getLong(KEY_ID, -1) val name: String? get() = bundle.getString(KEY_NAME) val url: Uri? diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt index b8a00d607c..6b658c6141 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt @@ -5,8 +5,6 @@ import android.content.Intent import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.Intents import com.github.kr328.clash.service.data.ClashDatabase -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext fun Context.sendBroadcastSelf(intent: Intent) { this.sendBroadcast(intent.setPackage(this.packageName)) diff --git a/service/src/main/java/com/github/kr328/clash/service/util/ServiceUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/ServiceUtils.kt new file mode 100644 index 0000000000..fc2157cf36 --- /dev/null +++ b/service/src/main/java/com/github/kr328/clash/service/util/ServiceUtils.kt @@ -0,0 +1,13 @@ +package com.github.kr328.clash.service.util + +import android.content.Context +import android.content.Intent +import android.os.Build + +fun Context.startForegroundServiceCompat(intent: Intent) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } +} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/Timeout.kt b/service/src/main/java/com/github/kr328/clash/service/util/Timeout.kt index e19d43703b..d5ba5b0c67 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/Timeout.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/Timeout.kt @@ -1,6 +1,9 @@ package com.github.kr328.clash.service.util -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch fun CoroutineScope.timeout(timeout: Long): Job { return launch { diff --git a/service/src/main/res/drawable/ic_notification.xml b/service/src/main/res/drawable/ic_notification.xml index 9189393130..02e55acb0d 100644 --- a/service/src/main/res/drawable/ic_notification.xml +++ b/service/src/main/res/drawable/ic_notification.xml @@ -1,9 +1,9 @@ - - + android:height="200dp" + android:viewportWidth="349" + android:viewportHeight="336"> + From 01d9d231b561ae5804dc60b37e93d173391dbe8d Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 15 Feb 2020 01:03:23 +0800 Subject: [PATCH 083/358] [WIP] UI refactor --- .../com/github/kr328/clash/ProxiesActivity.kt | 100 +++++++++++------- .../clash/adapter/AbstractProxyAdapter.kt | 3 +- .../kr328/clash/adapter/GridProxyAdapter.kt | 27 +++-- .../kr328/clash/adapter/ProxyChipAdapter.kt | 75 +++++++++++++ app/src/main/res/anim/simple_alpha.xml | 5 + app/src/main/res/layout/activity_proxies.xml | 15 +-- .../main/res/layout/adapter_proxies_chip.xml | 27 +++++ app/src/main/res/layout/page_proxies.xml | 8 ++ app/src/main/res/values/strings.xml | 8 ++ .../java/com/github/kr328/clash/core/Clash.kt | 2 - .../kr328/clash/core/model/ProxyGroup.kt | 3 - .../kr328/clash/core/model/ProxyGroupList.kt | 2 +- .../clash/core/serialization/MergedParcels.kt | 3 - .../clash/core/transact/DoneCallbackImpl.kt | 1 - .../com/github/kr328/clash/core/utils/Log.kt | 23 ++-- 15 files changed, 227 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt create mode 100644 app/src/main/res/anim/simple_alpha.xml create mode 100644 app/src/main/res/layout/adapter_proxies_chip.xml create mode 100644 app/src/main/res/layout/page_proxies.xml diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index b4c1e6b5ed..253b6d8f6f 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -1,16 +1,19 @@ package com.github.kr328.clash import android.os.Bundle -import androidx.core.view.isEmpty +import android.util.DisplayMetrics +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.adapter.AbstractProxyAdapter import com.github.kr328.clash.adapter.GridProxyAdapter -import com.github.kr328.clash.core.model.ProxyGroup -import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.adapter.ProxyChipAdapter import com.github.kr328.clash.remote.withClash import com.github.kr328.clash.utils.ProxySorter -import com.google.android.material.chip.Chip import kotlinx.android.synthetic.main.activity_proxies.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -18,6 +21,7 @@ class ProxiesActivity : BaseActivity() { private val activity: ProxiesActivity get() = this private var maskedGroup: MutableSet = mutableSetOf() + private val updateChipChannel = Channel(Channel.CONFLATED) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -34,6 +38,58 @@ class ProxiesActivity : BaseActivity() { } } } + + mainList.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + updateChipChannel.offer(Unit) + } + }) + + chipList.adapter = ProxyChipAdapter(this) { + launch { + (mainList.adapter!! as AbstractProxyAdapter).getGroupPosition(it)?.also { + val scroller = (object : LinearSmoothScroller(activity) { + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float { + return 40f / displayMetrics!!.densityDpi + } + + init { + targetPosition = it + } + }) + + mainList.layoutManager?.startSmoothScroll(scroller) + } + } + } + chipList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) + chipList.itemAnimator?.changeDuration = 80 + + launch { + var currentChecked = "" + + while (isActive) { + updateChipChannel.receive() + + val currentGroup = (mainList.adapter!! as AbstractProxyAdapter).getCurrentGroup() + + if (currentChecked == currentGroup) + continue + + currentChecked = currentGroup + + (chipList.adapter!! as ProxyChipAdapter).apply { + selected = currentChecked + + (chipList.layoutManager!! as LinearLayoutManager) + .scrollToPositionWithOffset(chips.indexOf(currentChecked), 0) + } + } + } } override fun onStart() { @@ -58,45 +114,13 @@ class ProxiesActivity : BaseActivity() { sorter.sort(proxies.toList()) } - if (chipGroup.isEmpty()) { - filtered.map(ProxyGroup::name).forEach { - val chip = Chip(activity).apply { - text = it - - chipBackgroundColor = getColorStateList(R.color.proxies_chip_colors) - rippleColor = getColorStateList(R.color.proxies_chip_colors) - setTextColor(getColorStateList(R.color.proxies_chip_text_colors)) - checkedIcon = null - - isCheckable = true - isClickable = true - isFocusable = true - isChecked = !maskedGroup.contains(it) - - setOnCheckedChangeListener { _, isChecked -> - if (isChecked) { - maskedGroup.remove(it) - } else { - maskedGroup.add(it) - } - - refreshList(it) - } - } - - chipGroup.addView(chip) - } - } - (mainList.adapter!! as AbstractProxyAdapter).apply { root = filtered.filter { !maskedGroup.contains(it.name) } applyChange() - - if (scrollTo != null) { - scrollToGroup(scrollTo) - } } + + (chipList.adapter!! as ProxyChipAdapter).chips = filtered.map { it.name } } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt index 35b9fa7c53..06c980a692 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt @@ -7,5 +7,6 @@ interface AbstractProxyAdapter { var onSelectProxyListener: suspend (String, String) -> Unit suspend fun applyChange() - suspend fun scrollToGroup(name: String) + suspend fun getGroupPosition(name: String): Int? + suspend fun getCurrentGroup(): String } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt index 3615a6439e..465b4826fc 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt @@ -14,7 +14,6 @@ import com.github.kr328.clash.ProxiesActivity import com.github.kr328.clash.R import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.core.model.ProxyGroup -import com.github.kr328.clash.core.utils.Log import com.google.android.material.card.MaterialCardView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -25,13 +24,18 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) RecyclerView.Adapter(), AbstractProxyAdapter { private interface RenderInfo { val name: String + val group: String + } + + private data class ProxyGroupInfo(override val name: String) : RenderInfo { + override val group: String + get() = name } - private data class ProxyGroupInfo(override val name: String) : RenderInfo private data class ProxyInfo( override val name: String, val type: Proxy.Type, - val group: String, + override val group: String, val selectable: Boolean, val delay: Short, val active: Boolean @@ -115,17 +119,24 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) } } - override suspend fun scrollToGroup(name: String) { - val position = withContext(Dispatchers.Default) { + override suspend fun getGroupPosition(name: String): Int? { + return withContext(Dispatchers.Default) { renderList.mapIndexed { index, p -> - if ( p is ProxyGroupInfo && p.name == name ) + if (p is ProxyGroupInfo && p.name == name) index else -1 }.singleOrNull { it >= 0 } - } ?: return + } + } + + override suspend fun getCurrentGroup(): String { + val position = layoutManager.findFirstCompletelyVisibleItemPosition() + + if (position < 0) + return "" - layoutManager.scrollToPositionWithOffset(position, 0) + return renderList[position].group } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt new file mode 100644 index 0000000000..b3061f795d --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt @@ -0,0 +1,75 @@ +package com.github.kr328.clash.adapter + +import android.content.Context +import android.graphics.Color +import android.util.TypedValue +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.annotation.ColorInt +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.R +import com.google.android.material.card.MaterialCardView + +class ProxyChipAdapter( + private val context: Context, + private val onClick: (String) -> Unit +) : + RecyclerView.Adapter() { + var chips = listOf() + var selected: String = "" + set(value) { + val lastIndex = chips.indexOf(field) + val newIndex = chips.indexOf(value) + + field = value + + if (lastIndex >= 0) + notifyItemChanged(lastIndex) + if (newIndex >= 0) + notifyItemChanged(newIndex) + } + + @ColorInt + private val colorOnSurface: Int + + init { + val typedValue = TypedValue() + + context.theme.resolveAttribute(R.attr.colorOnSurface, typedValue, true) + colorOnSurface = typedValue.data + } + + class Holder(root: View) : RecyclerView.ViewHolder(root) { + val card: MaterialCardView = root.findViewById(R.id.root) + val title: TextView = root.findViewById(android.R.id.title) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + val layoutInflater = LayoutInflater.from(context) + + return Holder(layoutInflater.inflate(R.layout.adapter_proxies_chip, parent, false)) + } + + override fun getItemCount(): Int { + return chips.size + } + + override fun onBindViewHolder(holder: Holder, position: Int) { + val current = chips[position] + + holder.title.text = current + holder.card.setOnClickListener { + onClick(current) + } + + if (selected == current) { + holder.title.setTextColor(Color.WHITE) + holder.card.setCardBackgroundColor(context.getColor(R.color.primaryCardColorStarted)) + } else { + holder.title.setTextColor(colorOnSurface) + holder.card.setCardBackgroundColor(context.getColor(R.color.chipBackgroundColor)) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/anim/simple_alpha.xml b/app/src/main/res/anim/simple_alpha.xml new file mode 100644 index 0000000000..e370f2b05b --- /dev/null +++ b/app/src/main/res/anim/simple_alpha.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_proxies.xml b/app/src/main/res/layout/activity_proxies.xml index 70d6b385bb..b8d01400a3 100644 --- a/app/src/main/res/layout/activity_proxies.xml +++ b/app/src/main/res/layout/activity_proxies.xml @@ -17,20 +17,11 @@ android:layout_height="wrap_content" /> - - - - + android:layout_marginBottom="10dp"/> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/page_proxies.xml b/app/src/main/res/layout/page_proxies.xml new file mode 100644 index 0000000000..f14841ad0c --- /dev/null +++ b/app/src/main/res/layout/page_proxies.xml @@ -0,0 +1,8 @@ + + + + + \ 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 dbcda1822f..01c9651d92 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -74,6 +74,14 @@ New Profile Edit Profile Clash Start Failure + Refresh + Group Order + Proxy Order + Default + Delay + Direct + Global + Rule Start URL Provider Failure diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 46e83aa5c3..648da75cd5 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -2,7 +2,6 @@ package com.github.kr328.clash.core import android.content.Context import bridge.Bridge -import bridge.EventPoll import com.github.kr328.clash.core.event.EventStream import com.github.kr328.clash.core.event.LogEvent import com.github.kr328.clash.core.model.General @@ -15,7 +14,6 @@ import com.github.kr328.clash.core.transact.ProxyGroupCollectionImpl import kotlinx.coroutines.CompletableDeferred import java.io.File import java.io.InputStream -import java.util.concurrent.CompletableFuture object Clash { private var initialized = false diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt index da51f1eea8..aa22ba82e3 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt @@ -1,8 +1,5 @@ package com.github.kr328.clash.core.model -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.core.serialization.MergedParcels import kotlinx.serialization.Serializable @Serializable diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt index 3f09873270..194af0d79e 100644 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt +++ b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupList.kt @@ -6,7 +6,7 @@ import com.github.kr328.clash.core.serialization.MergedParcels import kotlinx.serialization.Serializable @Serializable -data class ProxyGroupList(val list: List): Parcelable { +data class ProxyGroupList(val list: List) : Parcelable { override fun writeToParcel(parcel: Parcel, flags: Int) { MergedParcels.dump(serializer(), this, parcel) } diff --git a/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt index a60977499e..3221d53b7a 100644 --- a/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt +++ b/core/src/main/java/com/github/kr328/clash/core/serialization/MergedParcels.kt @@ -1,7 +1,6 @@ package com.github.kr328.clash.core.serialization import android.os.Parcel -import com.github.kr328.clash.core.utils.Log import kotlinx.serialization.* import kotlinx.serialization.modules.EmptyModule import kotlinx.serialization.modules.SerialModule @@ -18,8 +17,6 @@ object MergedParcels : AbstractSerialFormat(EmptyModule) { parcel.writeStringList(encoder.getStringList()) parcel.appendFrom(data, 0, data.dataSize()) - - Log.i("Send ${parcel.dataSize()} bytes") } finally { data.recycle() } diff --git a/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt index 2319d6d422..5319f91571 100644 --- a/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt +++ b/core/src/main/java/com/github/kr328/clash/core/transact/DoneCallbackImpl.kt @@ -2,7 +2,6 @@ package com.github.kr328.clash.core.transact import bridge.DoneCallback import kotlinx.coroutines.CompletableDeferred -import java.util.concurrent.CompletableFuture class DoneCallbackImpl : DoneCallback, CompletableDeferred by CompletableDeferred() { override fun doneWithError(e: Exception?) { diff --git a/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt b/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt index d23f4d4fa2..c003b54f5a 100644 --- a/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt +++ b/core/src/main/java/com/github/kr328/clash/core/utils/Log.kt @@ -3,10 +3,21 @@ package com.github.kr328.clash.core.utils import com.github.kr328.clash.core.Constants.TAG object Log { - fun i(message: String, throwable: Throwable? = null) = android.util.Log.i(TAG, message, throwable) - fun w(message: String, throwable: Throwable? = null) = android.util.Log.w(TAG, message, throwable) - fun e(message: String, throwable: Throwable? = null) = android.util.Log.e(TAG, message, throwable) - fun d(message: String, throwable: Throwable? = null) = android.util.Log.d(TAG, message, throwable) - fun v(message: String, throwable: Throwable? = null) = android.util.Log.v(TAG, message, throwable) - fun wtf(message: String, throwable: Throwable? = null) = android.util.Log.wtf(TAG, message, throwable) + fun i(message: String, throwable: Throwable? = null) = + android.util.Log.i(TAG, message, throwable) + + fun w(message: String, throwable: Throwable? = null) = + android.util.Log.w(TAG, message, throwable) + + fun e(message: String, throwable: Throwable? = null) = + android.util.Log.e(TAG, message, throwable) + + fun d(message: String, throwable: Throwable? = null) = + android.util.Log.d(TAG, message, throwable) + + fun v(message: String, throwable: Throwable? = null) = + android.util.Log.v(TAG, message, throwable) + + fun wtf(message: String, throwable: Throwable? = null) = + android.util.Log.wtf(TAG, message, throwable) } From 718388db51f2efd2609fa42949ba1a1f0f9c974f Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sat, 15 Feb 2020 14:42:09 +0800 Subject: [PATCH 084/358] [WIP] UI refactor --- .../com/github/kr328/clash/ProxiesActivity.kt | 67 +++----------- .../kr328/clash/adapter/ProxyChipAdapter.kt | 54 +++++++++++- .../kr328/clash/view/ProxiesTabMediator.kt | 88 +++++++++++++++++++ .../main/res/layout/adapter_proxies_chip.xml | 3 +- .../src/main/res/drawable/ic_notification.xml | 10 +-- 5 files changed, 158 insertions(+), 64 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index 253b6d8f6f..a06c861fa5 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -1,33 +1,29 @@ package com.github.kr328.clash import android.os.Bundle -import android.util.DisplayMetrics import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.LinearSmoothScroller -import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.adapter.AbstractProxyAdapter import com.github.kr328.clash.adapter.GridProxyAdapter import com.github.kr328.clash.adapter.ProxyChipAdapter import com.github.kr328.clash.remote.withClash import com.github.kr328.clash.utils.ProxySorter +import com.github.kr328.clash.view.ProxiesTabMediator import kotlinx.android.synthetic.main.activity_proxies.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ProxiesActivity : BaseActivity() { private val activity: ProxiesActivity get() = this - private var maskedGroup: MutableSet = mutableSetOf() - private val updateChipChannel = Channel(Channel.CONFLATED) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_proxies) setSupportActionBar(toolbar) + val mediator = ProxiesTabMediator(this, mainList, chipList) + GridProxyAdapter(this).apply { mainList.layoutManager = layoutManager mainList.adapter = this @@ -39,56 +35,16 @@ class ProxiesActivity : BaseActivity() { } } - mainList.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - updateChipChannel.offer(Unit) - } - }) - chipList.adapter = ProxyChipAdapter(this) { launch { - (mainList.adapter!! as AbstractProxyAdapter).getGroupPosition(it)?.also { - val scroller = (object : LinearSmoothScroller(activity) { - override fun getVerticalSnapPreference(): Int { - return SNAP_TO_START - } - - override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float { - return 40f / displayMetrics!!.densityDpi - } - - init { - targetPosition = it - } - }) - - mainList.layoutManager?.startSmoothScroll(scroller) - } + mediator.scrollTo(it) } } chipList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) - chipList.itemAnimator?.changeDuration = 80 + chipList.itemAnimator?.changeDuration = 0 launch { - var currentChecked = "" - - while (isActive) { - updateChipChannel.receive() - - val currentGroup = (mainList.adapter!! as AbstractProxyAdapter).getCurrentGroup() - - if (currentChecked == currentGroup) - continue - - currentChecked = currentGroup - - (chipList.adapter!! as ProxyChipAdapter).apply { - selected = currentChecked - - (chipList.layoutManager!! as LinearLayoutManager) - .scrollToPositionWithOffset(chips.indexOf(currentChecked), 0) - } - } + mediator.exec() } } @@ -102,7 +58,7 @@ class ProxiesActivity : BaseActivity() { finish() } - private fun refreshList(scrollTo: String? = null) { + private fun refreshList() { launch { val proxies = withClash { queryAllProxyGroups() @@ -110,17 +66,20 @@ class ProxiesActivity : BaseActivity() { val sorter = ProxySorter(ProxySorter.Order.DEFAULT, ProxySorter.Order.DEFAULT) - val filtered = withContext(Dispatchers.Default) { + val sorted = withContext(Dispatchers.Default) { sorter.sort(proxies.toList()) } (mainList.adapter!! as AbstractProxyAdapter).apply { - root = filtered.filter { !maskedGroup.contains(it.name) } + root = sorted applyChange() } - (chipList.adapter!! as ProxyChipAdapter).chips = filtered.map { it.name } + (chipList.adapter!! as ProxyChipAdapter).apply { + chips = sorted.map { it.name } + notifyDataSetChanged() + } } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt index b3061f795d..964c1556e4 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash.adapter +import android.animation.ValueAnimator import android.content.Context import android.graphics.Color import android.util.TypedValue @@ -44,6 +45,51 @@ class ProxyChipAdapter( class Holder(root: View) : RecyclerView.ViewHolder(root) { val card: MaterialCardView = root.findViewById(R.id.root) val title: TextView = root.findViewById(android.R.id.title) + + private var cardAnimator: ValueAnimator? = null + private var cardColor: Int = card.cardBackgroundColor.defaultColor + private var titleAnimator: ValueAnimator? = null + private var titleColor: Int = title.textColors.defaultColor + + fun setCardColorAnimation(color: Int) { + if (cardColor == color) + return + + cardAnimator?.cancel() + + cardAnimator = ValueAnimator.ofArgb(cardColor, color).apply { + addUpdateListener { + val v = animatedValue as Int + + card.setCardBackgroundColor(v) + + cardColor = v + } + + duration = 200 + start() + } + } + + fun setTitleColorAnimation(color: Int) { + if (color == titleColor) + return + + titleAnimator?.cancel() + + titleAnimator = ValueAnimator.ofArgb(titleColor, color).apply { + addUpdateListener { + val v = animatedValue as Int + + title.setTextColor(v) + + titleColor = v + } + + duration = 200 + start() + } + } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { @@ -65,11 +111,11 @@ class ProxyChipAdapter( } if (selected == current) { - holder.title.setTextColor(Color.WHITE) - holder.card.setCardBackgroundColor(context.getColor(R.color.primaryCardColorStarted)) + holder.setTitleColorAnimation(Color.WHITE) + holder.setCardColorAnimation(context.getColor(R.color.primaryCardColorStarted)) } else { - holder.title.setTextColor(colorOnSurface) - holder.card.setCardBackgroundColor(context.getColor(R.color.chipBackgroundColor)) + holder.setTitleColorAnimation(colorOnSurface) + holder.setCardColorAnimation(context.getColor(R.color.chipBackgroundColor)) } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt b/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt new file mode 100644 index 0000000000..48929e7d58 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt @@ -0,0 +1,88 @@ +package com.github.kr328.clash.view + +import android.util.DisplayMetrics +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.ProxiesActivity +import com.github.kr328.clash.adapter.AbstractProxyAdapter +import com.github.kr328.clash.adapter.ProxyChipAdapter +import kotlinx.coroutines.channels.Channel + +class ProxiesTabMediator( + private val context: ProxiesActivity, + private val proxiesView: RecyclerView, + private val chipView: RecyclerView +) { + private var preventChipScroll = false + private val updateChipChannel = Channel(Channel.CONFLATED) + + init { + proxiesView.addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + updateChipChannel.offer(Unit) + } + }) + } + + suspend fun exec() { + while (true) { + var currentChecked = "" + + while (true) { + updateChipChannel.receive() + + val currentGroup = (proxiesView.adapter!! as AbstractProxyAdapter).getCurrentGroup() + + if (currentChecked == currentGroup) + continue + + currentChecked = currentGroup + + (chipView.adapter!! as ProxyChipAdapter).apply { + selected = currentChecked + + if (!preventChipScroll) + chipView.smoothScrollToPosition(chips.indexOf(currentChecked)) + } + } + } + } + + suspend fun scrollTo(group: String) { + (proxiesView.adapter!! as AbstractProxyAdapter).apply { + val index = getGroupPosition(group) ?: return + + scrollTo(index) + } + } + + private fun scrollTo(position: Int) { + val scroller = (object : LinearSmoothScroller(context) { + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float { + return 35f / displayMetrics!!.densityDpi + } + + override fun onStop() { + super.onStop() + + preventChipScroll = false + } + + override fun onStart() { + super.onStart() + + preventChipScroll = true + } + + init { + targetPosition = position + } + }) + + proxiesView.layoutManager!!.startSmoothScroll(scroller) + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_proxies_chip.xml b/app/src/main/res/layout/adapter_proxies_chip.xml index d43393f8c1..125faa0b8e 100644 --- a/app/src/main/res/layout/adapter_proxies_chip.xml +++ b/app/src/main/res/layout/adapter_proxies_chip.xml @@ -9,7 +9,8 @@ android:layout_marginTop="10dp" app:cardCornerRadius="12dp" android:focusable="true" - android:clickable="true"> + android:clickable="true" + app:cardBackgroundColor="@color/chipBackgroundColor"> + android:width="402.47244dp" + android:height="381.04724dp" + android:viewportWidth="402.47244" + android:viewportHeight="381.04724"> + android:pathData="M95.758,327.156l4,-41l12,-97l13,-94l12,-61l2,-7c2,-5 4,-6 9,-3l10,9l26,41c3,4 6,5 11,4c28,-6 57,-5 85,1c5,1 7,-1 9,-5a1453,1453 0,0 1,30 -49c4,-4 7,-4 9,2l8,36l28,177c5,34 8,67 11,100c2,12 1,13 -11,14c-54,5 -108,5 -162,4c-30,0 -59,-2 -88,-3c-6,0 -12,-3 -18,-4l-17,-1c-18,-2 -34,-9 -44,-26c-17,-28 -6,-65 29,-75c4,-1 8,0 12,1c3,2 3,5 0,7l-6,5c-4,3 -9,6 -12,10a31,31 0,0 0,7 49c10,5 20,6 31,6zM176.758,139.156c-8,0 -15,7 -15,15s6,15 15,15c8,0 15,-6 15,-15c0,-8 -7,-15 -15,-15zM299.758,169.156c8,0 15,-6 15,-15c0,-8 -6,-15 -15,-15a15,15 0,1 0,0 30zM216.758,189.156c4,7 11,7 20,1c7,6 16,6 18,-1c-8,3 -14,2 -19,-7c-4,9 -10,10 -19,7z"/> From d5a0299d917f10d39cc55e9c835b142838290548 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 16 Feb 2020 02:04:54 +0800 Subject: [PATCH 085/358] [WIP] UI refactor --- .../com/github/kr328/clash/BaseActivity.kt | 29 ++- .../com/github/kr328/clash/ProxiesActivity.kt | 173 +++++++++++++++++- .../kr328/clash/adapter/ProxyChipAdapter.kt | 2 +- .../kr328/clash/preference/UiPreferences.kt | 56 ++++++ .../github/kr328/clash/remote/Broadcasts.kt | 17 +- .../github/kr328/clash/remote/ClashClient.kt | 4 + .../com/github/kr328/clash/remote/Remote.kt | 27 ++- .../kr328/clash/view/ProxiesTabMediator.kt | 31 +++- app/src/main/res/layout/activity_proxies.xml | 2 +- app/src/main/res/menu/menu_profile_popup.xml | 7 - app/src/main/res/menu/proxies.xml | 31 ++++ app/src/main/res/values/strings.xml | 8 +- core/build.gradle | 2 +- core/src/main/golang/bridge/general.go | 11 ++ core/src/main/golang/bridge/init.go | 5 +- core/src/main/golang/clash | 2 +- core/src/main/golang/main.go | 32 +++- core/src/main/golang/profile/download.go | 13 -- core/src/main/golang/profile/load.go | 39 +++- .../java/com/github/kr328/clash/core/Clash.kt | 7 + .../kr328/clash/service/IClashManager.aidl | 1 + .../kr328/clash/service/ClashManager.kt | 23 ++- .../clash/service/ClashManagerService.kt | 5 +- .../kr328/clash/service/ClashService.kt | 11 +- .../github/kr328/clash/service/TunService.kt | 4 +- .../service/data/ClashProfileProxyDao.kt | 2 +- .../service/data/ClashProfileProxyEntity.kt | 2 +- 27 files changed, 467 insertions(+), 79 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt delete mode 100644 app/src/main/res/menu/menu_profile_popup.xml create mode 100644 app/src/main/res/menu/proxies.xml diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 8fe474c54b..2bfdb210d8 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -6,14 +6,14 @@ import android.content.Intent import android.content.res.Configuration import android.os.Build import android.os.Bundle -import android.view.LayoutInflater -import android.view.View +import android.view.* import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR -import android.view.ViewGroup import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout +import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.preference.UiPreferences import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.service.data.ClashProfileEntity import com.google.android.material.snackbar.Snackbar @@ -53,6 +53,9 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() get() = Broadcasts.clashRunning val rootView: View get() = overrideRootView ?: window.decorView + var menu: Menu? = null + lateinit var uiPreference: UiPreferences + private set open suspend fun onClashStarted() {} open suspend fun onClashStopped(reason: String?) {} @@ -82,6 +85,8 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + uiPreference = UiPreferences(this) + resetLightNavigationBar() } @@ -97,9 +102,27 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() Broadcasts.unregister(receiver) } + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + this.menu = menu + return super.onCreateOptionsMenu(menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if ( super.onOptionsItemSelected(item) ) + return true + + if ( item.itemId == android.R.id.home ) { + onSupportNavigateUp() + return true + } + + return false + } + override fun setSupportActionBar(toolbar: Toolbar?) { super.setSupportActionBar(toolbar) + supportActionBar?.setDisplayShowHomeEnabled(shouldDisplayHomeAsUpEnabled()) supportActionBar?.setDisplayHomeAsUpEnabled(shouldDisplayHomeAsUpEnabled()) } diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index a06c861fa5..dd0cf8454b 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -1,10 +1,15 @@ package com.github.kr328.clash import android.os.Bundle +import android.view.Menu +import android.view.MenuItem import androidx.recyclerview.widget.LinearLayoutManager import com.github.kr328.clash.adapter.AbstractProxyAdapter import com.github.kr328.clash.adapter.GridProxyAdapter import com.github.kr328.clash.adapter.ProxyChipAdapter +import com.github.kr328.clash.core.model.General +import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.preference.UiPreferences import com.github.kr328.clash.remote.withClash import com.github.kr328.clash.utils.ProxySorter import com.github.kr328.clash.view.ProxiesTabMediator @@ -14,15 +19,21 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ProxiesActivity : BaseActivity() { - private val activity: ProxiesActivity - get() = this + private lateinit var mediator: ProxiesTabMediator + private val doScrollToLastProxy by lazy { + val selected = uiPreference.get(UiPreferences.LAST_SELECT_GROUP) + + launch { + mediator.scrollToDirect(selected) + } + } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_proxies) setSupportActionBar(toolbar) - val mediator = ProxiesTabMediator(this, mainList, chipList) + mediator = ProxiesTabMediator(this, mainList, chipList) GridProxyAdapter(this).apply { mainList.layoutManager = layoutManager @@ -46,28 +57,174 @@ class ProxiesActivity : BaseActivity() { launch { mediator.exec() } + + refreshList() } - override fun onStart() { - super.onStart() + override fun onStop() { + uiPreference.edit { + put(UiPreferences.LAST_SELECT_GROUP, (chipList.adapter!! as ProxyChipAdapter).selected) + } - refreshList() + super.onStop() + } + + override fun onCreateOptionsMenu(menu: Menu?): Boolean { + if (!super.onCreateOptionsMenu(menu)) + return false + + menuInflater.inflate(R.menu.proxies, menu) + + setupMenu() + + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + if (super.onOptionsItemSelected(item)) + return true + + if (item.itemId == R.id.menuRefresh) { + refreshList() + return true + } + + launch { + when (item.itemId) { + R.id.modeDirect -> { + withClash { + setProxyMode(General.Mode.DIRECT) + } + } + R.id.modeGlobal -> { + withClash { + setProxyMode(General.Mode.GLOBAL) + } + } + R.id.modeRule -> { + withClash { + setProxyMode(General.Mode.RULE) + } + } + R.id.groupDefault -> { + uiPreference.edit { + put(UiPreferences.PROXY_GROUP_SORT, UiPreferences.PROXY_SORT_DEFAULT) + } + } + R.id.groupName -> { + uiPreference.edit { + put(UiPreferences.PROXY_GROUP_SORT, UiPreferences.PROXY_SORT_NAME) + } + } + R.id.groupDelay -> { + uiPreference.edit { + put(UiPreferences.PROXY_GROUP_SORT, UiPreferences.PROXY_SORT_DELAY) + } + } + R.id.proxyDefault -> { + uiPreference.edit { + put(UiPreferences.PROXY_PROXY_SORT, UiPreferences.PROXY_SORT_DEFAULT) + } + } + R.id.proxyName -> { + uiPreference.edit { + put(UiPreferences.PROXY_PROXY_SORT, UiPreferences.PROXY_SORT_NAME) + } + } + R.id.proxyDelay -> { + uiPreference.edit { + put(UiPreferences.PROXY_PROXY_SORT, UiPreferences.PROXY_SORT_DELAY) + } + } + else -> return@launch + } + + item.isChecked = true + + refreshList() + } + + return true } override suspend fun onClashStarted() { finish() } + private fun setupMenu() { + launch { + val general = withClash { + queryGeneral() + } + + menu?.apply { + when (general.mode) { + General.Mode.DIRECT -> + findItem(R.id.modeDirect).isChecked = true + General.Mode.GLOBAL -> + findItem(R.id.modeGlobal).isChecked = true + General.Mode.RULE -> + findItem(R.id.modeRule).isChecked = true + } + when (uiPreference.get(UiPreferences.PROXY_GROUP_SORT)) { + UiPreferences.PROXY_SORT_DEFAULT -> + findItem(R.id.groupDefault).isChecked = true + UiPreferences.PROXY_SORT_NAME -> + findItem(R.id.groupName).isChecked = true + UiPreferences.PROXY_SORT_DELAY -> + findItem(R.id.proxyDelay).isChecked = true + } + when (uiPreference.get(UiPreferences.PROXY_PROXY_SORT)) { + UiPreferences.PROXY_SORT_DEFAULT -> + findItem(R.id.proxyDefault).isChecked = true + UiPreferences.PROXY_SORT_NAME -> + findItem(R.id.proxyName).isChecked = true + UiPreferences.PROXY_SORT_DELAY -> + findItem(R.id.proxyDelay).isChecked = true + } + } + } + } + private fun refreshList() { launch { + val general = withClash { + queryGeneral() + } val proxies = withClash { queryAllProxyGroups() } - val sorter = ProxySorter(ProxySorter.Order.DEFAULT, ProxySorter.Order.DEFAULT) + val groupSort = when (uiPreference.get(UiPreferences.PROXY_GROUP_SORT)) { + UiPreferences.PROXY_SORT_DEFAULT -> + ProxySorter.Order.DEFAULT + UiPreferences.PROXY_SORT_NAME -> + ProxySorter.Order.NAME_INCREASE + UiPreferences.PROXY_SORT_DELAY -> + ProxySorter.Order.DELAY_INCREASE + else -> throw IllegalArgumentException() + } + + val proxySort = when (uiPreference.get(UiPreferences.PROXY_PROXY_SORT)) { + UiPreferences.PROXY_SORT_DEFAULT -> + ProxySorter.Order.DEFAULT + UiPreferences.PROXY_SORT_NAME -> + ProxySorter.Order.NAME_INCREASE + UiPreferences.PROXY_SORT_DELAY -> + ProxySorter.Order.DELAY_INCREASE + else -> throw IllegalArgumentException() + } + + val sorter = ProxySorter(groupSort, proxySort) val sorted = withContext(Dispatchers.Default) { sorter.sort(proxies.toList()) + }.run { + when (general.mode) { + General.Mode.GLOBAL -> this + General.Mode.DIRECT -> emptyList() + General.Mode.RULE -> this.filter { it.name != "GLOBAL" } + } } (mainList.adapter!! as AbstractProxyAdapter).apply { @@ -80,6 +237,8 @@ class ProxiesActivity : BaseActivity() { chips = sorted.map { it.name } notifyDataSetChanged() } + + doScrollToLastProxy } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt index 964c1556e4..cf86286698 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyChipAdapter.kt @@ -15,7 +15,7 @@ import com.google.android.material.card.MaterialCardView class ProxyChipAdapter( private val context: Context, - private val onClick: (String) -> Unit + val onClick: (String) -> Unit ) : RecyclerView.Adapter() { var chips = listOf() diff --git a/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt b/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt new file mode 100644 index 0000000000..a894e3423d --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt @@ -0,0 +1,56 @@ +package com.github.kr328.clash.preference + +import android.content.Context +import android.content.SharedPreferences +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class UiPreferences(context: Context) { + companion object { + private const val FILE_NAME = "ui" + + const val PROXY_SORT_DEFAULT = "default" + const val PROXY_SORT_NAME = "name" + const val PROXY_SORT_DELAY = "delay" + + val PROXY_GROUP_SORT = StringEntry("proxy_group_sort", PROXY_SORT_DEFAULT) + val PROXY_PROXY_SORT = StringEntry("proxy_proxy_sort", PROXY_SORT_DEFAULT) + val LAST_SELECT_GROUP = StringEntry("last_select_group", "") + } + + interface Entry { + fun get(sharedPreferences: SharedPreferences): T + fun put(editor: SharedPreferences.Editor, value: T) + } + + class StringEntry(private val key: String, private val defaultValue: String? = null) : + Entry { + override fun get(sharedPreferences: SharedPreferences): String { + return sharedPreferences.getString(key, defaultValue)!! + } + + override fun put(editor: SharedPreferences.Editor, value: String) { + editor.putString(key, value) + } + } + + class Editor(private val editor: SharedPreferences.Editor) { + fun > put(e: E, value: T) { + e.put(editor, value) + } + } + + private val sharedPreferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) + + fun > get(e: E): T { + return e.get(sharedPreferences) + } + + fun edit(block: Editor.() -> Unit) { + val editor = sharedPreferences.edit() + + Editor(editor).apply(block) + + editor.apply() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt index 400c492340..d5b8a7433a 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt @@ -75,13 +75,24 @@ object Broadcasts { null ) - clashRunning = pong != null + val current = pong != null + if (current != clashRunning) { + clashRunning = current + + if (current) { + receivers.forEach { + it.onStarted() + } + } else { + receivers.forEach { + it.onStopped(null) + } + } + } } override fun onStop(owner: LifecycleOwner) { application.unregisterReceiver(broadcastReceiver) - - clashRunning = false } }) } diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt index ceb1d6be3f..d56db3a881 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -40,4 +40,8 @@ class ClashClient(private val service: IClashManager) { withContext(Dispatchers.IO) { service.queryBandwidth() } + + suspend fun setProxyMode(mode: General.Mode) = withContext(Dispatchers.IO) { + service.setProxyMode(mode.toString()) + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/Remote.kt b/app/src/main/java/com/github/kr328/clash/remote/Remote.kt index 61419b3ecc..e7ff0fa25e 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Remote.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Remote.kt @@ -4,6 +4,7 @@ import android.app.Application import android.content.ComponentName import android.content.Context import android.content.ServiceConnection +import android.os.Handler import android.os.IBinder import androidx.core.content.edit import androidx.lifecycle.DefaultLifecycleObserver @@ -82,8 +83,12 @@ object Remote { } fun init(application: Application) { + val handler = Handler() + ProcessLifecycleOwner.get().lifecycle.addObserver(object : DefaultLifecycleObserver { override fun onStart(owner: LifecycleOwner) { + handler.removeMessages(0) + GlobalScope.launch { if (!verifyApk(application)) { application.startActivity(ApkBrokenActivity::class.intent) @@ -109,17 +114,19 @@ object Remote { } override fun onStop(owner: LifecycleOwner) { - clashConnection?.also { - application.unbindService(it) - it.onServiceDisconnected(null) - } - profileConnection?.also { - application.unbindService(it) - it.onServiceDisconnected(null) - } + handler.postDelayed({ + clashConnection?.also { + application.unbindService(it) + it.onServiceDisconnected(null) + } + profileConnection?.also { + application.unbindService(it) + it.onServiceDisconnected(null) + } - clashConnection = null - profileConnection = null + clashConnection = null + profileConnection = null + }, 5000) } }) } diff --git a/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt b/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt index 48929e7d58..644dd8c192 100644 --- a/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt +++ b/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt @@ -1,12 +1,15 @@ package com.github.kr328.clash.view import android.util.DisplayMetrics +import androidx.recyclerview.widget.GridLayoutManager +import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearSmoothScroller import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.ProxiesActivity import com.github.kr328.clash.adapter.AbstractProxyAdapter import com.github.kr328.clash.adapter.ProxyChipAdapter import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay class ProxiesTabMediator( private val context: ProxiesActivity, @@ -38,13 +41,33 @@ class ProxiesTabMediator( currentChecked = currentGroup - (chipView.adapter!! as ProxyChipAdapter).apply { - selected = currentChecked + val adapter = chipView.adapter!! as ProxyChipAdapter - if (!preventChipScroll) - chipView.smoothScrollToPosition(chips.indexOf(currentChecked)) + adapter.selected = currentChecked + + if (!preventChipScroll) { + val index = adapter.chips.indexOf(currentChecked) + + if (index < 0) + continue + + chipView.smoothScrollToPosition(index) } + + delay(200) } + + } + } + + suspend fun scrollToDirect(group: String) { + val position = (proxiesView.adapter!! as AbstractProxyAdapter).getGroupPosition(group) ?: return + + when ( val m = proxiesView.layoutManager ) { + is GridLayoutManager -> + m.scrollToPositionWithOffset(position, 0) + is LinearLayoutManager -> + m.scrollToPositionWithOffset(position, 0) } } diff --git a/app/src/main/res/layout/activity_proxies.xml b/app/src/main/res/layout/activity_proxies.xml index b8d01400a3..d9b0f34799 100644 --- a/app/src/main/res/layout/activity_proxies.xml +++ b/app/src/main/res/layout/activity_proxies.xml @@ -1,6 +1,6 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/menu/proxies.xml b/app/src/main/res/menu/proxies.xml new file mode 100644 index 0000000000..759bb7a4b2 --- /dev/null +++ b/app/src/main/res/menu/proxies.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 01c9651d92..be9f96d91c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -75,13 +75,15 @@ Edit Profile Clash Start Failure Refresh - Group Order - Proxy Order - Default + Delay Direct Global Rule + Mode + Sort Group + Sort Proxy + Default Start URL Provider Failure diff --git a/core/build.gradle b/core/build.gradle index f09de143c1..0e6b4ff960 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -49,7 +49,7 @@ dependencies { afterEvaluate { def ds = tasks.register("downloadMMDB", MMDBDowloadTask.class) { onlyIf { - System.currentTimeMillis() - file("$buildDir/intermediates/dynamic_assets/Country.mmdb").lastModified() > 24 * 3600 * 1000L + System.currentTimeMillis() - file("$buildDir/intermediates/dynamic_assets/Country.mmdb").lastModified() > 7 * 24 * 3600 * 1000L } output = "$buildDir/intermediates/dynamic_assets/Country.mmdb" diff --git a/core/src/main/golang/bridge/general.go b/core/src/main/golang/bridge/general.go index ac422e3606..312768660f 100644 --- a/core/src/main/golang/bridge/general.go +++ b/core/src/main/golang/bridge/general.go @@ -25,3 +25,14 @@ func QueryGeneral() *TunnelGeneral { return result } + +func SetProxyMode(mode string) { + switch mode { + case "Direct": + tunnel.Instance().SetMode(tunnel.Direct) + case "Global": + tunnel.Instance().SetMode(tunnel.Global) + case "Rule": + tunnel.Instance().SetMode(tunnel.Rule) + } +} diff --git a/core/src/main/golang/bridge/init.go b/core/src/main/golang/bridge/init.go index 71dc8aadc6..b789066c3a 100644 --- a/core/src/main/golang/bridge/init.go +++ b/core/src/main/golang/bridge/init.go @@ -8,7 +8,10 @@ import ( ) func LoadMMDB(data []byte) { - mmdb.LoadFromBytes(data) + dataClone := make([]byte, len(data)) + copy(dataClone, data) + + mmdb.LoadFromBytes(dataClone) } func SetHome(homeDir string) { diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index 8971f2c8a5..1de786635c 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit 8971f2c8a570a9fc15df6dccbd3331d04f775e7f +Subproject commit 1de786635ce775b7053ae68dd73ff916bfb146eb diff --git a/core/src/main/golang/main.go b/core/src/main/golang/main.go index 38dd16da61..a3edd94b99 100644 --- a/core/src/main/golang/main.go +++ b/core/src/main/golang/main.go @@ -1,3 +1,33 @@ package main -func main() {} +import ( + "io/ioutil" + "net" + "os" + + "github.com/Dreamacro/clash/component/mmdb" +) + +func main() { + f, err := os.Open("./Country.mmdb") + if err != nil { + println(err) + return + } + + buf, err := ioutil.ReadAll(f) + if err != nil { + println(err) + return + } + + mmdb.LoadFromBytes(buf) + + c, err := mmdb.Instance().Country(net.ParseIP("114.114.114.114")) + if err != nil { + println(err) + return + } + + println(c.Country.IsoCode) +} diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go index b62c5a3d66..a989e4f63f 100644 --- a/core/src/main/golang/profile/download.go +++ b/core/src/main/golang/profile/download.go @@ -10,7 +10,6 @@ import ( "github.com/Dreamacro/clash/adapters/inbound" "github.com/Dreamacro/clash/component/socks5" - "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/tunnel" ) @@ -78,15 +77,3 @@ func SaveAndCheck(data []byte, output, baseDir string) error { return ioutil.WriteFile(output, data, defaultFileMode) } - -func parseConfig(data []byte, baseDir string) (*config.Config, error) { - raw, err := config.UnmarshalRawConfig(data) - if err != nil { - return nil, err - } - - raw.ExternalUI = "" - raw.ExternalController = "" - - return config.ParseRawConfig(raw, baseDir) -} diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index 8c3ac58f2f..c889fe7e62 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -1,27 +1,29 @@ package profile import ( + "fmt" "io/ioutil" "net" "github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/config" + "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" "github.com/Dreamacro/clash/hub/executor" "github.com/Dreamacro/clash/log" "github.com/kr328/cfa/tun" ) +const tunAddress = "172.31.255.253/30" + const defaultConfig = ` +log: debug mode: Direct Proxy: -- name: "ss1" - type: ss - server: server - port: 443 - cipher: chacha20-ietf-poly1305 - password: "password" - # udp: true +- name: "broadcast" + type: socks5 + server: 255.255.255.255 + port: 1080 Proxy Group: - name: "select" @@ -34,11 +36,15 @@ Rule: // LoadDefault - load default configure func LoadDefault() { - defaultC, _ := config.Parse([]byte(defaultConfig)) - - tun.ResetDnsRedirect() + defaultC, err := parseConfig([]byte(defaultConfig), constant.Path.HomeDir()) + if err != nil { + log.Warnln("Load Default Failure " + err.Error()) + return + } executor.ApplyConfig(defaultC, true) + + tun.ResetDnsRedirect() } // LoadFromFile - load file @@ -103,3 +109,16 @@ func LoadFromFile(path, baseDir string) error { return nil } + +func parseConfig(data []byte, baseDir string) (*config.Config, error) { + raw, err := config.UnmarshalRawConfig(data) + if err != nil { + return nil, err + } + + raw.ExternalUI = "" + raw.ExternalController = "" + raw.Rule = append([]string{fmt.Sprintf("IP-CIDR,%s,REJECT", tunAddress)}, raw.Rule...) + + return config.ParseRawConfig(raw, baseDir) +} diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 648da75cd5..1e570cf30d 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -11,6 +11,7 @@ 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 @@ -30,6 +31,8 @@ object Clash { Bridge.loadMMDB(bytes) Bridge.setHome(context.cacheDir.absolutePath) Bridge.reset() + + Log.d("MMDB loaded ${bytes.size}") } fun start() { @@ -97,6 +100,10 @@ object Clash { } } + fun setProxyMode(mode: String) { + Bridge.setProxyMode(mode) + } + fun queryGeneral(): General { val t = Bridge.queryGeneral() diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl index 09dff42e63..700bd59110 100644 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl @@ -8,6 +8,7 @@ interface IClashManager { // Control boolean setSelectProxy(String proxy, String selected); void startHealthCheck(String group, IStreamCallback callback); + void setProxyMode(String mode); // Query ProxyGroupList queryAllProxies(); diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index ce55c46f08..0958fa583f 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -4,14 +4,22 @@ 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.ProxyGroup import com.github.kr328.clash.core.model.ProxyGroupList -import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.data.ClashProfileProxyEntity import com.github.kr328.clash.service.ipc.IStreamCallback import com.github.kr328.clash.service.ipc.ParcelableContainer +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch -class ClashManager(context: Context) : IClashManager.Stub() { +class ClashManager(context: Context, parent: CoroutineScope) : + IClashManager.Stub(), CoroutineScope by parent { private val settings = context.getSharedPreferences("service", Context.MODE_PRIVATE) + private val database = ClashDatabase.getInstance(context) + + override fun setProxyMode(mode: String?) { + Clash.setProxyMode(requireNotNull(mode)) + } override fun queryAllProxies(): ProxyGroupList { return ProxyGroupList(Clash.queryProxyGroups()) @@ -24,11 +32,18 @@ class ClashManager(context: Context) : IClashManager.Stub() { override fun setSelectProxy(proxy: String?, selected: String?): Boolean { require(proxy != null && selected != null) + launch { + val current = database.openClashProfileDao() + .queryActiveProfile() ?: return@launch + database.openClashProfileProxyDao() + .setSelectedForProfile(ClashProfileProxyEntity(current.id, proxy, selected)) + } + return Clash.setSelectedProxy(proxy, selected) } override fun putSetting(key: String?, value: String?): Boolean { - settings.edit { + settings.edit(commit = false) { putString(key, value) } return true diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt index 093928c086..c1dc9a81f3 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt @@ -1,11 +1,10 @@ package com.github.kr328.clash.service -import android.app.Service import android.content.Intent import android.os.IBinder -class ClashManagerService : Service() { +class ClashManagerService : BaseService() { override fun onBind(intent: Intent?): IBinder? { - return ClashManager(this) + return ClashManager(this, this) } } \ No newline at end of file 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 df6b93c474..ef033c9fc6 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 @@ -24,7 +24,7 @@ class ClashService : BaseService() { private lateinit var notification: ClashNotification private var stopReason: String? = null private val reloadChannel = Channel(Channel.CONFLATED) - private val settings: Settings by lazy { Settings(ClashManager(this)) } + private val settings: Settings by lazy { Settings(ClashManager(this, this)) } private val profileObserver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.`package` != packageName) @@ -93,8 +93,8 @@ class ClashService : BaseService() { } private suspend fun reloadProfile() = withContext(Dispatchers.IO) { - val active = ClashDatabase.getInstance(service).openClashProfileDao().queryActiveProfile() - ?: return@withContext stopSelf("Empty active profile") + val active = ClashDatabase.getInstance(service).openClashProfileDao() + .queryActiveProfile() ?: return@withContext stopSelf("Empty active profile") try { Clash.loadProfile( @@ -102,6 +102,11 @@ class ClashService : BaseService() { resolveBase(active.id) ).await() + ClashDatabase.getInstance(service).openClashProfileProxyDao() + .querySelectedForProfile(active.id).forEach { + Clash.setSelectedProxy(it.proxy, it.selected) + } + notification.setProfile(active.name) } catch (e: Exception) { stopSelf("Load profile failure") 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 ed565c2366..36d966de29 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 @@ -50,7 +50,9 @@ class TunService : VpnService(), CoroutineScope by MainScope() { override fun onCreate() { super.onCreate() - settings = Settings(ClashManager(this)) + Clash.initialize(this) + + settings = Settings(ClashManager(this, this)) defaultNetworkChannel = DefaultNetworkChannel(this, this) diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyDao.kt index 900b0a5456..971cb1d803 100644 --- a/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyDao.kt +++ b/service/src/main/java/com/github/kr328/clash/service/data/ClashProfileProxyDao.kt @@ -11,7 +11,7 @@ interface ClashProfileProxyDao { suspend fun setSelectedForProfile(item: ClashProfileProxyEntity) @Query("SELECT * FROM profile_select_proxies WHERE profile_id = :id") - suspend fun querySelectedForProfile(id: Int): List + suspend fun querySelectedForProfile(id: Long): List @Query("DELETE FROM profile_select_proxies WHERE profile_id = :id AND proxy in (:selected)") suspend fun removeSelectedForProfile(id: Int, selected: List) 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 ca3002d6f9..d5766eac8f 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 @@ -14,7 +14,7 @@ import androidx.room.* )] ) data class ClashProfileProxyEntity( - @ColumnInfo(name = "profile_id") val profileId: Int, + @ColumnInfo(name = "profile_id") val profileId: Long, @ColumnInfo(name = "proxy") val proxy: String, @ColumnInfo(name = "selected") val selected: String, @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Int = 0 From 5d4b6b4fb550215c5ccd6a7b7ecae27067bc29d8 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 16 Feb 2020 15:54:22 +0800 Subject: [PATCH 086/358] add prefix merger --- .../com/github/kr328/clash/ProxiesActivity.kt | 63 +++++++- .../clash/adapter/AbstractProxyAdapter.kt | 19 ++- .../kr328/clash/adapter/GridProxyAdapter.kt | 143 +++++++++--------- .../kr328/clash/preference/UiPreferences.kt | 14 +- .../github/kr328/clash/utils/PrefixMerger.kt | 66 ++++++++ .../main/res/layout/adapter_grid_proxy.xml | 6 +- app/src/main/res/menu/proxies.xml | 7 + app/src/main/res/values/strings.xml | 2 + 8 files changed, 235 insertions(+), 85 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index dd0cf8454b..c069564885 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -8,20 +8,22 @@ import com.github.kr328.clash.adapter.AbstractProxyAdapter import com.github.kr328.clash.adapter.GridProxyAdapter import com.github.kr328.clash.adapter.ProxyChipAdapter import com.github.kr328.clash.core.model.General -import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.preference.UiPreferences import com.github.kr328.clash.remote.withClash +import com.github.kr328.clash.utils.PrefixMerger import com.github.kr328.clash.utils.ProxySorter import com.github.kr328.clash.view.ProxiesTabMediator import kotlinx.android.synthetic.main.activity_proxies.* import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ProxiesActivity : BaseActivity() { private lateinit var mediator: ProxiesTabMediator private val doScrollToLastProxy by lazy { - val selected = uiPreference.get(UiPreferences.LAST_SELECT_GROUP) + val selected = uiPreference.get(UiPreferences.PROXY_LAST_SELECT_GROUP) launch { mediator.scrollToDirect(selected) @@ -63,7 +65,10 @@ class ProxiesActivity : BaseActivity() { override fun onStop() { uiPreference.edit { - put(UiPreferences.LAST_SELECT_GROUP, (chipList.adapter!! as ProxyChipAdapter).selected) + put( + UiPreferences.PROXY_LAST_SELECT_GROUP, + (chipList.adapter!! as ProxyChipAdapter).selected + ) } super.onStop() @@ -136,6 +141,17 @@ class ProxiesActivity : BaseActivity() { put(UiPreferences.PROXY_PROXY_SORT, UiPreferences.PROXY_SORT_DELAY) } } + R.id.utilsMergePrefix -> { + item.isChecked = !item.isChecked + + uiPreference.edit { + put(UiPreferences.PROXY_MERGE_PREFIX, item.isChecked) + } + + refreshList() + + return@launch + } else -> return@launch } @@ -182,6 +198,9 @@ class ProxiesActivity : BaseActivity() { UiPreferences.PROXY_SORT_DELAY -> findItem(R.id.proxyDelay).isChecked = true } + + findItem(R.id.utilsMergePrefix).isChecked = + uiPreference.get(UiPreferences.PROXY_MERGE_PREFIX) } } } @@ -195,6 +214,16 @@ class ProxiesActivity : BaseActivity() { queryAllProxyGroups() } + val prefix = if (uiPreference.get(UiPreferences.PROXY_MERGE_PREFIX)) { + proxies.map { + async { PrefixMerger.merge(it.proxies.map { p -> it.name to p.name }) { it.second } } + }.flatMap { + it.await() + }.map { + it.value to it + }.toMap() + } else emptyMap() + val groupSort = when (uiPreference.get(UiPreferences.PROXY_GROUP_SORT)) { UiPreferences.PROXY_SORT_DEFAULT -> ProxySorter.Order.DEFAULT @@ -218,7 +247,7 @@ class ProxiesActivity : BaseActivity() { val sorter = ProxySorter(groupSort, proxySort) val sorted = withContext(Dispatchers.Default) { - sorter.sort(proxies.toList()) + sorter.sort(proxies) }.run { when (general.mode) { General.Mode.GLOBAL -> this @@ -227,12 +256,30 @@ class ProxiesActivity : BaseActivity() { } } - (mainList.adapter!! as AbstractProxyAdapter).apply { - root = sorted - - applyChange() + val newList = withContext(Dispatchers.Default) { + sorted.map { + AbstractProxyAdapter.ProxyGroupInfo(it.name, + it.proxies.map { p -> + val r = prefix.getOrElse(it.name to p.name) { + PrefixMerger.Result(p.name, "", p) + } + + AbstractProxyAdapter.ProxyInfo( + p.name, + it.name, + r.prefix, + if (r.content.isEmpty()) p.type.toString() else r.content, + p.delay.toShort(), + it.type == Proxy.Type.SELECT, + p.name == it.current + ) + } + ) + } } + (mainList.adapter!! as AbstractProxyAdapter).applyChange(newList) + (chipList.adapter!! as ProxyChipAdapter).apply { chips = sorted.map { it.name } notifyDataSetChanged() diff --git a/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt index 06c980a692..1232d209b3 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt @@ -1,12 +1,23 @@ package com.github.kr328.clash.adapter -import com.github.kr328.clash.core.model.ProxyGroup - interface AbstractProxyAdapter { - var root: List + data class ProxyGroupInfo( + val name: String, + val proxies: List + ) + data class ProxyInfo( + val name: String, + val group: String, + val prefix: String, + val content: String, + val delay: Short, + val selectable: Boolean, + val active: Boolean + ) + var onSelectProxyListener: suspend (String, String) -> Unit - suspend fun applyChange() + suspend fun applyChange(newList: List) suspend fun getGroupPosition(name: String): Int? suspend fun getCurrentGroup(): String } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt index 465b4826fc..cf9aeedc58 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt @@ -12,11 +12,11 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import com.github.kr328.clash.ProxiesActivity import com.github.kr328.clash.R -import com.github.kr328.clash.core.model.Proxy -import com.github.kr328.clash.core.model.ProxyGroup +import com.github.kr328.clash.core.utils.Log import com.google.android.material.card.MaterialCardView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext @@ -27,20 +27,22 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) val group: String } - private data class ProxyGroupInfo(override val name: String) : RenderInfo { + private data class ProxyGroupRenderInfo(val info: AbstractProxyAdapter.ProxyGroupInfo) : + RenderInfo { + override val name: String + get() = info.name override val group: String - get() = name + get() = info.name } - private data class ProxyInfo( - override val name: String, - val type: Proxy.Type, - override val group: String, - val selectable: Boolean, - val delay: Short, - val active: Boolean - ) : RenderInfo + private data class ProxyRenderInfo(val info: AbstractProxyAdapter.ProxyInfo) : RenderInfo { + override val name: String + get() = info.name + override val group: String + get() = info.group + } + private var rootMutex = Mutex() private var renderList = emptyList() @ColorInt private val colorSurface: Int @@ -61,15 +63,15 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return when (renderList[position]) { - is ProxyGroupInfo -> spanCount - is ProxyInfo -> 1 + is ProxyGroupRenderInfo -> spanCount + is ProxyRenderInfo -> 1 else -> throw IllegalArgumentException() } } } } - override var root = listOf() + private var root = listOf() override var onSelectProxyListener: suspend (String, String) -> Unit = { _, _ -> } private class ProxyGroupHeader(view: View) : RecyclerView.ViewHolder(view) { @@ -79,50 +81,47 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) private class ProxyItem(view: View) : RecyclerView.ViewHolder(view) { val root: MaterialCardView = view.findViewById(R.id.root) - val name: TextView = view.findViewById(R.id.name) - val type: TextView = view.findViewById(R.id.type) + val prefix: TextView = view.findViewById(R.id.prefix) + val content: TextView = view.findViewById(R.id.content) val delay: TextView = view.findViewById(R.id.delay) } - override suspend fun applyChange() = withContext(Dispatchers.Default) { - val newRenderList = root - .flatMap { - listOf(ProxyGroupInfo(it.name)) + it.proxies.map { p -> - ProxyInfo( - p.name, - p.type, - it.name, - it.type == Proxy.Type.SELECT, - p.delay.toShort(), - it.current == p.name - ) + override suspend fun applyChange(newList: List) = + withContext(Dispatchers.Default) { + rootMutex.lock() + + val newRenderList = newList + .flatMap { + listOf(ProxyGroupRenderInfo(it)) + it.proxies.map { p -> ProxyRenderInfo(p) } } - } - val oldRenderList = renderList + val oldRenderList = renderList - val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldRenderList[oldItemPosition]::class == newRenderList[newItemPosition]::class && - oldRenderList[oldItemPosition].name == newRenderList[newItemPosition].name + val result = DiffUtil.calculateDiff(object : DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldRenderList[oldItemPosition]::class == newRenderList[newItemPosition]::class && + oldRenderList[oldItemPosition].name == newRenderList[newItemPosition].name - override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = - oldRenderList[oldItemPosition] == newRenderList[newItemPosition] + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) = + oldRenderList[oldItemPosition] == newRenderList[newItemPosition] - override fun getOldListSize(): Int = oldRenderList.size - override fun getNewListSize(): Int = newRenderList.size - }) + override fun getOldListSize(): Int = oldRenderList.size + override fun getNewListSize(): Int = newRenderList.size + }) - withContext(Dispatchers.Main) { - renderList = newRenderList - result.dispatchUpdatesTo(this@GridProxyAdapter) + withContext(Dispatchers.Main) { + root = newList + renderList = newRenderList + result.dispatchUpdatesTo(this@GridProxyAdapter) + } + + rootMutex.unlock() } - } override suspend fun getGroupPosition(name: String): Int? { return withContext(Dispatchers.Default) { renderList.mapIndexed { index, p -> - if (p is ProxyGroupInfo && p.name == name) + if (p is ProxyGroupRenderInfo && p.name == name) index else -1 @@ -158,49 +157,55 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { when (holder) { is ProxyGroupHeader -> { - val current = renderList[position] as ProxyGroupInfo + val current = renderList[position] as ProxyGroupRenderInfo - holder.name.text = current.name + holder.name.text = current.info.name holder.urlTest.setOnClickListener { } } is ProxyItem -> { - val current = renderList[position] as ProxyInfo + val current = renderList[position] as ProxyRenderInfo - holder.name.text = current.name - holder.type.text = current.type.toString() + holder.prefix.text = current.info.prefix + holder.content.text = current.info.content - if (current.delay > 0) - holder.delay.text = current.delay.toString() + if (current.info.delay > 0) + holder.delay.text = current.info.delay.toString() else - holder.delay.text = "N/A" + holder.delay.text = "" - if (current.active) { - holder.name.setTextColor(Color.WHITE) - holder.type.setTextColor(Color.WHITE) + if (current.info.active) { + holder.prefix.setTextColor(Color.WHITE) + holder.content.setTextColor(Color.WHITE) holder.delay.setTextColor(Color.WHITE) holder.root.setCardBackgroundColor(context.getColor(R.color.primaryCardColorStarted)) } else { - holder.name.setTextColor(colorOnSurface) - holder.type.setTextColor(colorOnSurface) + holder.prefix.setTextColor(colorOnSurface) + holder.content.setTextColor(colorOnSurface) holder.delay.setTextColor(colorOnSurface) holder.root.setCardBackgroundColor(colorSurface) } - if (current.selectable) { + if (current.info.selectable) { holder.root.setOnClickListener { - root = root.map { - if (it.name == current.group) { - it.copy(current = current.name) - } else { - it + context.launch { + rootMutex.lock() + val n = withContext(Dispatchers.Default) { + root.map { + if (it.name == current.group) { + it.copy(proxies = it.proxies.map { p -> + p.copy(active = p.name == current.name) + }) + } else { + it + } + } } - } + rootMutex.unlock() - context.launch { - applyChange() + applyChange(n) onSelectProxyListener(current.group, current.name) } @@ -219,8 +224,8 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) override fun getItemCount(): Int = renderList.size override fun getItemViewType(position: Int): Int { return when (renderList[position]) { - is ProxyGroupInfo -> 1 - is ProxyInfo -> 2 + is ProxyGroupRenderInfo -> 1 + is ProxyRenderInfo -> 2 else -> throw IllegalArgumentException() } } diff --git a/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt b/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt index a894e3423d..316a8c2cc7 100644 --- a/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt +++ b/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt @@ -15,7 +15,8 @@ class UiPreferences(context: Context) { val PROXY_GROUP_SORT = StringEntry("proxy_group_sort", PROXY_SORT_DEFAULT) val PROXY_PROXY_SORT = StringEntry("proxy_proxy_sort", PROXY_SORT_DEFAULT) - val LAST_SELECT_GROUP = StringEntry("last_select_group", "") + val PROXY_LAST_SELECT_GROUP = StringEntry("proxy_last_select_group", "") + val PROXY_MERGE_PREFIX = BooleanEntry("proxy_merge_prefix", true) } interface Entry { @@ -34,6 +35,17 @@ class UiPreferences(context: Context) { } } + class BooleanEntry(private val key: String, private val defaultValue: Boolean = false): + Entry { + override fun get(sharedPreferences: SharedPreferences): Boolean { + return sharedPreferences.getBoolean(key, defaultValue) + } + + override fun put(editor: SharedPreferences.Editor, value: Boolean) { + editor.putBoolean(key, value) + } + } + class Editor(private val editor: SharedPreferences.Editor) { fun > put(e: E, value: T) { e.put(editor, value) diff --git a/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt b/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt new file mode 100644 index 0000000000..60d92face7 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt @@ -0,0 +1,66 @@ +package com.github.kr328.clash.utils + +import com.github.kr328.clash.core.utils.Log +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +object PrefixMerger { + private val REGEX_PREFIX_TRIM = Regex("[-]*$") + + data class Result(val prefix: String, val content: String, val value: T) + + suspend fun merge(values: List, transform: (T) -> String): List> = + withContext(Dispatchers.Default) { + val pairs = values.map { + transform(it).trim() to it + } + + val groups = mutableListOf>>() + var mergingGroup = mutableListOf>() + var currentChar: Char = 0.toChar() + val result = mutableListOf>() + + for (pair in pairs) { + if (pair.first[0] == currentChar) { + mergingGroup.add(pair) + } else { + if (mergingGroup.isNotEmpty()) { + groups.add(mergingGroup) + mergingGroup = mutableListOf() + } + + currentChar = pair.first[0] + mergingGroup.add(pair) + } + } + + if ( mergingGroup.isNotEmpty() ) + groups.add(mergingGroup) + + for (group in groups) { + var diffIndex = 0 + + diff@ for (charIndex in group[0].first.indices) { + for (stringIndex in 0 until (group.size - 1)) { + if (group[stringIndex].first[charIndex] != group[stringIndex + 1].first[charIndex]) + break@diff + } + + diffIndex++ + } + + group.forEach { + result.add( + Result( + it.first.substring(0, diffIndex) + .replace(REGEX_PREFIX_TRIM, ""), + it.first.substring(diffIndex), + it.second + ) + ) + } + } + + result + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_grid_proxy.xml b/app/src/main/res/layout/adapter_grid_proxy.xml index 4323479b5f..9567198de4 100644 --- a/app/src/main/res/layout/adapter_grid_proxy.xml +++ b/app/src/main/res/layout/adapter_grid_proxy.xml @@ -19,7 +19,7 @@ android:layout_height="wrap_content"> + + + + + + + \ 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 be9f96d91c..31c4f93ad3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,6 +84,8 @@ Sort Group Sort Proxy Default + Utils + Merge Prefix Start URL Provider Failure From a5064fdd4f1b230801adf767564b64020262bdbd Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 17 Feb 2020 00:16:41 +0800 Subject: [PATCH 087/358] proxy activity refactored --- app/src/main/ic_launcher-web.png | Bin 17440 -> 17100 bytes .../com/github/kr328/clash/BaseActivity.kt | 5 +- .../com/github/kr328/clash/ProxiesActivity.kt | 158 ++++++++++++------ .../clash/adapter/AbstractProxyAdapter.kt | 23 --- .../{GridProxyAdapter.kt => ProxyAdapter.kt} | 105 +++++++----- .../kr328/clash/preference/UiPreferences.kt | 6 +- .../github/kr328/clash/remote/ClashClient.kt | 5 +- .../github/kr328/clash/utils/PrefixMerger.kt | 3 +- .../github/kr328/clash/utils/ProxySorter.kt | 45 ++--- .../github/kr328/clash/utils/ScrollBinding.kt | 74 ++++++++ .../kr328/clash/view/ProxiesTabMediator.kt | 111 ------------ app/src/main/res/anim/simple_alpha.xml | 5 - .../main/res/color/proxies_chip_colors.xml | 5 - .../res/color/proxies_chip_text_colors.xml | 5 - app/src/main/res/drawable/ic_boot.xml | 9 - app/src/main/res/drawable/ic_cloud.xml | 9 - app/src/main/res/drawable/ic_delete_sweep.xml | 9 - app/src/main/res/drawable/ic_expand_less.xml | 9 - app/src/main/res/drawable/ic_expand_more.xml | 9 - app/src/main/res/drawable/ic_filter.xml | 9 - app/src/main/res/drawable/ic_input.xml | 9 - app/src/main/res/drawable/ic_label.xml | 9 - .../res/drawable/ic_launcher_foreground.xml | 12 +- app/src/main/res/drawable/ic_link.xml | 9 - app/src/main/res/drawable/ic_logo.xml | 12 +- app/src/main/res/drawable/ic_profile_edit.xml | 9 - .../main/res/drawable/ic_profile_refresh.xml | 9 - app/src/main/res/drawable/ic_refresh.xml | 9 - .../main/res/drawable/ic_settings_color.xml | 9 - app/src/main/res/drawable/ic_sync.xml | 9 - app/src/main/res/layout/activity_feedback.xml | 23 --- app/src/main/res/layout/activity_logs.xml | 22 --- app/src/main/res/layout/activity_main.xml | 3 - app/src/main/res/layout/activity_profiles.xml | 1 - .../res/layout/activity_setting_access.xml | 64 ------- .../layout/activity_setting_application.xml | 23 --- .../main/res/layout/activity_setting_main.xml | 23 --- .../res/layout/activity_setting_proxy.xml | 47 ------ .../main/res/layout/adapter_access_app.xml | 50 ------ .../res/layout/adapter_grid_proxy_group.xml | 27 ++- app/src/main/res/layout/adapter_log.xml | 29 ---- app/src/main/res/layout/dialog_about.xml | 30 ---- .../res/layout/dialog_profile_updating.xml | 15 -- app/src/main/res/layout/dialog_text_edit.xml | 16 -- app/src/main/res/layout/page_proxies.xml | 8 - .../main/res/layout/preference_main_item.xml | 29 ---- .../main/res/layout/preference_proxy_item.xml | 40 ----- app/src/main/res/layout/view_fat_item.xml | 61 ------- .../main/res/layout/view_radio_fat_item.xml | 71 -------- app/src/main/res/layout/view_title.xml | 7 - app/src/main/res/menu/menu_setting_access.xml | 16 -- app/src/main/res/mipmap-hdpi/ic_launcher.png | Bin 1800 -> 1770 bytes .../res/mipmap-hdpi/ic_launcher_round.png | Bin 3657 -> 3649 bytes app/src/main/res/mipmap-mdpi/ic_launcher.png | Bin 1225 -> 1217 bytes .../res/mipmap-mdpi/ic_launcher_round.png | Bin 2320 -> 2284 bytes app/src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 2429 -> 2460 bytes .../res/mipmap-xhdpi/ic_launcher_round.png | Bin 5197 -> 5166 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 3760 -> 3730 bytes .../res/mipmap-xxhdpi/ic_launcher_round.png | Bin 8074 -> 8151 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 5240 -> 5206 bytes .../res/mipmap-xxxhdpi/ic_launcher_round.png | Bin 11658 -> 11679 bytes app/src/main/res/values/colors.xml | 2 - app/src/main/res/values/strings.xml | 99 +---------- app/src/main/res/xml/feedback.xml | 20 --- app/src/main/res/xml/setting_application.xml | 11 -- app/src/main/res/xml/setting_main.xml | 19 --- app/src/main/res/xml/setting_proxy.xml | 27 --- .../main/res/drawable/ic_demo_drawable.xml | 9 - design/src/main/res/values/strings.xml | 1 - .../clash/service/ProfileBackgroundService.kt | 12 +- .../main/res/drawable/ic_update_failure.xml | 9 - .../main/res/drawable/ic_update_normal.xml | 9 - service/src/main/res/drawable/ic_updating.xml | 9 - service/src/main/res/values/strings.xml | 7 - 74 files changed, 322 insertions(+), 1217 deletions(-) delete mode 100644 app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt rename app/src/main/java/com/github/kr328/clash/adapter/{GridProxyAdapter.kt => ProxyAdapter.kt} (70%) create mode 100644 app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt delete mode 100644 app/src/main/res/anim/simple_alpha.xml delete mode 100644 app/src/main/res/color/proxies_chip_colors.xml delete mode 100644 app/src/main/res/color/proxies_chip_text_colors.xml delete mode 100644 app/src/main/res/drawable/ic_boot.xml delete mode 100644 app/src/main/res/drawable/ic_cloud.xml delete mode 100644 app/src/main/res/drawable/ic_delete_sweep.xml delete mode 100644 app/src/main/res/drawable/ic_expand_less.xml delete mode 100644 app/src/main/res/drawable/ic_expand_more.xml delete mode 100644 app/src/main/res/drawable/ic_filter.xml delete mode 100644 app/src/main/res/drawable/ic_input.xml delete mode 100644 app/src/main/res/drawable/ic_label.xml delete mode 100644 app/src/main/res/drawable/ic_link.xml delete mode 100644 app/src/main/res/drawable/ic_profile_edit.xml delete mode 100644 app/src/main/res/drawable/ic_profile_refresh.xml delete mode 100644 app/src/main/res/drawable/ic_refresh.xml delete mode 100644 app/src/main/res/drawable/ic_settings_color.xml delete mode 100644 app/src/main/res/drawable/ic_sync.xml delete mode 100644 app/src/main/res/layout/activity_feedback.xml delete mode 100644 app/src/main/res/layout/activity_logs.xml delete mode 100644 app/src/main/res/layout/activity_setting_access.xml delete mode 100644 app/src/main/res/layout/activity_setting_application.xml delete mode 100644 app/src/main/res/layout/activity_setting_main.xml delete mode 100644 app/src/main/res/layout/activity_setting_proxy.xml delete mode 100644 app/src/main/res/layout/adapter_access_app.xml delete mode 100644 app/src/main/res/layout/adapter_log.xml delete mode 100644 app/src/main/res/layout/dialog_about.xml delete mode 100644 app/src/main/res/layout/dialog_profile_updating.xml delete mode 100644 app/src/main/res/layout/dialog_text_edit.xml delete mode 100644 app/src/main/res/layout/page_proxies.xml delete mode 100644 app/src/main/res/layout/preference_main_item.xml delete mode 100644 app/src/main/res/layout/preference_proxy_item.xml delete mode 100644 app/src/main/res/layout/view_fat_item.xml delete mode 100644 app/src/main/res/layout/view_radio_fat_item.xml delete mode 100644 app/src/main/res/layout/view_title.xml delete mode 100644 app/src/main/res/menu/menu_setting_access.xml delete mode 100644 app/src/main/res/xml/feedback.xml delete mode 100644 app/src/main/res/xml/setting_application.xml delete mode 100644 app/src/main/res/xml/setting_main.xml delete mode 100644 app/src/main/res/xml/setting_proxy.xml delete mode 100644 design/src/main/res/drawable/ic_demo_drawable.xml delete mode 100644 service/src/main/res/drawable/ic_update_failure.xml delete mode 100644 service/src/main/res/drawable/ic_update_normal.xml delete mode 100644 service/src/main/res/drawable/ic_updating.xml diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png index 45ffbf65e3bc8f556600738e680013683681d66b..6940fc4ca951a534c725e4dda83d52918bdc65fb 100644 GIT binary patch literal 17100 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 MYuqWkZ5r_Z0258$&;S4c diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 2bfdb210d8..6d7a43e275 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -12,7 +12,6 @@ import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout -import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.preference.UiPreferences import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.service.data.ClashProfileEntity @@ -108,10 +107,10 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() } override fun onOptionsItemSelected(item: MenuItem): Boolean { - if ( super.onOptionsItemSelected(item) ) + if (super.onOptionsItemSelected(item)) return true - if ( item.itemId == android.R.id.home ) { + if (item.itemId == android.R.id.home) { onSupportNavigateUp() return true } diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index c069564885..41cd780fac 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -4,8 +4,9 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.recyclerview.widget.LinearLayoutManager -import com.github.kr328.clash.adapter.AbstractProxyAdapter -import com.github.kr328.clash.adapter.GridProxyAdapter +import androidx.recyclerview.widget.LinearSmoothScroller +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.adapter.ProxyAdapter import com.github.kr328.clash.adapter.ProxyChipAdapter import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.Proxy @@ -13,51 +14,74 @@ import com.github.kr328.clash.preference.UiPreferences import com.github.kr328.clash.remote.withClash import com.github.kr328.clash.utils.PrefixMerger import com.github.kr328.clash.utils.ProxySorter -import com.github.kr328.clash.view.ProxiesTabMediator +import com.github.kr328.clash.utils.ScrollBinding import kotlinx.android.synthetic.main.activity_proxies.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -class ProxiesActivity : BaseActivity() { - private lateinit var mediator: ProxiesTabMediator +class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { + private val scrollBinding = ScrollBinding(this, this) private val doScrollToLastProxy by lazy { val selected = uiPreference.get(UiPreferences.PROXY_LAST_SELECT_GROUP) launch { - mediator.scrollToDirect(selected) + scrollBinding.scrollMaster(selected) } } + private val mainListAdapter: ProxyAdapter + get() = mainList.adapter as ProxyAdapter + private val chipListAdapter: ProxyChipAdapter + get() = chipList.adapter as ProxyChipAdapter + private val urlTesting: MutableSet = mutableSetOf() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_proxies) setSupportActionBar(toolbar) - mediator = ProxiesTabMediator(this, mainList, chipList) - - GridProxyAdapter(this).apply { - mainList.layoutManager = layoutManager - mainList.adapter = this + mainList.adapter = ProxyAdapter(this, { group, proxy -> + launch { + withClash { + setSelectProxy(group, proxy) + } + } + }, { + launch { + urlTesting.add(it) - onSelectProxyListener = { group, name -> withClash { - setSelectProxy(group, name) + urlTesting.add(it) + + startHealthCheck(it) + + urlTesting.remove(it) + + refreshList() } } - } + }) + + mainList.layoutManager = mainListAdapter.layoutManager chipList.adapter = ProxyChipAdapter(this) { launch { - mediator.scrollTo(it) + scrollBinding.scrollMaster(it) } } chipList.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false) chipList.itemAnimator?.changeDuration = 0 launch { - mediator.exec() + mainList.addOnScrollListener(object: RecyclerView.OnScrollListener(){ + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + scrollBinding.sendMasterScrolled() + } + }) + + scrollBinding.exec() } refreshList() @@ -214,57 +238,62 @@ class ProxiesActivity : BaseActivity() { queryAllProxyGroups() } - val prefix = if (uiPreference.get(UiPreferences.PROXY_MERGE_PREFIX)) { - proxies.map { - async { PrefixMerger.merge(it.proxies.map { p -> it.name to p.name }) { it.second } } - }.flatMap { - it.await() - }.map { - it.value to it - }.toMap() - } else emptyMap() - - val groupSort = when (uiPreference.get(UiPreferences.PROXY_GROUP_SORT)) { - UiPreferences.PROXY_SORT_DEFAULT -> - ProxySorter.Order.DEFAULT - UiPreferences.PROXY_SORT_NAME -> - ProxySorter.Order.NAME_INCREASE - UiPreferences.PROXY_SORT_DELAY -> - ProxySorter.Order.DELAY_INCREASE - else -> throw IllegalArgumentException() + val prefixDeferred = async { + if (uiPreference.get(UiPreferences.PROXY_MERGE_PREFIX)) { + proxies.map { + async { PrefixMerger.merge(it.proxies.map { p -> it.name to p.name }) { it.second } } + }.flatMap { + it.await() + }.map { + it.value to it + }.toMap() + } else emptyMap() } - val proxySort = when (uiPreference.get(UiPreferences.PROXY_PROXY_SORT)) { - UiPreferences.PROXY_SORT_DEFAULT -> - ProxySorter.Order.DEFAULT - UiPreferences.PROXY_SORT_NAME -> - ProxySorter.Order.NAME_INCREASE - UiPreferences.PROXY_SORT_DELAY -> - ProxySorter.Order.DELAY_INCREASE - else -> throw IllegalArgumentException() - } + val sortDeferred = async { + val groupSort = when (uiPreference.get(UiPreferences.PROXY_GROUP_SORT)) { + UiPreferences.PROXY_SORT_DEFAULT -> + ProxySorter.Order.DEFAULT + UiPreferences.PROXY_SORT_NAME -> + ProxySorter.Order.NAME_INCREASE + UiPreferences.PROXY_SORT_DELAY -> + ProxySorter.Order.DELAY_INCREASE + else -> throw IllegalArgumentException() + } + + val proxySort = when (uiPreference.get(UiPreferences.PROXY_PROXY_SORT)) { + UiPreferences.PROXY_SORT_DEFAULT -> + ProxySorter.Order.DEFAULT + UiPreferences.PROXY_SORT_NAME -> + ProxySorter.Order.NAME_INCREASE + UiPreferences.PROXY_SORT_DELAY -> + ProxySorter.Order.DELAY_INCREASE + else -> throw IllegalArgumentException() + } - val sorter = ProxySorter(groupSort, proxySort) + val sorter = ProxySorter(groupSort, proxySort) - val sorted = withContext(Dispatchers.Default) { - sorter.sort(proxies) - }.run { - when (general.mode) { - General.Mode.GLOBAL -> this - General.Mode.DIRECT -> emptyList() - General.Mode.RULE -> this.filter { it.name != "GLOBAL" } + sorter.sort(proxies).run { + when (general.mode) { + General.Mode.GLOBAL -> this + General.Mode.DIRECT -> emptyList() + General.Mode.RULE -> this.filter { it.name != "GLOBAL" } + } } } + val prefix = prefixDeferred.await() + val sorted = sortDeferred.await() + val newList = withContext(Dispatchers.Default) { sorted.map { - AbstractProxyAdapter.ProxyGroupInfo(it.name, + ProxyAdapter.ProxyGroupInfo(it.name, it.proxies.map { p -> val r = prefix.getOrElse(it.name to p.name) { PrefixMerger.Result(p.name, "", p) } - AbstractProxyAdapter.ProxyInfo( + ProxyAdapter.ProxyInfo( p.name, it.name, r.prefix, @@ -278,7 +307,7 @@ class ProxiesActivity : BaseActivity() { } } - (mainList.adapter!! as AbstractProxyAdapter).applyChange(newList) + mainListAdapter.applyChange(newList, urlTesting) (chipList.adapter!! as ProxyChipAdapter).apply { chips = sorted.map { it.name } @@ -288,4 +317,25 @@ class ProxiesActivity : BaseActivity() { doScrollToLastProxy } } + + override suspend fun getCurrentMasterToken(): String { + return mainListAdapter.getCurrentGroup() + } + + override suspend fun onMasterTokenChanged(token: String) { + val position = chipListAdapter.chips.indexOf(token) + + if (position < 0) + return + + chipList.scrollToPosition(position) + } + + override suspend fun getMasterTokenPosition(token: String): Int { + return mainListAdapter.getGroupPosition(token) + } + + override suspend fun doMasterScroll(scroller: LinearSmoothScroller) { + mainListAdapter.layoutManager.startSmoothScroll(scroller) + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt deleted file mode 100644 index 1232d209b3..0000000000 --- a/app/src/main/java/com/github/kr328/clash/adapter/AbstractProxyAdapter.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.kr328.clash.adapter - -interface AbstractProxyAdapter { - data class ProxyGroupInfo( - val name: String, - val proxies: List - ) - data class ProxyInfo( - val name: String, - val group: String, - val prefix: String, - val content: String, - val delay: Short, - val selectable: Boolean, - val active: Boolean - ) - - var onSelectProxyListener: suspend (String, String) -> Unit - - suspend fun applyChange(newList: List) - suspend fun getGroupPosition(name: String): Int? - suspend fun getCurrentGroup(): String -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt similarity index 70% rename from app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt rename to app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt index cf9aeedc58..a4ea2a963b 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/GridProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt @@ -1,5 +1,6 @@ package com.github.kr328.clash.adapter +import android.content.Context import android.graphics.Color import android.util.TypedValue import android.view.LayoutInflater @@ -10,24 +11,43 @@ import androidx.annotation.ColorInt import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView -import com.github.kr328.clash.ProxiesActivity import com.github.kr328.clash.R -import com.github.kr328.clash.core.utils.Log import com.google.android.material.card.MaterialCardView import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.withContext -class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) : - RecyclerView.Adapter(), AbstractProxyAdapter { - private interface RenderInfo { +class ProxyAdapter( + private val context: Context, + val onSelect: (String, String) -> Unit, + val onUrlTest: (String) -> Unit +): RecyclerView.Adapter() { + companion object { + const val DEFAULT_SPAN_COUNT = 2 + } + + data class ProxyGroupInfo( + val name: String, + val proxies: List + ) + + data class ProxyInfo( + val name: String, + val group: String, + val prefix: String, + val content: String, + val delay: Short, + val selectable: Boolean, + val active: Boolean + ) + + interface RenderInfo { val name: String val group: String } - private data class ProxyGroupRenderInfo(val info: AbstractProxyAdapter.ProxyGroupInfo) : + private data class ProxyGroupRenderInfo(val info: ProxyGroupInfo) : RenderInfo { override val name: String get() = info.name @@ -35,15 +55,18 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) get() = info.name } - private data class ProxyRenderInfo(val info: AbstractProxyAdapter.ProxyInfo) : RenderInfo { + private data class ProxyRenderInfo(val info: ProxyInfo) : RenderInfo { override val name: String get() = info.name override val group: String get() = info.group } + private var rootMutex = Mutex() - private var renderList = emptyList() + private var urlTesting: Set = emptySet() + private var renderList = mutableListOf() + private val activeList: MutableMap = mutableMapOf() @ColorInt private val colorSurface: Int @ColorInt @@ -59,7 +82,7 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) colorOnSurface = typedValue.data } - val layoutManager = GridLayoutManager(context, spanCount).apply { + val layoutManager = GridLayoutManager(context, DEFAULT_SPAN_COUNT).apply { spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { return when (renderList[position]) { @@ -71,12 +94,12 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) } } - private var root = listOf() - override var onSelectProxyListener: suspend (String, String) -> Unit = { _, _ -> } + private var root = listOf() private class ProxyGroupHeader(view: View) : RecyclerView.ViewHolder(view) { val name: TextView = view.findViewById(R.id.name) val urlTest: View = view.findViewById(R.id.urlTest) + val urlTestProgress: View = view.findViewById(R.id.urlTestProgress) } private class ProxyItem(view: View) : RecyclerView.ViewHolder(view) { @@ -86,7 +109,7 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) val delay: TextView = view.findViewById(R.id.delay) } - override suspend fun applyChange(newList: List) = + suspend fun applyChange(newList: List, testing: Set) = withContext(Dispatchers.Default) { rootMutex.lock() @@ -111,25 +134,26 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) withContext(Dispatchers.Main) { root = newList - renderList = newRenderList - result.dispatchUpdatesTo(this@GridProxyAdapter) + renderList = newRenderList.toMutableList() + urlTesting = testing + result.dispatchUpdatesTo(this@ProxyAdapter) } rootMutex.unlock() } - override suspend fun getGroupPosition(name: String): Int? { + suspend fun getGroupPosition(name: String): Int { return withContext(Dispatchers.Default) { renderList.mapIndexed { index, p -> if (p is ProxyGroupRenderInfo && p.name == name) index else -1 - }.singleOrNull { it >= 0 } + }.singleOrNull() ?: -1 } } - override suspend fun getCurrentGroup(): String { + fun getCurrentGroup(): String { val position = layoutManager.findFirstCompletelyVisibleItemPosition() if (position < 0) @@ -161,7 +185,19 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) holder.name.text = current.info.name holder.urlTest.setOnClickListener { + holder.urlTest.visibility = View.GONE + holder.urlTestProgress.visibility = View.VISIBLE + onUrlTest(current.name) + } + + if ( urlTesting.contains(current.name) ) { + holder.urlTest.visibility = View.GONE + holder.urlTestProgress.visibility = View.VISIBLE + } + else { + holder.urlTest.visibility = View.VISIBLE + holder.urlTestProgress.visibility = View.GONE } } is ProxyItem -> { @@ -170,13 +206,14 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) holder.prefix.text = current.info.prefix holder.content.text = current.info.content - if (current.info.delay > 0) holder.delay.text = current.info.delay.toString() else holder.delay.text = "" if (current.info.active) { + activeList[current.group] = position + holder.prefix.setTextColor(Color.WHITE) holder.content.setTextColor(Color.WHITE) holder.delay.setTextColor(Color.WHITE) @@ -190,25 +227,17 @@ class GridProxyAdapter(private val context: ProxiesActivity, spanCount: Int = 2) if (current.info.selectable) { holder.root.setOnClickListener { - context.launch { - rootMutex.lock() - val n = withContext(Dispatchers.Default) { - root.map { - if (it.name == current.group) { - it.copy(proxies = it.proxies.map { p -> - p.copy(active = p.name == current.name) - }) - } else { - it - } - } - } - rootMutex.unlock() - - applyChange(n) - - onSelectProxyListener(current.group, current.name) - } + val oldPosition = activeList[current.group] ?: return@setOnClickListener + val old = renderList[oldPosition] as ProxyRenderInfo + val new = renderList[position] as ProxyRenderInfo + + renderList[oldPosition] = old.copy(info = old.info.copy(active = false)) + renderList[position] = new.copy(info = new.info.copy(active = true)) + + notifyItemChanged(oldPosition) + notifyItemChanged(position) + + onSelect(current.group, current.name) } holder.root.isClickable = true holder.root.isFocusable = true diff --git a/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt b/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt index 316a8c2cc7..ea5618a05d 100644 --- a/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt +++ b/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt @@ -2,8 +2,6 @@ package com.github.kr328.clash.preference import android.content.Context import android.content.SharedPreferences -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext class UiPreferences(context: Context) { companion object { @@ -35,8 +33,8 @@ class UiPreferences(context: Context) { } } - class BooleanEntry(private val key: String, private val defaultValue: Boolean = false): - Entry { + class BooleanEntry(private val key: String, private val defaultValue: Boolean = false) : + Entry { override fun get(sharedPreferences: SharedPreferences): Boolean { return sharedPreferences.getBoolean(key, defaultValue) } diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt index d56db3a881..93ac66039d 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -5,6 +5,7 @@ import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.ProxyGroup import com.github.kr328.clash.service.IClashManager import com.github.kr328.clash.service.ipc.IStreamCallback +import com.github.kr328.clash.service.ipc.ParcelableContainer import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -16,7 +17,7 @@ class ClashClient(private val service: IClashManager) { suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) { CompletableDeferred().apply { - service.startHealthCheck(group, object : IStreamCallback.Default() { + service.startHealthCheck(group, object: IStreamCallback.Stub() { override fun complete() { this@apply.complete(Unit) } @@ -24,6 +25,8 @@ class ClashClient(private val service: IClashManager) { override fun completeExceptionally(reason: String?) { this@apply.completeExceptionally(RemoteException(reason)) } + + override fun send(data: ParcelableContainer?) {} }) } }.await() diff --git a/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt b/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt index 60d92face7..172da16e8a 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/PrefixMerger.kt @@ -1,6 +1,5 @@ package com.github.kr328.clash.utils -import com.github.kr328.clash.core.utils.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -34,7 +33,7 @@ object PrefixMerger { } } - if ( mergingGroup.isNotEmpty() ) + if (mergingGroup.isNotEmpty()) groups.add(mergingGroup) for (group in groups) { diff --git a/app/src/main/java/com/github/kr328/clash/utils/ProxySorter.kt b/app/src/main/java/com/github/kr328/clash/utils/ProxySorter.kt index f6ba14fe8b..ae2ffdc706 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/ProxySorter.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/ProxySorter.kt @@ -2,37 +2,40 @@ package com.github.kr328.clash.utils import com.github.kr328.clash.core.model.Proxy import com.github.kr328.clash.core.model.ProxyGroup +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext class ProxySorter(val groupOrder: Order, val proxyOrder: Order) { enum class Order { DEFAULT, DELAY_INCREASE, DELAY_DECREASE, NAME_INCREASE, NAME_DECREASE } - fun sort(proxyGroup: List): List { - val global = proxyGroup.singleOrNull { - it.name == "GLOBAL" - } - - val sortedGroup = when (groupOrder) { - Order.DEFAULT -> groupSortWithDefault(global, proxyGroup) - Order.DELAY_INCREASE -> groupSortWithDelay(true, proxyGroup) - Order.DELAY_DECREASE -> groupSortWithDelay(false, proxyGroup) - Order.NAME_INCREASE -> groupSortWithName(true, proxyGroup) - Order.NAME_DECREASE -> groupSortWithName(false, proxyGroup) - } + suspend fun sort(proxyGroup: List): List = + withContext(Dispatchers.Default) { + val global = proxyGroup.singleOrNull { + it.name == "GLOBAL" + } - return sortedGroup.map { - val sortedProxy = when (proxyOrder) { - Order.DEFAULT -> it.proxies - Order.DELAY_INCREASE -> proxySortWithDelay(true, it.proxies) - Order.DELAY_DECREASE -> proxySortWithDelay(false, it.proxies) - Order.NAME_INCREASE -> proxySortWithName(true, it.proxies) - Order.NAME_DECREASE -> proxySortWithName(false, it.proxies) + val sortedGroup = when (groupOrder) { + Order.DEFAULT -> groupSortWithDefault(global, proxyGroup) + Order.DELAY_INCREASE -> groupSortWithDelay(true, proxyGroup) + Order.DELAY_DECREASE -> groupSortWithDelay(false, proxyGroup) + Order.NAME_INCREASE -> groupSortWithName(true, proxyGroup) + Order.NAME_DECREASE -> groupSortWithName(false, proxyGroup) } - it.copy(proxies = sortedProxy) + sortedGroup.map { + val sortedProxy = when (proxyOrder) { + Order.DEFAULT -> it.proxies + Order.DELAY_INCREASE -> proxySortWithDelay(true, it.proxies) + Order.DELAY_DECREASE -> proxySortWithDelay(false, it.proxies) + Order.NAME_INCREASE -> proxySortWithName(true, it.proxies) + Order.NAME_DECREASE -> proxySortWithName(false, it.proxies) + } + + it.copy(proxies = sortedProxy) + } } - } private fun groupSortWithDefault( global: ProxyGroup?, diff --git a/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt b/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt new file mode 100644 index 0000000000..a239d6c74e --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt @@ -0,0 +1,74 @@ +package com.github.kr328.clash.utils + +import android.content.Context +import androidx.recyclerview.widget.LinearSmoothScroller +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.delay + +class ScrollBinding( + private val context: Context, + private val callback: Callback +) { + interface Callback { + suspend fun getCurrentMasterToken(): String + suspend fun onMasterTokenChanged(token: String) + suspend fun getMasterTokenPosition(token: String): Int + suspend fun doMasterScroll(scroller: LinearSmoothScroller) + } + + private val updateChannel = Channel(Channel.CONFLATED) + private var preventSlaveScroll = false + + fun sendMasterScrolled() { + updateChannel.offer(Unit) + } + + suspend fun scrollMaster(token: String) { + val position = callback.getMasterTokenPosition(token) + + if (position < 0) + return + + val scroller = (object : LinearSmoothScroller(context) { + override fun getVerticalSnapPreference(): Int { + return SNAP_TO_START + } + + override fun onStop() { + super.onStop() + + preventSlaveScroll = false + } + + override fun onStart() { + super.onStart() + + preventSlaveScroll = true + } + + init { + targetPosition = position + } + }) + + callback.doMasterScroll(scroller) + } + + suspend fun exec() { + var lastToken: String? = null + + while (true) { + updateChannel.receive() + + val currentToken = callback.getCurrentMasterToken() + if (lastToken == currentToken) + continue + + lastToken = currentToken + + callback.onMasterTokenChanged(currentToken) + + delay(200) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt b/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt deleted file mode 100644 index 644dd8c192..0000000000 --- a/app/src/main/java/com/github/kr328/clash/view/ProxiesTabMediator.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.github.kr328.clash.view - -import android.util.DisplayMetrics -import androidx.recyclerview.widget.GridLayoutManager -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.LinearSmoothScroller -import androidx.recyclerview.widget.RecyclerView -import com.github.kr328.clash.ProxiesActivity -import com.github.kr328.clash.adapter.AbstractProxyAdapter -import com.github.kr328.clash.adapter.ProxyChipAdapter -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay - -class ProxiesTabMediator( - private val context: ProxiesActivity, - private val proxiesView: RecyclerView, - private val chipView: RecyclerView -) { - private var preventChipScroll = false - private val updateChipChannel = Channel(Channel.CONFLATED) - - init { - proxiesView.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - updateChipChannel.offer(Unit) - } - }) - } - - suspend fun exec() { - while (true) { - var currentChecked = "" - - while (true) { - updateChipChannel.receive() - - val currentGroup = (proxiesView.adapter!! as AbstractProxyAdapter).getCurrentGroup() - - if (currentChecked == currentGroup) - continue - - currentChecked = currentGroup - - val adapter = chipView.adapter!! as ProxyChipAdapter - - adapter.selected = currentChecked - - if (!preventChipScroll) { - val index = adapter.chips.indexOf(currentChecked) - - if (index < 0) - continue - - chipView.smoothScrollToPosition(index) - } - - delay(200) - } - - } - } - - suspend fun scrollToDirect(group: String) { - val position = (proxiesView.adapter!! as AbstractProxyAdapter).getGroupPosition(group) ?: return - - when ( val m = proxiesView.layoutManager ) { - is GridLayoutManager -> - m.scrollToPositionWithOffset(position, 0) - is LinearLayoutManager -> - m.scrollToPositionWithOffset(position, 0) - } - } - - suspend fun scrollTo(group: String) { - (proxiesView.adapter!! as AbstractProxyAdapter).apply { - val index = getGroupPosition(group) ?: return - - scrollTo(index) - } - } - - private fun scrollTo(position: Int) { - val scroller = (object : LinearSmoothScroller(context) { - override fun getVerticalSnapPreference(): Int { - return SNAP_TO_START - } - - override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float { - return 35f / displayMetrics!!.densityDpi - } - - override fun onStop() { - super.onStop() - - preventChipScroll = false - } - - override fun onStart() { - super.onStart() - - preventChipScroll = true - } - - init { - targetPosition = position - } - }) - - proxiesView.layoutManager!!.startSmoothScroll(scroller) - } -} \ No newline at end of file diff --git a/app/src/main/res/anim/simple_alpha.xml b/app/src/main/res/anim/simple_alpha.xml deleted file mode 100644 index e370f2b05b..0000000000 --- a/app/src/main/res/anim/simple_alpha.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/proxies_chip_colors.xml b/app/src/main/res/color/proxies_chip_colors.xml deleted file mode 100644 index 4f053273a6..0000000000 --- a/app/src/main/res/color/proxies_chip_colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/proxies_chip_text_colors.xml b/app/src/main/res/color/proxies_chip_text_colors.xml deleted file mode 100644 index 5e2e624e74..0000000000 --- a/app/src/main/res/color/proxies_chip_text_colors.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_boot.xml b/app/src/main/res/drawable/ic_boot.xml deleted file mode 100644 index cbd73a8b22..0000000000 --- a/app/src/main/res/drawable/ic_boot.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_cloud.xml b/app/src/main/res/drawable/ic_cloud.xml deleted file mode 100644 index b60a4a5b4b..0000000000 --- a/app/src/main/res/drawable/ic_cloud.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_delete_sweep.xml b/app/src/main/res/drawable/ic_delete_sweep.xml deleted file mode 100644 index 17b0eea87a..0000000000 --- a/app/src/main/res/drawable/ic_delete_sweep.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_expand_less.xml b/app/src/main/res/drawable/ic_expand_less.xml deleted file mode 100644 index 7b6a65628c..0000000000 --- a/app/src/main/res/drawable/ic_expand_less.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_expand_more.xml b/app/src/main/res/drawable/ic_expand_more.xml deleted file mode 100644 index c1f391b617..0000000000 --- a/app/src/main/res/drawable/ic_expand_more.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_filter.xml b/app/src/main/res/drawable/ic_filter.xml deleted file mode 100644 index 72a680cfd9..0000000000 --- a/app/src/main/res/drawable/ic_filter.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_input.xml b/app/src/main/res/drawable/ic_input.xml deleted file mode 100644 index 54d69695a9..0000000000 --- a/app/src/main/res/drawable/ic_input.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_label.xml b/app/src/main/res/drawable/ic_label.xml deleted file mode 100644 index c1e4bc708b..0000000000 --- a/app/src/main/res/drawable/ic_label.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index 2ed6af7ee4..a547804d45 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -1,10 +1,12 @@ - - + android:viewportWidth="796.13654" + android:viewportHeight="796.13654"> + + diff --git a/app/src/main/res/drawable/ic_link.xml b/app/src/main/res/drawable/ic_link.xml deleted file mode 100644 index f615dd387f..0000000000 --- a/app/src/main/res/drawable/ic_link.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_logo.xml b/app/src/main/res/drawable/ic_logo.xml index 2d625a11ba..a0b9a0ffd2 100644 --- a/app/src/main/res/drawable/ic_logo.xml +++ b/app/src/main/res/drawable/ic_logo.xml @@ -1,5 +1,9 @@ - - + + diff --git a/app/src/main/res/drawable/ic_profile_edit.xml b/app/src/main/res/drawable/ic_profile_edit.xml deleted file mode 100644 index 5505cc7756..0000000000 --- a/app/src/main/res/drawable/ic_profile_edit.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_profile_refresh.xml b/app/src/main/res/drawable/ic_profile_refresh.xml deleted file mode 100644 index 1f9072a36f..0000000000 --- a/app/src/main/res/drawable/ic_profile_refresh.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_refresh.xml b/app/src/main/res/drawable/ic_refresh.xml deleted file mode 100644 index 8229a9a64c..0000000000 --- a/app/src/main/res/drawable/ic_refresh.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_settings_color.xml b/app/src/main/res/drawable/ic_settings_color.xml deleted file mode 100644 index d092e0420b..0000000000 --- a/app/src/main/res/drawable/ic_settings_color.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_sync.xml b/app/src/main/res/drawable/ic_sync.xml deleted file mode 100644 index ce8796cb79..0000000000 --- a/app/src/main/res/drawable/ic_sync.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/layout/activity_feedback.xml b/app/src/main/res/layout/activity_feedback.xml deleted file mode 100644 index 6c3eb2494b..0000000000 --- a/app/src/main/res/layout/activity_feedback.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_logs.xml b/app/src/main/res/layout/activity_logs.xml deleted file mode 100644 index 0b7623e399..0000000000 --- a/app/src/main/res/layout/activity_logs.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 28241a4db6..d9a6958284 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -89,7 +89,6 @@ app:summary="@string/not_selected" /> diff --git a/app/src/main/res/layout/activity_setting_access.xml b/app/src/main/res/layout/activity_setting_access.xml deleted file mode 100644 index 3fd6e290b1..0000000000 --- a/app/src/main/res/layout/activity_setting_access.xml +++ /dev/null @@ -1,64 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_setting_application.xml b/app/src/main/res/layout/activity_setting_application.xml deleted file mode 100644 index 517df13c07..0000000000 --- a/app/src/main/res/layout/activity_setting_application.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_setting_main.xml b/app/src/main/res/layout/activity_setting_main.xml deleted file mode 100644 index b37b61ce4b..0000000000 --- a/app/src/main/res/layout/activity_setting_main.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_setting_proxy.xml b/app/src/main/res/layout/activity_setting_proxy.xml deleted file mode 100644 index 78cb180413..0000000000 --- a/app/src/main/res/layout/activity_setting_proxy.xml +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - \ 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 deleted file mode 100644 index 0ff4534982..0000000000 --- a/app/src/main/res/layout/adapter_access_app.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_grid_proxy_group.xml b/app/src/main/res/layout/adapter_grid_proxy_group.xml index 8889e8436f..2d10228087 100644 --- a/app/src/main/res/layout/adapter_grid_proxy_group.xml +++ b/app/src/main/res/layout/adapter_grid_proxy_group.xml @@ -12,16 +12,29 @@ android:text="@string/launch_name" android:id="@+id/name" android:layout_width="wrap_content" - android:layout_height="wrap_content" /> + android:layout_height="wrap_content" + android:layout_toStartOf="@id/urlTestGroup" + android:layout_alignParentStart="true" /> - + android:layout_alignBottom="@id/name"> + + + diff --git a/app/src/main/res/layout/adapter_log.xml b/app/src/main/res/layout/adapter_log.xml deleted file mode 100644 index bb03cc8954..0000000000 --- a/app/src/main/res/layout/adapter_log.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_about.xml b/app/src/main/res/layout/dialog_about.xml deleted file mode 100644 index d8eccea385..0000000000 --- a/app/src/main/res/layout/dialog_about.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_profile_updating.xml b/app/src/main/res/layout/dialog_profile_updating.xml deleted file mode 100644 index d1e43dde40..0000000000 --- a/app/src/main/res/layout/dialog_profile_updating.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_text_edit.xml b/app/src/main/res/layout/dialog_text_edit.xml deleted file mode 100644 index 37beaed216..0000000000 --- a/app/src/main/res/layout/dialog_text_edit.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/page_proxies.xml b/app/src/main/res/layout/page_proxies.xml deleted file mode 100644 index f14841ad0c..0000000000 --- a/app/src/main/res/layout/page_proxies.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/preference_main_item.xml b/app/src/main/res/layout/preference_main_item.xml deleted file mode 100644 index 6f6efefaf5..0000000000 --- a/app/src/main/res/layout/preference_main_item.xml +++ /dev/null @@ -1,29 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/preference_proxy_item.xml b/app/src/main/res/layout/preference_proxy_item.xml deleted file mode 100644 index 0a76fdd233..0000000000 --- a/app/src/main/res/layout/preference_proxy_item.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_fat_item.xml b/app/src/main/res/layout/view_fat_item.xml deleted file mode 100644 index 2f82aa36fa..0000000000 --- a/app/src/main/res/layout/view_fat_item.xml +++ /dev/null @@ -1,61 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_radio_fat_item.xml b/app/src/main/res/layout/view_radio_fat_item.xml deleted file mode 100644 index 6ec7802d5d..0000000000 --- a/app/src/main/res/layout/view_radio_fat_item.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_title.xml b/app/src/main/res/layout/view_title.xml deleted file mode 100644 index d829e291cc..0000000000 --- a/app/src/main/res/layout/view_title.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - \ 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 deleted file mode 100644 index b339004253..0000000000 --- a/app/src/main/res/menu/menu_setting_access.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png index 603514f78158c755ffd162d206c4d2bd8ce4172b..ca2d3add5e315bb61434067f1ea09b88b7727a18 100644 GIT binary patch delta 1754 zcmV<01||834(bh%B!96Pr=t`p zEeQ2p$6BkkT8m>lYAaYO7||9~aDq}r@QtwwzKRM=l_b>`Q64n`2A>U8(O`tc5JDd4 z={B zzr|u%EmlB8PJf5P0rmCundmDUKmjI`X%iX~BOvsNGz7E_ePshE&|olRixm*|AOxh< zYPX}WYyoY4008Y^4XCN9DM##pG#brL36RhL-D$N#yUl)IY@r>q#VRg96Tkim;!^V9 zwz0L(w$^MdhIz7G;sJE+W;2A0Uk=ZPFM%cL1z@*3Sby6(Tk{vZK0yYfXRcubo1cJG zXX@dls6-0rm9fj9gIYGW|5mtS&L9Do+4?(KwwdM+XXQge>sZ!orTUEc!lOi%4%CwaqRaR7># zozcZHdVh+LF{w~l4cZIW;Fwwi+51$Gnp*^`vX4RP z*Ld3acu9`%0kzz*z|iRLdjLW`AGx&il=&|Wc`pIpoU)Sky6&Hinvv#h>cBrh<(exn zgn;-z2y8WC(hAUBHwYWhzTz`5I3nIBfG{HdUVnX2*nrkYVR*(o=-pXs8G{(}Svu)0 zMKB{_3yo~<_2I;ZU;#CoT6^R!V`itrjax?OaLucN7y_y$`4HN?)qDrS2`u;e*TyA- zk$EJgg<~V#1Ee!;{*_nTi5i>!UFX7joV$S zh`tp5&Tkdxg$3yJxyziuaYwT4hfeVu5YD?i-|zpmf-^v<H?pym}uUlrhu>xzrzku!M^NgqL%%Hj*4- zm_1QeN9SQl8xF&?Z*%CqK$uWs*neZ<3JCr%sn;78dJ`7vv6M1u#%h{x;bo+_6?t@! z?q#~cqmnBL{n*_xXTs_t-hUltAx9UFu-Vd1Z^Fdr*Qjryx9Bku&iimXw1KwJCfdi} z!Cb7D@khE1Sh`PI0wfGTn;!r`?4Q*z7!0EALGH()YHDiO_aMDqFBkg*b>-#dIjjMd zm6d(!Hs;N3x&r7u^pQ-n*$fJWA`X46tgIa1?L)l}5j~~T>8i0SqJKdIB?h4{=u=fy zm4^8E4EpNz9}VQ^=Odtjv9YnEwOXylXf%S|ZU;0hWZ`LLGMS*FqCy)L6%~#?%4D)Y zKQ=8aEbL*jc$#RqTrN*6Dk@T_)#?MKrKJTbm1@7IluBin*WKSU{f;kex|RjF@8|>i zfdBS(&Wq%VKI;8PzO8u}<%K1uWP>9gXS+1Nlj`}lbRsfKiH!2lzy1kwEzGB07*qoM6N<$g2Tm3i2wiq delta 1784 zcmV#zCgudNozqx&_QFc<>NaD)&!Hc`~B*XyC8qGE1WpPd2Z zsnhA=gbIkL-G5{0ijQ1AfOcVl?fnkwOYMTsDLnn5RgKl zIE2131(fs%0Q!kBpr)p#WT68pD=SOo3?LIZpkACqpuN3AIDky1b_h>A0hg3@#9q$< zohzw=wCsYO*y0wTzw4Ud#c{zfYPJY2D{uD5R@+@YOn+Fq4Q8xLgqAj)FaRArTSNh& z=B?Qat=fBxZEa0C1A``nP}o(qD*gk)`C`+aYzrWCE-773Uw78l)f>0rwSW!O&cN{@ zkW+Al|A6%Ojj%9ezXcF#^!z9&zgF**9fQ#Xi^Ec^?8K93;yoaoCq~bYwsH)$Tyz+; zIz5df{C`aCt!5G-ji9V-pe!#qkEkLM5NiKf@H8r=vm_>1{0{SlISN;O3y`{D!cD5^+Pryd!PA8%_W=6o^ ziPlR!mK+QUVQv5_uBfqd)GsI+v7Q)1`rn%EfbX*Bt`%`iyYPa*jzO=11oDM$O&?3MaF2&Hxf0{=?4M=_?bd zvskbu{G8(uprKPjA%hgWckkVYit7#V=A2Ddrz?Y+j2nP3TDo(1+>&^h_04V=<{#$p z9E7J;&<{!QZjgQ1awzjW{{Y?78(_iOWPhhuC7g(d*i6L#rws|2`~!qb{N!aj9CmWl zED?n6I0grg<-_JZ*<^Lv1N1_%|CEPv%L zpw>1WG^kp@U@%!;W3$cCov?+=IY2s+*TmyJ z=`~|l5N~hk`aWKJcQqgs_Ln#T$Vh+$$ERBwRy)&jK&95u%f$G8yG z7`8dh)jFA@<=xfbfy^DLL2KJX@ z_e5T}b(Y_nprhI!DT%ySTtm<8X*{YLAuCF@gYPkGL_ zdmv!NPMGxNc6wUA|7i^PEq{o1KwDt^l6bn`tkt`zt>qDiA$V&BMD068udvDrEBV#5 zH{Xqls~nagcoWop1IkD{fGC9OsLcD|{%8vq3$))Mgxi4K_er|}xdGV%N_qqUF@IJ= ztyT**g3OOYm6w+@k06ywB^LSvb%lk6$&3LN6cj8n8)I_QH;1E#K7W#_)oMW^k*q*p zi;IiD560GB?O@_=u=5aNg46+1@zVF9~wwcPe(xg0|NucC=`ma z*49=q8jXO4`7AuGG#U*Q6%{G`{r$(Gk0O!C+k?p%J9g~jWb-`HNU>NPCY4Ghd3kxq za&vPt+Aa@**;4& zl*or@IM3zN>B}=CMvQn0eeCgXTo6e8NHp~)+X2Mr(|i{C(CJG*4;F@}K;$0o;U4b6 ai}5c?dC5>~5e^ps0000~ij}3$r2>~o>A93k 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 LNkvXXu0mjfi}gdZ delta 1205 zcmV;m1WNnC3CRhNB!7@eL_t(|+U=QbOjB1F$K7Cy&SYQQmnG9pn1+GI%)}VRvMqzM z>12s6b#A8FLSr=h;5Ln}fEbJmoz5vU0d*P`>;Q3H&>>-Hs1%_hRl#8eWb;4AgW^qOzxr42>v-ZIJg%x0%nwr zTrSsswDwd;fM92FyWKA!z!9)CHF*H2Hywgk-^+rEn)4B*H@9DhxgTzZ zACH{o575`|fp4?(3lz<<(zo_&x%fTi0C^OtR5hDcw3 zR}KR9haP7y`>(Cz7Ige|2kdx_kAx$~8{w6-jV$egLei>{3bgj-k z%n!hN&k3(D%VS1fPTdGqhE_Hsug)tO(L8nNvlqIcw{HMetl!7((GbiH;KG#~{~A>? zaY8G`VKVuX{1E{NaxK>W{$usx&>Nqw!DWTO2Y=vXeH)G>ny{U8{zqFNE?pD0A9P;7 z!w2BNFUGLO-p1yUnR5shuPI>jL#TAsSP@(R@(NE(Xaovsx9&6_fK{7|qXIz753kSn z_yHEJE{F=i(zS(bPa!D)6<}z-%3gPG{{Z-WgW?c6Q7!!VTlP%J0Lc9J2Wwz~aw`L% zv48%q%_UAo6IGl-!&a}70hn65VGeFUb{5yNnx}i0uHIx5Q#1fbSFTy511K!5gLzBy z;Bx2R;=_N(W%Ov{`&e|Uv0XZVeHCXQ0oU_&=PcsGf37l#8_#fNPM$0^DSMe$70I1y;;l?FC(0^oTca^v#H>v>H1^WMNFQYn)(muETd$$;4^UIg+NEd9bIB(%DukB9jt6~k(`siM_vW@g!PwHtI(CimGU#i~ z>=R2URh?;t+OzHKyAb8K-gAJ<-Oum4kla2k{(nF2HZ?UR$n_2Abh^3W0N9~{UVpED z$?NsnDU)0dIM5q79F88XRvSki4F*GOxFe>?U0q!j2toOPKzM~<*@7T|*=+t5y-kVa zj4UQ4B_*k)rRBKIW^*{5&Id^EqWEztSf<@>cbH736UoWR)O=zhJ*k|6nyFAIQq^j8 zhDxR4A}Tu#19=&>_8)~%DwWUT&n~H`=Q(RCc^TKiJ9QRpd(ZGx$PW7?$Rzv&<{jZ@ Tfcfc*00000NkvXXu0mjfP;N}{ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png index bbb46a9d0f1da5dedb49ea24e4f2e264dd197073..c5e43ad44eb4fd8a0b901f6847b060baaef60440 100644 GIT binary patch delta 2272 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 Q01E&B07*qoM6N<$g3t%9Bme*a delta 2418 zcmV-&361ug6a5m9B!5InL_t(|+U=bSP*hbI$MKcTR8!MqD#!p|6j@Z3)s$w5kJPXt zveF36Oi5BJ5Z@r^`XEarkVl$@fM8@Gt4X4wsF;cm#3c~{S&}yki!8gq*ZGcjcNW+s zVK44p?)hf^a~Zjw!~TEwp2v62-D_pVf&~i}ELgB$!GeWw0)GH{6ZK)D>U}$nrhunqJc#ABGo|7gMNlQ;+-&eypPDA=rmCok%Fj(JBmr#&s@fNfu7u_ z?-lP?J~=uP$%vq(r3IRso0&MFA4Iacy1HS!-U(Wo3Wef+Qb8_fd}C8nlZG6dn3|h7 zFB;Js0^pzY^ndh+c)Js{g=jRI0KD=^WolAc3%lyu2|ZD(R4T~N&)>ic0PXNfDwXyp zg=ZmQ#Ldh$EUh;sB_$U;Jw5N%_t2!PUnY~;lY%R_N}8G3MgcTqD^XfnS{WT3?P&5{ z!2Rc585tSFNCkBaz^wr&FE3X{L`1xZ-thsTcWP?t(|-)Wtpg}4E2|3&3mbvn@d2Pu za&od01JEe|%HZJOm(e>0fB|3t7yt&qLJea8TI)tbBlw3V!TO)}Lj4U@w;5lps0ZJT zzrmkJFE9Z5DwJ2iGw-Yfd*?;q6O=%K@ZU@{L)5Nx7&Oio7D!?l0DYyM$brY-^ro^M z;{&lNCx5U$UhBA&UjnawyoS~>_OtJJg)Ddg!O?%|0H9u)8VCjQa^A{sRI6d$>KI-5 z&%64Ai~wN(h}0Sl%=Q1Z6#&%N>vr;XmF`VH4-ONT=*q@sNP74j1AwZiu7gQ)HnjtQ zChb3(ZF2dw${V!FXT=izp1c^Nk5ef=g9BM zt7>&+Unweur`}#}u%}y;KBGO4>$OSS|l61-V5g!E58T0k>8rZov zvt7yqXWF50Y2pNMjhKt+07J%owI&7{8`Zjnl$broJ8eGRJrN0qboFV-q;I8xCV%hS z4e{axa86c4(*cIe3*54Y8m~R~@ZY&$+l6K)G4)idIzwaIr#;^B3K1uOgPE6L@OWRt zS2z==62^`vt;V(n;-$iq;%Y@yzai9 z=7}bI2Gh>6s~zy9xtrh%zhB@DkADqDJN42Eh4=tSBBi{HaEzt0JELP+m&FHQj)7&! z&UCPNeC4AxFx@ATw)dtCS)-GVi3-3IKV9rSQW0tIV2)(0|wHPuH8Y zBTsMuPMo`9bgkAL8Zv${RMjZO0|4_cxYlba08ltF6b%4#r=bn0?X0;+0|?xbY;gcK zY~3dw0CUL9)Y1UV3)}&XYE91paHBy*^ElYKwcP+^uALAa$#s4x)G8Z<4*<>n>)1t@ zz9^FJy?N0s05Zv_NBjE*RevMh{cLFfUYoX-UNC|NfMe+;KP19HZiIu`&VaCFYP$9u zTx2nphBoU^bi!hgmsSZE0R9bVzIKK>oJ_Qm(z576i2iw}vaf>UbRjKm zb^tia!L20PI{+V+Fm~=VpGb=Z;O-j*^$jXv_5tH&ZPa}b@1O*u`E}fnX{i9F%-c-Y zDg+5Y5+T)ft~de1)qkqC9WnM5&%Nhwu>f#g05@?75q%`!p>cBG`VGd&|6U0qvGAAdpGvFA8_4q*P8SeUVN zE8P)2%w+{H8>;wD#*O$OkbZGUk6n<#zq6vM7B1$=>E6)Y2hPxMM%kKp1U5$>psP}= zLX+t*cHz1>n7wj4%v$j?%v`n&rhXF+ZVSU&Y0A71y5Cb@Q;6{H)1HGHx3G*a19reS z>vzJ^O^L8JN`DGlNPiV4&7`?0d|dUctcX6?-IeVrqW#7!oPlEsTwmI!>e*M8vG0sy z02lxUfB|3t7yt&~c7Ay`uK-d~Ql4c1I(G+zgoM1p8-V!uct=urB?EA40OWFcl|&*L z&IbT|{YIaWBS${e(9n?0USab8uP{N6WHMQ@ataylCZf#CJc8}QF+CDm9zyND83O_q~fL@>{oHxC!ZbM)teYke6pUBmh^WwG` k&w>RD7A#n>V9_(;KRQ_AIjs~az5oCK07*qoM6N<$f@|fX0{{R3 diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png index d6b423cec38f9ea7c6adcbe381b3d933b2f829e0..341e5fcfc3c939f1534df2e422f7a0b690a4a7e4 100644 GIT binary patch literal 5166 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 literal 5197 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 UgIu26)h7z*YTehYMLv4@ALO45+5i9m diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png index a0486b25c2238e74d4e26b18ac66bff4c25cd804..c8a8350d13c05cec0a32be9a91ffca803ae3c2ce 100644 GIT binary patch literal 8151 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 literal 8074 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 diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 0f9cb71accf3bfc482749897bd84d20d4b216bac..d1c9a200b312f3a2805452f3ea7df5f4510757af 100644 GIT binary patch literal 5206 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 PdLhtO*H^1jv5WX01U9Ky literal 5240 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 literal 11658 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 CRkiH^ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e53bc91e7c..47f8576191 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -13,6 +13,4 @@ #FFFFFF - #d3d3d3 - #FF888888 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 31c4f93ad3..d719eee63f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -8,12 +8,9 @@ %s Forwarded Proxy - Rule Mode Direct Mode - Global Mode Profiles - %s Activated Not selected Logs @@ -21,8 +18,6 @@ Feedback About - Local - Remote Unknown Recently @@ -38,27 +33,22 @@ URL Import from URL External - Import from %s - + Application Broken Profile Name - Source Profile Name Profile URL Auto Update - Auto Update Interval In minutes Invalid Interval - Invalid Profile Download Failure Detail Update Edit Delete - Clear Cache Properties Duplicate Reset Providers @@ -89,96 +79,9 @@ Start URL Provider Failure - %s (File) - %s (URL) - Remove - Profile Updating - New Profile - File - From Local Storage - - URL - From Internet - - Import From File - Import From URL - - Name - Unlabeled - - File - content:// - - URL - https:// OK Cancel - Empty Name - Empty Path - Invalid Config: %s - - %s - %s - - Setting - Application - Proxy - - VPN Mode - Handle all system traffic - Proxy Only Mode - Start clash core only - VPN Settings - Access Control - Configure access permission for apps - Bypass Private Network - Bypass private network subnet - IPv6 Support - Routing IPv6 traffic - DNS Hijacking - Redirect ALL dns traffic to clash - - Global - Allow all apps - Whitelist - Only allowing selected apps - Blacklist - Disallow selected apps - Loading - Show Mode - Hide Mode - Select All - Select Invert - Clear Selection - - Behavior - Start on Boot - Start clash on system boot - - Clash Start Service - Starting Clash - - Not Implemented - - ID - Feedback ID - Unknown - Copied - - Source & Issues - Upstream - Github - - Groups - Telegram Channel - Telegram Group - - https://github.com/Dreamacro/clash - https://github.com/Kr328/ClashForAndroid - https://t.me/clash_for_android - https://t.me/clash_for_android_channel - - Power by Clash diff --git a/app/src/main/res/xml/feedback.xml b/app/src/main/res/xml/feedback.xml deleted file mode 100644 index 8e4ba50074..0000000000 --- a/app/src/main/res/xml/feedback.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/setting_application.xml b/app/src/main/res/xml/setting_application.xml deleted file mode 100644 index a91f2c2952..0000000000 --- a/app/src/main/res/xml/setting_application.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/setting_main.xml b/app/src/main/res/xml/setting_main.xml deleted file mode 100644 index f8528ceff2..0000000000 --- a/app/src/main/res/xml/setting_main.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/setting_proxy.xml b/app/src/main/res/xml/setting_proxy.xml deleted file mode 100644 index 2d3d0ddea9..0000000000 --- a/app/src/main/res/xml/setting_proxy.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/design/src/main/res/drawable/ic_demo_drawable.xml b/design/src/main/res/drawable/ic_demo_drawable.xml deleted file mode 100644 index a4f5ea0e71..0000000000 --- a/design/src/main/res/drawable/ic_demo_drawable.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index 1e5e6c15da..76ac825190 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -1,5 +1,4 @@ - Demo String OK Cancel diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt index 302d145f38..aeda6638e7 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt @@ -201,7 +201,8 @@ class ProfileBackgroundService : BaseService() { private fun startForeground() { val notification = NotificationCompat.Builder(this, SERVICE_STATUS_CHANNEL) .setContentTitle(getText(R.string.profile_service_status_title)) - .setSmallIcon(R.drawable.ic_updating) + .setColor(getColor(R.color.colorAccentService)) + .setSmallIcon(R.drawable.ic_notification) .setOnlyAlertOnce(true) .build() @@ -215,7 +216,8 @@ class ProfileBackgroundService : BaseService() { val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) .setContentTitle(getText(R.string.profile_status_title)) .setContentText(getString(R.string.profile_status_updating, entity.name)) - .setSmallIcon(R.drawable.ic_update_normal) + .setColor(getColor(R.color.colorAccentService)) + .setSmallIcon(R.drawable.ic_notification) .setOnlyAlertOnce(true) .setOngoing(true) .build() @@ -236,7 +238,8 @@ class ProfileBackgroundService : BaseService() { val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) .setContentTitle(getText(R.string.profile_status_title)) .setContentText(getString(R.string.profile_status_update_completed, entity.name)) - .setSmallIcon(R.drawable.ic_update_normal) + .setColor(getColor(R.color.colorAccentService)) + .setSmallIcon(R.drawable.ic_notification) .setOnlyAlertOnce(true) .build() @@ -256,7 +259,8 @@ class ProfileBackgroundService : BaseService() { val notification = NotificationCompat.Builder(this, SERVICE_RESULT_CHANNEL) .setContentTitle(getString(R.string.profile_status_update_failure, entity.name)) .setContentText(reason) - .setSmallIcon(R.drawable.ic_update_normal) + .setColor(getColor(R.color.colorAccentService)) + .setSmallIcon(R.drawable.ic_notification) .setOnlyAlertOnce(true) .build() diff --git a/service/src/main/res/drawable/ic_update_failure.xml b/service/src/main/res/drawable/ic_update_failure.xml deleted file mode 100644 index 0cbb996010..0000000000 --- a/service/src/main/res/drawable/ic_update_failure.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/service/src/main/res/drawable/ic_update_normal.xml b/service/src/main/res/drawable/ic_update_normal.xml deleted file mode 100644 index c24862da57..0000000000 --- a/service/src/main/res/drawable/ic_update_normal.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/service/src/main/res/drawable/ic_updating.xml b/service/src/main/res/drawable/ic_updating.xml deleted file mode 100644 index 1d835e94b5..0000000000 --- a/service/src/main/res/drawable/ic_updating.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/service/src/main/res/values/strings.xml b/service/src/main/res/values/strings.xml index d5ff463f74..b6b67c4a04 100644 --- a/service/src/main/res/values/strings.xml +++ b/service/src/main/res/values/strings.xml @@ -2,13 +2,6 @@ Clash Clash Status - Running - Profile %s loaded - VPN - - UP - Down - 0 Byte/s "%1$s↑\t%2$s↓" From 4848a0f6d9743869f217b88212fc79dcce288448 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 17 Feb 2020 00:43:31 +0800 Subject: [PATCH 088/358] fix auto proxy scroll --- .../com/github/kr328/clash/ProxiesActivity.kt | 14 ++++--- .../kr328/clash/adapter/ProxyAdapter.kt | 41 ++++++++++++------- .../github/kr328/clash/remote/ClashClient.kt | 2 +- .../github/kr328/clash/utils/ScrollBinding.kt | 18 +++++--- 4 files changed, 47 insertions(+), 28 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index 41cd780fac..527b63992d 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -10,6 +10,7 @@ import com.github.kr328.clash.adapter.ProxyAdapter import com.github.kr328.clash.adapter.ProxyChipAdapter import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.Proxy +import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.preference.UiPreferences import com.github.kr328.clash.remote.withClash import com.github.kr328.clash.utils.PrefixMerger @@ -75,7 +76,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { chipList.itemAnimator?.changeDuration = 0 launch { - mainList.addOnScrollListener(object: RecyclerView.OnScrollListener(){ + mainList.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { scrollBinding.sendMasterScrolled() } @@ -318,24 +319,25 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { } } - override suspend fun getCurrentMasterToken(): String { + override fun getCurrentMasterToken(): String { return mainListAdapter.getCurrentGroup() } - override suspend fun onMasterTokenChanged(token: String) { + override fun onMasterTokenChanged(token: String) { + chipListAdapter.selected = token val position = chipListAdapter.chips.indexOf(token) if (position < 0) return - chipList.scrollToPosition(position) + chipList.smoothScrollToPosition(position) } - override suspend fun getMasterTokenPosition(token: String): Int { + override fun getMasterTokenPosition(token: String): Int { return mainListAdapter.getGroupPosition(token) } - override suspend fun doMasterScroll(scroller: LinearSmoothScroller) { + override fun doMasterScroll(scroller: LinearSmoothScroller) { mainListAdapter.layoutManager.startSmoothScroll(scroller) } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt index a4ea2a963b..935bd6ddec 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt @@ -22,7 +22,7 @@ class ProxyAdapter( private val context: Context, val onSelect: (String, String) -> Unit, val onUrlTest: (String) -> Unit -): RecyclerView.Adapter() { +) : RecyclerView.Adapter() { companion object { const val DEFAULT_SPAN_COUNT = 2 } @@ -66,7 +66,8 @@ class ProxyAdapter( private var rootMutex = Mutex() private var urlTesting: Set = emptySet() private var renderList = mutableListOf() - private val activeList: MutableMap = mutableMapOf() + private var activeList: MutableMap = mutableMapOf() + private var groupPosition: MutableMap = mutableMapOf() @ColorInt private val colorSurface: Int @ColorInt @@ -132,25 +133,34 @@ class ProxyAdapter( override fun getNewListSize(): Int = newRenderList.size }) + val groupCache: MutableMap = mutableMapOf() + val activeCache: MutableMap = mutableMapOf() + + newRenderList.forEachIndexed { index, it -> + when ( it ) { + is ProxyGroupRenderInfo -> + groupCache[it.name] = index + is ProxyRenderInfo -> { + if ( it.info.active ) + activeCache[it.name] = index + } + } + } + withContext(Dispatchers.Main) { root = newList renderList = newRenderList.toMutableList() urlTesting = testing + groupPosition = groupCache + activeList = activeCache result.dispatchUpdatesTo(this@ProxyAdapter) } rootMutex.unlock() } - suspend fun getGroupPosition(name: String): Int { - return withContext(Dispatchers.Default) { - renderList.mapIndexed { index, p -> - if (p is ProxyGroupRenderInfo && p.name == name) - index - else - -1 - }.singleOrNull() ?: -1 - } + fun getGroupPosition(name: String): Int { + return groupPosition[name] ?: -1 } fun getCurrentGroup(): String { @@ -183,6 +193,8 @@ class ProxyAdapter( is ProxyGroupHeader -> { val current = renderList[position] as ProxyGroupRenderInfo + groupPosition[current.name] = position + holder.name.text = current.info.name holder.urlTest.setOnClickListener { holder.urlTest.visibility = View.GONE @@ -191,11 +203,10 @@ class ProxyAdapter( onUrlTest(current.name) } - if ( urlTesting.contains(current.name) ) { + if (urlTesting.contains(current.name)) { holder.urlTest.visibility = View.GONE holder.urlTestProgress.visibility = View.VISIBLE - } - else { + } else { holder.urlTest.visibility = View.VISIBLE holder.urlTestProgress.visibility = View.GONE } @@ -209,7 +220,7 @@ class ProxyAdapter( if (current.info.delay > 0) holder.delay.text = current.info.delay.toString() else - holder.delay.text = "" + holder.delay.text = if (current.info.selectable) "" else "N/A" if (current.info.active) { activeList[current.group] = position diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt index 93ac66039d..cf3bc360ff 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -17,7 +17,7 @@ class ClashClient(private val service: IClashManager) { suspend fun startHealthCheck(group: String) = withContext(Dispatchers.IO) { CompletableDeferred().apply { - service.startHealthCheck(group, object: IStreamCallback.Stub() { + service.startHealthCheck(group, object : IStreamCallback.Stub() { override fun complete() { this@apply.complete(Unit) } diff --git a/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt b/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt index a239d6c74e..659581da82 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt @@ -1,7 +1,9 @@ package com.github.kr328.clash.utils import android.content.Context +import android.util.DisplayMetrics import androidx.recyclerview.widget.LinearSmoothScroller +import com.github.kr328.clash.core.utils.Log import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay @@ -10,10 +12,10 @@ class ScrollBinding( private val callback: Callback ) { interface Callback { - suspend fun getCurrentMasterToken(): String - suspend fun onMasterTokenChanged(token: String) - suspend fun getMasterTokenPosition(token: String): Int - suspend fun doMasterScroll(scroller: LinearSmoothScroller) + fun getCurrentMasterToken(): String + fun onMasterTokenChanged(token: String) + fun getMasterTokenPosition(token: String): Int + fun doMasterScroll(scroller: LinearSmoothScroller) } private val updateChannel = Channel(Channel.CONFLATED) @@ -23,7 +25,7 @@ class ScrollBinding( updateChannel.offer(Unit) } - suspend fun scrollMaster(token: String) { + fun scrollMaster(token: String) { val position = callback.getMasterTokenPosition(token) if (position < 0) @@ -46,6 +48,10 @@ class ScrollBinding( preventSlaveScroll = true } + override fun calculateSpeedPerPixel(displayMetrics: DisplayMetrics?): Float { + return super.calculateSpeedPerPixel(displayMetrics) * 0.8f + } + init { targetPosition = position } @@ -61,7 +67,7 @@ class ScrollBinding( updateChannel.receive() val currentToken = callback.getCurrentMasterToken() - if (lastToken == currentToken) + if (preventSlaveScroll || lastToken == currentToken) continue lastToken = currentToken From 72932a255d31f7e2574f018daa8393696b16c23a Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Mon, 17 Feb 2020 01:54:12 +0800 Subject: [PATCH 089/358] add basic multiple language support --- .../com/github/kr328/clash/BaseActivity.kt | 29 +++++++++++++++++-- .../kr328/clash/preference/UiPreferences.kt | 7 ++++- app/src/main/res/layout/activity_main.xml | 9 +++--- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 6d7a43e275..807c6af69f 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -20,6 +20,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import java.util.* abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() { class EmptyBroadcastReceiver : BroadcastReceiver() { @@ -55,6 +56,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() var menu: Menu? = null lateinit var uiPreference: UiPreferences private set + lateinit var language: String open suspend fun onClashStarted() {} open suspend fun onClashStopped(reason: String?) {} @@ -81,17 +83,40 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() super.setContentView(base) } + override fun attachBaseContext(newBase: Context?) { + val base = newBase ?: return super.attachBaseContext(newBase) + + uiPreference = UiPreferences(base) + + language = uiPreference.get(UiPreferences.LANGUAGE) + + val languageOverride = language.split("-") + if ( language.isEmpty() ) + return super.attachBaseContext(base) + + val configuration = base.resources.configuration + val localeOverride = if ( languageOverride.size == 2 ) + Locale(languageOverride[0], languageOverride[1]) + else + Locale(languageOverride[0]) + + configuration.setLocale(localeOverride) + + super.attachBaseContext(base.createConfigurationContext(configuration)) + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - uiPreference = UiPreferences(this) - resetLightNavigationBar() } override fun onStart() { super.onStart() + if ( language != uiPreference.get(UiPreferences.LANGUAGE) ) + recreate() + Broadcasts.register(receiver) } diff --git a/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt b/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt index ea5618a05d..33762da742 100644 --- a/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt +++ b/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt @@ -11,10 +11,15 @@ class UiPreferences(context: Context) { const val PROXY_SORT_NAME = "name" const val PROXY_SORT_DELAY = "delay" + const val LANGUAGE_DEFAULT = "" + const val LANGUAGE_EN = "en" + const val LANGUAGE_ZH_CN = "zh-CN" + val PROXY_GROUP_SORT = StringEntry("proxy_group_sort", PROXY_SORT_DEFAULT) val PROXY_PROXY_SORT = StringEntry("proxy_proxy_sort", PROXY_SORT_DEFAULT) val PROXY_LAST_SELECT_GROUP = StringEntry("proxy_last_select_group", "") val PROXY_MERGE_PREFIX = BooleanEntry("proxy_merge_prefix", true) + val LANGUAGE = StringEntry("language", "") } interface Entry { @@ -22,7 +27,7 @@ class UiPreferences(context: Context) { fun put(editor: SharedPreferences.Editor, value: T) } - class StringEntry(private val key: String, private val defaultValue: String? = null) : + class StringEntry(private val key: String, private val defaultValue: String) : Entry { override fun get(sharedPreferences: SharedPreferences): String { return sharedPreferences.getString(key, defaultValue)!! diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index d9a6958284..20cb227cdb 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -20,10 +20,9 @@ android:gravity="center_vertical"> + android:layout_marginStart="25dp" /> Date: Tue, 18 Feb 2020 00:53:18 +0800 Subject: [PATCH 090/358] add logs activity --- app/src/main/AndroidManifest.xml | 20 +- .../com/github/kr328/clash/BaseActivity.kt | 7 +- .../java/com/github/kr328/clash/Constants.kt | 4 + .../github/kr328/clash/LogViewerActivity.kt | 145 ++++++++++ .../com/github/kr328/clash/LogcatService.kt | 248 +++++++++++++++++ .../com/github/kr328/clash/LogsActivity.kt | 262 ++++++++++++++++++ .../com/github/kr328/clash/MainActivity.kt | 4 + .../com/github/kr328/clash/ProxiesActivity.kt | 1 - .../kr328/clash/adapter/LiveLogAdapter.kt | 56 ++++ .../github/kr328/clash/adapter/LogAdapter.kt | 42 +++ .../kr328/clash/adapter/LogFileAdapter.kt | 59 ++++ .../kr328/clash/adapter/ProxyAdapter.kt | 4 +- .../com/github/kr328/clash/model/LogFile.kt | 20 ++ .../com/github/kr328/clash/utils/FileUtils.kt | 8 + .../github/kr328/clash/utils/ScrollBinding.kt | 1 - app/src/main/res/drawable/ic_adb.xml | 9 + app/src/main/res/drawable/ic_adjust.xml | 9 + app/src/main/res/drawable/ic_clear_all.xml | 9 + .../res/drawable/ic_launcher_foreground.xml | 2 +- app/src/main/res/drawable/ic_save.xml | 10 +- app/src/main/res/drawable/ic_stop.xml | 9 + .../main/res/layout/activity_log_viewer.xml | 35 +++ app/src/main/res/layout/activity_logs.xml | 40 +++ app/src/main/res/layout/activity_main.xml | 23 +- .../main/res/layout/activity_profile_edit.xml | 20 +- app/src/main/res/layout/adapter_log.xml | 29 ++ app/src/main/res/layout/adapter_log_file.xml | 43 +++ app/src/main/res/values/strings.xml | 17 ++ app/src/main/res/xml/export_contents.xml | 9 + app/src/main/res/xml/full_backup_content.xml | 1 + core/src/main/golang/profile/download.go | 4 + design/src/main/AndroidManifest.xml | 3 +- .../github/kr328/clash/design/common/Base.kt | 5 +- .../kr328/clash/design/common/Category.kt | 35 +++ .../clash/design/common/CommonUiBuilder.kt | 31 +++ .../kr328/clash/design/common/Custom.kt | 31 +++ .../kr328/clash/design/common/Option.kt | 5 - .../kr328/clash/design/common/TextInput.kt | 5 - design/src/main/res/layout/view_category.xml | 27 ++ .../kr328/clash/service/ClashManager.kt | 4 + .../kr328/clash/service/ProfileProcessor.kt | 6 +- .../service/data/ClashProfileProxyEntity.kt | 7 +- .../clash/service/util/BroadcastUtils.kt | 2 - 43 files changed, 1251 insertions(+), 60 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/LogViewerActivity.kt create mode 100644 app/src/main/java/com/github/kr328/clash/LogcatService.kt create mode 100644 app/src/main/java/com/github/kr328/clash/LogsActivity.kt create mode 100644 app/src/main/java/com/github/kr328/clash/adapter/LiveLogAdapter.kt create mode 100644 app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt create mode 100644 app/src/main/java/com/github/kr328/clash/adapter/LogFileAdapter.kt create mode 100644 app/src/main/java/com/github/kr328/clash/model/LogFile.kt create mode 100644 app/src/main/java/com/github/kr328/clash/utils/FileUtils.kt create mode 100644 app/src/main/res/drawable/ic_adb.xml create mode 100644 app/src/main/res/drawable/ic_adjust.xml create mode 100644 app/src/main/res/drawable/ic_clear_all.xml create mode 100644 app/src/main/res/drawable/ic_stop.xml create mode 100644 app/src/main/res/layout/activity_log_viewer.xml create mode 100644 app/src/main/res/layout/activity_logs.xml create mode 100644 app/src/main/res/layout/adapter_log.xml create mode 100644 app/src/main/res/layout/adapter_log_file.xml create mode 100644 app/src/main/res/xml/export_contents.xml create mode 100644 design/src/main/java/com/github/kr328/clash/design/common/Category.kt create mode 100644 design/src/main/java/com/github/kr328/clash/design/common/Custom.kt create mode 100644 design/src/main/res/layout/view_category.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 339579f5a5..311194921e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,8 +3,8 @@ xmlns:tools="http://schemas.android.com/tools" package="com.github.kr328.clash"> - + + android:configChanges="uiMode" + android:launchMode="singleTask"> @@ -51,5 +52,20 @@ android:label="@string/proxy" android:exported="false" android:configChanges="uiMode" /> + + + diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index 807c6af69f..ce6cf86de2 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -91,11 +91,11 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() language = uiPreference.get(UiPreferences.LANGUAGE) val languageOverride = language.split("-") - if ( language.isEmpty() ) + if (language.isEmpty()) return super.attachBaseContext(base) val configuration = base.resources.configuration - val localeOverride = if ( languageOverride.size == 2 ) + val localeOverride = if (languageOverride.size == 2) Locale(languageOverride[0], languageOverride[1]) else Locale(languageOverride[0]) @@ -114,7 +114,7 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() override fun onStart() { super.onStart() - if ( language != uiPreference.get(UiPreferences.LANGUAGE) ) + if (language != uiPreference.get(UiPreferences.LANGUAGE)) recreate() Broadcasts.register(receiver) @@ -146,7 +146,6 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() override fun setSupportActionBar(toolbar: Toolbar?) { super.setSupportActionBar(toolbar) - supportActionBar?.setDisplayShowHomeEnabled(shouldDisplayHomeAsUpEnabled()) supportActionBar?.setDisplayHomeAsUpEnabled(shouldDisplayHomeAsUpEnabled()) } diff --git a/app/src/main/java/com/github/kr328/clash/Constants.kt b/app/src/main/java/com/github/kr328/clash/Constants.kt index 60b172d784..9d9f0c95f3 100644 --- a/app/src/main/java/com/github/kr328/clash/Constants.kt +++ b/app/src/main/java/com/github/kr328/clash/Constants.kt @@ -4,6 +4,10 @@ object Constants { const val PREFERENCE_NAME_APP = "app" const val PREFERENCE_KEY_LAST_INSTALL = "last_install" + const val LOG_DIR_NAME = "logs" + + const val FILE_PROVIDER_AUTH = BuildConfig.APPLICATION_ID + ".files" + const val URL_PROVIDER_TYPE_FILE = "file" const val URL_PROVIDER_TYPE_URL = "url" const val URL_PROVIDER_TYPE_EXTERNAL = "external" diff --git a/app/src/main/java/com/github/kr328/clash/LogViewerActivity.kt b/app/src/main/java/com/github/kr328/clash/LogViewerActivity.kt new file mode 100644 index 0000000000..1aa487f1f9 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/LogViewerActivity.kt @@ -0,0 +1,145 @@ +package com.github.kr328.clash + +import android.content.ComponentName +import android.content.Context +import android.content.ServiceConnection +import android.os.Bundle +import android.os.IBinder +import android.view.View +import androidx.core.net.toFile +import androidx.recyclerview.widget.LinearLayoutManager +import com.github.kr328.clash.adapter.LiveLogAdapter +import com.github.kr328.clash.adapter.LogAdapter +import com.github.kr328.clash.core.event.LogEvent +import com.github.kr328.clash.service.util.intent +import kotlinx.android.synthetic.main.activity_log_viewer.* +import kotlinx.coroutines.* +import kotlinx.coroutines.sync.Mutex +import java.io.File +import java.io.FileReader +import java.io.IOException +import java.lang.Exception +import kotlin.streams.toList + +class LogViewerActivity : BaseActivity() { + private val pauseMutex = Mutex() + private var pollingThread: Thread? = null + private val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + finish() + } + + override fun onServiceConnected(name: ComponentName?, service: IBinder?) { + val logcat = + requireNotNull(service?.queryLocalInterface(LogcatService::class.java.name)) as LogcatService + + startLogcatPoll(logcat) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_log_viewer) + + setSupportActionBar(toolbar) + + val file = intent?.data + + if (file == null) + startLiveMode() + else + startFileMode(file.toFile()) + } + + override fun onDestroy() { + super.onDestroy() + + pollingThread?.interrupt() + } + + override fun onStop() { + super.onStop() + + launch { + pauseMutex.lock() + } + } + + override fun onStart() { + super.onStart() + + launch { + if ( pauseMutex.isLocked ) + pauseMutex.unlock() + } + } + + private fun startLiveMode() { + mainList.layoutManager = LinearLayoutManager(this) + mainList.adapter = LiveLogAdapter(this) + mainList.itemAnimator?.addDuration = 100 + mainList.itemAnimator?.removeDuration = 100 + + stop.setOnClickListener { + stopService(LogcatService::class.intent) + finish() + } + + bindService(LogcatService::class.intent, connection, Context.BIND_AUTO_CREATE) + } + + private fun startFileMode(file: File) { + stop.visibility = View.GONE + + launch { + val items = withContext(Dispatchers.IO) { + try { + file.readText() + .split("\n") + .parallelStream() + .map { it.trim() } + .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) { + makeSnackbarException(getString(R.string.open_log_failure), e.message) + + throw CancellationException() + } + } + + mainList.layoutManager = LinearLayoutManager(this@LogViewerActivity) + mainList.adapter = LogAdapter(this@LogViewerActivity, items) + mainList.adapter!!.notifyItemRangeInserted(0, items.size) + } + } + + private fun startLogcatPoll(service: LogcatService) { + launch { + var offset = 0L + + while (isActive) { + pauseMutex.lock() + + val response = service.pollLogEvent(offset).await() + + (mainList.adapter as LiveLogAdapter).insertItems(response.logs) + + mainList.apply { + if ( computeVerticalScrollOffset() < 30 ) + scrollToPosition(0) + } + + offset = response.offset + response.logs.size + + pauseMutex.unlock() + + delay(200) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/LogcatService.kt b/app/src/main/java/com/github/kr328/clash/LogcatService.kt new file mode 100644 index 0000000000..ed2d1afd43 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/LogcatService.kt @@ -0,0 +1,248 @@ +package com.github.kr328.clash + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.app.PendingIntent +import android.app.Service +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Binder +import android.os.Build +import android.os.IBinder +import android.os.IInterface +import android.text.format.DateFormat +import androidx.collection.CircularArray +import androidx.core.app.NotificationCompat +import androidx.core.app.NotificationManagerCompat +import com.github.kr328.clash.core.event.LogEvent +import com.github.kr328.clash.core.utils.Log +import com.github.kr328.clash.model.LogFile +import com.github.kr328.clash.service.ClashManagerService +import com.github.kr328.clash.service.IClashManager +import com.github.kr328.clash.service.ipc.IStreamCallback +import com.github.kr328.clash.service.ipc.ParcelableContainer +import com.github.kr328.clash.service.util.intent +import com.github.kr328.clash.utils.logsDir +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.selects.select +import java.io.FileWriter +import java.io.IOException +import kotlin.math.max + +class LogcatService : Service(), CoroutineScope by MainScope(), IInterface { + companion object { + 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_CONTENT_FORMAT = "%d %s %s" + + var isServiceRunning: Boolean = false + } + + data class Request(val offset: Long, val response: CompletableDeferred) + data class Response(val offset: Long, val logs: List) + + private val logChannel = Channel(MAX_CACHE_COUNT) + private val requestChannel = Channel() + private val cache: CircularArray = CircularArray() + private var cacheOffset = 0L + private val entity = LogFile.generate() + + private val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + stopSelf() + } + + 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() + } + + override fun send(data: ParcelableContainer?) { + val logEvent = (data?.data as LogEvent?) ?: return + + if (!logChannel.offer(logEvent)) + Log.w("Drop log $logEvent") + } + }) + } + } + + override fun onCreate() { + super.onCreate() + + isServiceRunning = true + + createNotificationChannel() + showNotification() + + bindService(ClashManagerService::class.intent, connection, Context.BIND_AUTO_CREATE) + + launchProcessor() + + launchSaveThread() + } + + override fun onDestroy() { + cancel() + + stopForeground(true) + + super.onDestroy() + + isServiceRunning = false + } + + override fun onBind(intent: Intent?): IBinder? { + return this.asBinder() + } + + override fun asBinder(): IBinder { + return object : Binder() { + override fun queryLocalInterface(descriptor: String): IInterface? { + return this@LogcatService + } + } + } + + // Export to UI + suspend fun pollLogEvent(offset: Long): CompletableDeferred { + val request = Request(offset, CompletableDeferred()) + + requestChannel.send(request) + + return request.response + } + + private fun launchProcessor() { + launch { + val pendingRequest: MutableList = mutableListOf() + + withContext(Dispatchers.Default) { + while (isActive) { + select { + logChannel.onReceive { + cache.addLast(it) + + if (cache.size() > MAX_CACHE_COUNT) { + cache.removeFromStart(1) + cacheOffset++ + } + } + requestChannel.onReceive { + pendingRequest.add(it) + } + } + + // Handle pending requests + val iterator = pendingRequest.iterator() + while (iterator.hasNext()) { + val request = iterator.next() + + if (request.offset >= cacheOffset + cache.size()) + continue + + val logs = mutableListOf() + + val responseOffset = max(cacheOffset, request.offset) + val begin = (responseOffset - cacheOffset).toInt() + + for (i in begin until cache.size()) + logs.add(cache[i]) + + request.response.complete(Response(responseOffset, logs)) + + iterator.remove() + } + } + } + } + } + + private fun launchSaveThread() { + launch { + withContext(Dispatchers.IO) { + logsDir.mkdirs() + try { + FileWriter(logsDir.resolve(entity.fileName)).buffered().use { output -> + val dateFormat = DateFormat.getDateFormat(this@LogcatService) + var offset = 0L + + output.write("# Logcat on ${dateFormat.format(entity.date)}") + output.newLine() + + while (isActive) { + val response = pollLogEvent(offset).await() + + if (response.offset != offset) { + output.write("# Lost ${response.offset - offset} items") + output.newLine() + } + + response.logs.forEach { + output.write( + LOG_CONTENT_FORMAT.format( + it.time, + it.level, + it.message + ) + ) + output.newLine() + } + + offset = response.offset + response.logs.size + } + } + } catch (e: IOException) { + Log.w("Logcat file write failure", e) + } + } + } + } + + private fun createNotificationChannel() { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + return + + NotificationManagerCompat.from(this) + .createNotificationChannel( + NotificationChannel( + NOTIFICATION_CHANNEL_ID, + getString(R.string.clash_logcat), + NotificationManager.IMPORTANCE_DEFAULT + ) + ) + } + + private fun showNotification() { + val notification = NotificationCompat + .Builder(this, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.drawable.ic_notification) + .setColor(getColor(R.color.colorAccentService)) + .setContentTitle(getString(R.string.clash_logcat)) + .setContentText(getString(R.string.capturing_clash_log)) + .setContentIntent( + PendingIntent.getActivity( + this, + NOTIFICATION_ID, + LogsActivity::class.intent + .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK), + PendingIntent.FLAG_UPDATE_CURRENT + ) + ) + .build() + + startForeground(NOTIFICATION_ID, notification) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/LogsActivity.kt b/app/src/main/java/com/github/kr328/clash/LogsActivity.kt new file mode 100644 index 0000000000..4ed2dbbce2 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/LogsActivity.kt @@ -0,0 +1,262 @@ +package com.github.kr328.clash + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.text.format.DateFormat +import android.util.TypedValue +import android.view.ViewGroup +import androidx.annotation.ColorInt +import androidx.appcompat.app.AlertDialog +import androidx.core.content.FileProvider +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.LinearLayoutManager +import com.github.kr328.clash.adapter.LogFileAdapter +import com.github.kr328.clash.design.common.Category +import com.github.kr328.clash.design.view.CommonUiLayout +import com.github.kr328.clash.model.LogFile +import com.github.kr328.clash.service.util.intent +import com.github.kr328.clash.service.util.startForegroundServiceCompat +import com.github.kr328.clash.utils.logsDir +import com.google.android.material.bottomsheet.BottomSheetDialog +import com.google.android.material.snackbar.Snackbar +import kotlinx.android.synthetic.main.activity_logs.* +import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.FileInputStream +import java.util.* + +class LogsActivity : BaseActivity() { + companion object { + const val REQUEST_CODE = 50000 + } + + private var lastWriteFile: LogFile? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_logs) + + setSupportActionBar(toolbar) + + if ( LogcatService.isServiceRunning ) { + startActivity(LogViewerActivity::class.intent) + finish() + return + } + + commonUi.build { + option( + title = getString(R.string.clash_logcat), + summary = getString(R.string.tap_to_start), + icon = getDrawable(R.drawable.ic_adb) + ) { + onClick { + startForegroundServiceCompat(LogcatService::class.intent) + + startActivity(LogViewerActivity::class.intent) + + finish() + } + } + category(text = getString(R.string.history), id = "history", showTopSeparator = true) + } + + clearAll.setOnClickListener { + showClearAllDialog() + } + + val adapter = LogFileAdapter(this@LogsActivity, + onItemClicked = { + startActivity(LogViewerActivity::class.intent + .setData(Uri.fromFile(logsDir.resolve(it.fileName)))) + }, + onMenuClicked = this::showMenu) + val layoutManager = LinearLayoutManager(this@LogsActivity) + + mainList.layoutManager = layoutManager + mainList.adapter = adapter + } + + override fun onStart() { + super.onStart() + + if ( LogcatService.isServiceRunning ) + return + + refreshList() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if ( requestCode == REQUEST_CODE ) { + if ( resultCode == Activity.RESULT_OK ) { + val url = data?.data ?: return + val file = lastWriteFile ?: return + + lastWriteFile = null + + launch { + withContext(Dispatchers.IO) { + contentResolver.openOutputStream(url)?.use { output -> + FileInputStream(logsDir.resolve(file.fileName)).use { input -> + input.copyTo(output) + } + } + } + + Snackbar.make(rootView, R.string.file_exported, Snackbar.LENGTH_LONG).show() + } + } + return + } + + super.onActivityResult(requestCode, resultCode, data) + } + + private fun refreshList() { + launch { + val files = withContext(Dispatchers.IO) { + (logsDir.listFiles() ?: emptyArray()) + .asSequence() + .filter { it.name.endsWith(".log") } + .map { LogFile.parseFromFileName(it.name) } + .filterNotNull() + .toList() + } + + if (files.isEmpty()) + commonUi.screen.requireElement("history").isHidden = true + + val adapter = mainList.adapter as LogFileAdapter + val old = adapter.fileList + + val result = withContext(Dispatchers.Default) { + DiffUtil.calculateDiff(object: DiffUtil.Callback() { + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return old[oldItemPosition].fileName == files[newItemPosition].fileName + } + + override fun getOldListSize(): Int { + return old.size + } + + override fun getNewListSize(): Int { + return files.size + } + + override fun areContentsTheSame( + oldItemPosition: Int, + newItemPosition: Int + ): Boolean { + return old[oldItemPosition] == files[newItemPosition] + } + }) + } + + adapter.fileList = files + result.dispatchUpdatesTo(adapter) + } + } + + private fun showClearAllDialog() { + AlertDialog.Builder(this) + .setTitle(R.string.delete_all_logs) + .setMessage(R.string.delete_all_logs_warn) + .setPositiveButton(R.string.ok) { _, _ -> deleteAllLogs() } + .setNegativeButton(R.string.cancel) { _, _ -> } + .show() + } + + private fun showMenu(logFile: LogFile) { + val dialog = BottomSheetDialog(this) + val menu = CommonUiLayout(this).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + @ColorInt + val errorColor = TypedValue().run { + theme.resolveAttribute(R.attr.colorError, this, true) + data + } + + menu.build { + option( + icon = getDrawable(R.drawable.ic_save), + title = getString(R.string.export)) { + onClick { + export(logFile) + + dialog.dismiss() + } + } + option( + icon = getDrawable(R.drawable.ic_delete_colorful), + title = getString(R.string.delete)) { + textColor = errorColor + + onClick { + delete(logFile) + + dialog.dismiss() + } + } + } + + dialog.dismissWithAnimation = true + dialog.setContentView(menu) + dialog.show() + } + + private fun deleteAllLogs() { + launch { + withContext(Dispatchers.IO) { + logsDir.deleteRecursively() + } + + refreshList() + } + } + + private fun export(file: LogFile) { + if ( lastWriteFile != null ) + return + + val d = Date(file.date) + val date = DateFormat.getDateFormat(this) + val time = DateFormat.getTimeFormat(this) + + val exportName = getString(R.string.format_export_log_name, date.format(d), time.format(d)) + + val intent = Intent(Intent.ACTION_CREATE_DOCUMENT) + .setType("text/plain") + .putExtra(Intent.EXTRA_TITLE, exportName) + + lastWriteFile = file + + startActivityForResult(intent, REQUEST_CODE) + } + + private fun delete(file: LogFile) { + val d = { + launch { + withContext(Dispatchers.IO) { + logsDir.resolve(file.fileName).delete() + } + + refreshList() + } + } + + AlertDialog.Builder(this) + .setTitle(R.string.delete_log) + .setMessage(getString(R.string.delete_log_warn, file.fileName)) + .setPositiveButton(R.string.ok) { _, _ -> d() } + .setNegativeButton(R.string.cancel) { _, _ -> } + .show() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 10879c0e5d..ee5ee5d118 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -47,6 +47,10 @@ class MainActivity : BaseActivity() { profiles.setOnClickListener { startActivity(ProfilesActivity::class.intent) } + + logs.setOnClickListener { + startActivity(LogsActivity::class.intent) + } } override fun onStart() { diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index 527b63992d..c2cf1e17f8 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -10,7 +10,6 @@ import com.github.kr328.clash.adapter.ProxyAdapter import com.github.kr328.clash.adapter.ProxyChipAdapter import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.Proxy -import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.preference.UiPreferences import com.github.kr328.clash.remote.withClash import com.github.kr328.clash.utils.PrefixMerger diff --git a/app/src/main/java/com/github/kr328/clash/adapter/LiveLogAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/LiveLogAdapter.kt new file mode 100644 index 0000000000..c050d6541d --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/adapter/LiveLogAdapter.kt @@ -0,0 +1,56 @@ +package com.github.kr328.clash.adapter + +import android.content.Context +import android.view.LayoutInflater +import android.view.ViewGroup +import androidx.collection.CircularArray +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.R +import com.github.kr328.clash.core.event.LogEvent + +class LiveLogAdapter(private val context: Context) : RecyclerView.Adapter() { + companion object { + const val MAX_LOG_ITEMS = 100 + } + + private val circularArray = CircularArray(MAX_LOG_ITEMS) + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): LogAdapter.Holder { + return LogAdapter.Holder( + LayoutInflater.from(context).inflate( + R.layout.adapter_log, + parent, + false + ) + ) + } + + override fun getItemCount(): Int { + return circularArray.size() + } + + override fun onBindViewHolder(holder: LogAdapter.Holder, position: Int) { + holder.bind(circularArray[position]) + } + + fun insertItems(i: List) { + val items = if ( i.size > MAX_LOG_ITEMS ) { + i.subList(i.size - MAX_LOG_ITEMS, i.size) + } + else i + + val predictSize = items.size + circularArray.size() + + if ( predictSize > MAX_LOG_ITEMS ) { + val removeSize = predictSize - MAX_LOG_ITEMS + notifyItemRangeRemoved(MAX_LOG_ITEMS - removeSize, removeSize) + circularArray.removeFromEnd(removeSize) + } + + items.forEach { + circularArray.addFirst(it) + } + + notifyItemRangeInserted(0, items.size) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt new file mode 100644 index 0000000000..d56062cd49 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/adapter/LogAdapter.kt @@ -0,0 +1,42 @@ +package com.github.kr328.clash.adapter + +import android.content.Context +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.R +import com.github.kr328.clash.core.event.LogEvent +import java.util.* + +class LogAdapter( + private val context: Context, + private val logs: List +) : RecyclerView.Adapter() { + class Holder(view: View) : RecyclerView.ViewHolder(view) { + private val level: TextView = view.findViewById(R.id.level) + private val time: TextView = view.findViewById(R.id.time) + private val payload: TextView = view.findViewById(R.id.payload) + private val timeFormat = DateFormat.getTimeFormat(view.context) + + fun bind(logEvent: LogEvent) { + level.text = logEvent.level.toString() + time.text = timeFormat.format(Date(logEvent.time)) + payload.text = logEvent.message + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + return Holder(LayoutInflater.from(context).inflate(R.layout.adapter_log, parent, false)) + } + + override fun getItemCount(): Int { + return logs.size + } + + override fun onBindViewHolder(holder: Holder, position: Int) { + holder.bind(logs[position]) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/LogFileAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/LogFileAdapter.kt new file mode 100644 index 0000000000..50f12299da --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/adapter/LogFileAdapter.kt @@ -0,0 +1,59 @@ +package com.github.kr328.clash.adapter + +import android.content.Context +import android.text.format.DateFormat +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import com.github.kr328.clash.R +import com.github.kr328.clash.model.LogFile +import java.util.* + +class LogFileAdapter( + private val context: Context, + private val onItemClicked: (LogFile) -> Unit, + private val onMenuClicked: (LogFile) -> Unit +) : RecyclerView.Adapter() { + var fileList: List = emptyList() + + private val dateFormat = DateFormat.getDateFormat(context) + private val timeFormat = DateFormat.getTimeFormat(context) + + class Holder(view: View) : RecyclerView.ViewHolder(view) { + val root: View = view.findViewById(R.id.root) + val fileName: TextView = view.findViewById(R.id.fileName) + val date: TextView = view.findViewById(R.id.date) + val menu: View = view.findViewById(R.id.menu) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder { + return Holder( + LayoutInflater.from(context).inflate( + R.layout.adapter_log_file, + parent, + false + ) + ) + } + + override fun getItemCount(): Int { + return fileList.size + } + + override fun onBindViewHolder(holder: Holder, position: Int) { + val current = fileList[position] + val date = Date(current.date) + + holder.fileName.text = current.fileName + holder.date.text = context.getString(R.string.format_date_time, + dateFormat.format(date), timeFormat.format(date)) + holder.menu.setOnClickListener { + onMenuClicked(current) + } + holder.root.setOnClickListener { + onItemClicked(current) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt index 935bd6ddec..d735031df0 100644 --- a/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt +++ b/app/src/main/java/com/github/kr328/clash/adapter/ProxyAdapter.kt @@ -137,11 +137,11 @@ class ProxyAdapter( val activeCache: MutableMap = mutableMapOf() newRenderList.forEachIndexed { index, it -> - when ( it ) { + when (it) { is ProxyGroupRenderInfo -> groupCache[it.name] = index is ProxyRenderInfo -> { - if ( it.info.active ) + if (it.info.active) activeCache[it.name] = index } } diff --git a/app/src/main/java/com/github/kr328/clash/model/LogFile.kt b/app/src/main/java/com/github/kr328/clash/model/LogFile.kt new file mode 100644 index 0000000000..2ff5852ccf --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/model/LogFile.kt @@ -0,0 +1,20 @@ +package com.github.kr328.clash.model + +data class LogFile(val fileName: String, val date: Long) { + companion object { + private val REGEX_FILE = Regex("clash-(\\d+).log") + private const val FORMAT_FILE_NAME = "clash-%d.log" + + fun parseFromFileName(fileName: String): LogFile? { + return REGEX_FILE.matchEntire(fileName)?.run { + LogFile(fileName, groupValues[1].toLong()) + } + } + + fun generate(date: Long = System.currentTimeMillis()): LogFile { + val fileName = FORMAT_FILE_NAME.format(date) + + return LogFile(fileName, date) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/utils/FileUtils.kt b/app/src/main/java/com/github/kr328/clash/utils/FileUtils.kt new file mode 100644 index 0000000000..0c07533f1a --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/utils/FileUtils.kt @@ -0,0 +1,8 @@ +package com.github.kr328.clash.utils + +import android.content.Context +import com.github.kr328.clash.Constants +import java.io.File + +val Context.logsDir: File + get() = (externalCacheDir ?: cacheDir).resolve(Constants.LOG_DIR_NAME) \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt b/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt index 659581da82..f5a42db5d7 100644 --- a/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt +++ b/app/src/main/java/com/github/kr328/clash/utils/ScrollBinding.kt @@ -3,7 +3,6 @@ package com.github.kr328.clash.utils import android.content.Context import android.util.DisplayMetrics import androidx.recyclerview.widget.LinearSmoothScroller -import com.github.kr328.clash.core.utils.Log import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.delay diff --git a/app/src/main/res/drawable/ic_adb.xml b/app/src/main/res/drawable/ic_adb.xml new file mode 100644 index 0000000000..d982428d13 --- /dev/null +++ b/app/src/main/res/drawable/ic_adb.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_adjust.xml b/app/src/main/res/drawable/ic_adjust.xml new file mode 100644 index 0000000000..44852aba68 --- /dev/null +++ b/app/src/main/res/drawable/ic_adjust.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_clear_all.xml b/app/src/main/res/drawable/ic_clear_all.xml new file mode 100644 index 0000000000..a9769c2ac3 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_all.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_foreground.xml b/app/src/main/res/drawable/ic_launcher_foreground.xml index a547804d45..7479384c0b 100644 --- a/app/src/main/res/drawable/ic_launcher_foreground.xml +++ b/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -6,7 +6,7 @@ diff --git a/app/src/main/res/drawable/ic_save.xml b/app/src/main/res/drawable/ic_save.xml index faa65b0a82..a04303a272 100644 --- a/app/src/main/res/drawable/ic_save.xml +++ b/app/src/main/res/drawable/ic_save.xml @@ -1,9 +1,9 @@ + android:width="24dp" + android:height="24dp" + android:viewportWidth="24.0" + android:viewportHeight="24.0"> + android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/> diff --git a/app/src/main/res/drawable/ic_stop.xml b/app/src/main/res/drawable/ic_stop.xml new file mode 100644 index 0000000000..23529b7308 --- /dev/null +++ b/app/src/main/res/drawable/ic_stop.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/layout/activity_log_viewer.xml b/app/src/main/res/layout/activity_log_viewer.xml new file mode 100644 index 0000000000..16ef94e5a1 --- /dev/null +++ b/app/src/main/res/layout/activity_log_viewer.xml @@ -0,0 +1,35 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_logs.xml b/app/src/main/res/layout/activity_logs.xml new file mode 100644 index 0000000000..6d436e6ed3 --- /dev/null +++ b/app/src/main/res/layout/activity_logs.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 20cb227cdb..30af45041e 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -88,6 +88,7 @@ app:summary="@string/not_selected" /> - + android:background="@drawable/ic_logs" /> - + android:background="@drawable/ic_settings" /> - + android:background="@drawable/ic_feedback" /> + android:text="@string/support" /> - + android:background="@drawable/ic_about" /> + android:layout_gravity="end"> + android:foreground="@drawable/ic_save" + android:layout_width="25dp" + android:layout_height="25dp" + android:layout_gravity="center" + android:background="?attr/selectableItemBackgroundBorderless" + android:focusable="true" + android:clickable="true"/> diff --git a/app/src/main/res/layout/adapter_log.xml b/app/src/main/res/layout/adapter_log.xml new file mode 100644 index 0000000000..9492f03e3f --- /dev/null +++ b/app/src/main/res/layout/adapter_log.xml @@ -0,0 +1,29 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/adapter_log_file.xml b/app/src/main/res/layout/adapter_log_file.xml new file mode 100644 index 0000000000..743cb67070 --- /dev/null +++ b/app/src/main/res/layout/adapter_log_file.xml @@ -0,0 +1,43 @@ + + + + + + + + + + + + \ 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 d719eee63f..c2e523101a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -16,6 +16,7 @@ Logs Settings Feedback + Support About Unknown @@ -49,6 +50,8 @@ Update Edit Delete + Export + Send Properties Duplicate Reset Providers @@ -84,4 +87,18 @@ OK Cancel + Clash Logcat + Capturing clash core log + History + Enabled + Log Viewer + %s %s + %s %s.log + + Delete All Logs + All historical logs will *LOST* + Delete Log + %s will be deleted + Open Log File Failure + File Exported diff --git a/app/src/main/res/xml/export_contents.xml b/app/src/main/res/xml/export_contents.xml new file mode 100644 index 0000000000..c12988dcf2 --- /dev/null +++ b/app/src/main/res/xml/export_contents.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/xml/full_backup_content.xml b/app/src/main/res/xml/full_backup_content.xml index 38f025dcf4..9ce200ab4b 100644 --- a/app/src/main/res/xml/full_backup_content.xml +++ b/app/src/main/res/xml/full_backup_content.xml @@ -3,4 +3,5 @@ + \ No newline at end of file diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go index a989e4f63f..54e5baf812 100644 --- a/core/src/main/golang/profile/download.go +++ b/core/src/main/golang/profile/download.go @@ -7,6 +7,7 @@ import ( "net" "net/http" "os" + "syscall" "github.com/Dreamacro/clash/adapters/inbound" "github.com/Dreamacro/clash/component/socks5" @@ -59,7 +60,10 @@ func DownloadAndCheck(url, output, baseDir string) error { } func ReadAndCheck(fd int, output, baseDir string) error { + syscall.SetNonblock(fd, true) + file := os.NewFile(uintptr(fd), "/dev/null") + defer file.Close() data, err := ioutil.ReadAll(file) if err != nil { diff --git a/design/src/main/AndroidManifest.xml b/design/src/main/AndroidManifest.xml index 2b1af1ddeb..c6fb966459 100644 --- a/design/src/main/AndroidManifest.xml +++ b/design/src/main/AndroidManifest.xml @@ -1,2 +1 @@ - + diff --git a/design/src/main/java/com/github/kr328/clash/design/common/Base.kt b/design/src/main/java/com/github/kr328/clash/design/common/Base.kt index e801ccbec1..cc523fc0b6 100644 --- a/design/src/main/java/com/github/kr328/clash/design/common/Base.kt +++ b/design/src/main/java/com/github/kr328/clash/design/common/Base.kt @@ -32,5 +32,8 @@ abstract class Base(val screen: CommonUiScreen) { abstract val view: View abstract fun saveState(bundle: Bundle) abstract fun restoreState(bundle: Bundle) - protected abstract fun applyAttribute(enabled: Boolean, hidden: Boolean) + protected open fun applyAttribute(enabled: Boolean, hidden: Boolean) { + view.isEnabled = enabled + view.visibility = if ( hidden ) View.GONE else View.VISIBLE + } } \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/Category.kt b/design/src/main/java/com/github/kr328/clash/design/common/Category.kt new file mode 100644 index 0000000000..088bb7ef89 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/common/Category.kt @@ -0,0 +1,35 @@ +package com.github.kr328.clash.design.common + +import android.os.Bundle +import android.view.LayoutInflater +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) + + private val vText: TextView = view.findViewById(R.id.text) + private val vTopSeparator: View = view.findViewById(R.id.topSeparator) + private val vBottomSeparator: View = view.findViewById(R.id.bottomSeparator) + + var text: CharSequence + get() = vText.text + set(value) { vText.text = value } + + var showTopSeparator: Boolean + get() = vTopSeparator.visibility == View.VISIBLE + set(value) { + vTopSeparator.visibility = + 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 + } + + override fun saveState(bundle: Bundle) {} + override fun restoreState(bundle: Bundle) {} +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt b/design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt index 1642ccb4ae..fa8b4314c6 100644 --- a/design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt +++ b/design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt @@ -2,6 +2,7 @@ package com.github.kr328.clash.design.common import android.content.Context import android.graphics.drawable.Drawable +import android.view.View class CommonUiBuilder(val screen: CommonUiScreen) { val context: Context @@ -50,4 +51,34 @@ class CommonUiBuilder(val screen: CommonUiScreen) { screen.addElement(option) } + + fun category( + text: String = "", + showTopSeparator: Boolean = false, + showBottomSeparator: Boolean = false, + id: String? = null, + setup: Category.() -> Unit = {} + ) { + val category = Category(screen) + + category.text = text + category.showTopSeparator = showTopSeparator + category.showBottomSeparator = showBottomSeparator + category.id = id + + setup(category) + + screen.addElement(category) + } + + fun custom( + view: View, + setup: Custom.() -> Unit + ) { + val custom = Custom(screen, view) + + setup(custom) + + screen.addElement(custom) + } } \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/Custom.kt b/design/src/main/java/com/github/kr328/clash/design/common/Custom.kt new file mode 100644 index 0000000000..96fb78adc5 --- /dev/null +++ b/design/src/main/java/com/github/kr328/clash/design/common/Custom.kt @@ -0,0 +1,31 @@ +package com.github.kr328.clash.design.common + +import android.os.Bundle +import android.view.View + +class Custom(screen: CommonUiScreen, override val view: View): Base(screen) { + private var saveStateHandler: (Bundle) -> Unit = {} + private var restoreStateHandler: (Bundle) -> Unit = {} + private var applyAttributeHandler: (Boolean, Boolean) -> Unit = + { enable, hidden -> super.applyAttribute(enable, hidden) } + + fun onSaveState(handler: (Bundle) -> Unit) { + this.saveStateHandler = handler + } + + fun onRestoreState(handler: (Bundle) -> Unit) { + this.restoreStateHandler = handler + } + + fun onApplyAttribute(handler: (Boolean, Boolean) -> Unit) { + this.applyAttributeHandler = handler + } + + override fun saveState(bundle: Bundle) { + saveStateHandler(bundle) + } + + override fun restoreState(bundle: Bundle) { + restoreStateHandler(bundle) + } +} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/Option.kt b/design/src/main/java/com/github/kr328/clash/design/common/Option.kt index 90f3661b04..588ff151e6 100644 --- a/design/src/main/java/com/github/kr328/clash/design/common/Option.kt +++ b/design/src/main/java/com/github/kr328/clash/design/common/Option.kt @@ -48,11 +48,6 @@ class Option(screen: CommonUiScreen): Base(screen) { this.click = block } - override fun applyAttribute(enabled: Boolean, hidden: Boolean) { - view.isEnabled = enabled - view.visibility = if ( hidden ) View.GONE else View.VISIBLE - } - override fun saveState(bundle: Bundle) {} override fun restoreState(bundle: Bundle) {} } \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt b/design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt index 2872c68b68..a9448c3409 100644 --- a/design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt +++ b/design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt @@ -74,11 +74,6 @@ class TextInput(screen: CommonUiScreen) : Base(screen) { content = content } - override fun applyAttribute(enabled: Boolean, hidden: Boolean) { - view.isEnabled = enabled - view.visibility = if (hidden) View.GONE else View.VISIBLE - } - override fun saveState(bundle: Bundle) { if ( id == null ) return diff --git a/design/src/main/res/layout/view_category.xml b/design/src/main/res/layout/view_category.xml new file mode 100644 index 0000000000..d45c58a097 --- /dev/null +++ b/design/src/main/res/layout/view_category.xml @@ -0,0 +1,27 @@ + + + + + + \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt index 0958fa583f..deeccb9921 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt @@ -59,6 +59,10 @@ class ClashManager(context: Context, parent: CoroutineScope) : require(callback != null) Clash.openLogEvent().apply { + callback.asBinder()?.linkToDeath({ + close() + }, 0) + onEvent { try { callback.send(ParcelableContainer(it)) diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt index a097441012..9772c83bf0 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt @@ -64,10 +64,12 @@ class ProfileProcessor(private val context: Context) { baseDir.mkdirs() if (source.scheme == "content" || source.scheme == "file") { - val fd = context.contentResolver.openFileDescriptor(source, "r") + val parcelFileDescriptor = context.contentResolver.openFileDescriptor(source, "r") ?: throw FileNotFoundException("Unable to open file $source") - Clash.downloadProfile(fd.fd, target, baseDir).await() + val fd = parcelFileDescriptor.detachFd() + + Clash.downloadProfile(fd, target, baseDir).await() } else { Clash.downloadProfile(source.toString(), target, baseDir).await() } 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 d5766eac8f..c706430fc5 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 @@ -4,18 +4,17 @@ import androidx.room.* @Entity( tableName = "profile_select_proxies", - indices = [Index("profile_id")], foreignKeys = [ForeignKey( entity = ClashProfileEntity::class, childColumns = ["profile_id"], parentColumns = ["id"], onDelete = ForeignKey.CASCADE, onUpdate = ForeignKey.CASCADE - )] + )], + primaryKeys = ["profile_id", "proxy"] ) data class ClashProfileProxyEntity( @ColumnInfo(name = "profile_id") val profileId: Long, @ColumnInfo(name = "proxy") val proxy: String, - @ColumnInfo(name = "selected") val selected: String, - @ColumnInfo(name = "id") @PrimaryKey(autoGenerate = true) val id: Int = 0 + @ColumnInfo(name = "selected") val selected: String ) \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt index 6b658c6141..e9b5c2a240 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt @@ -16,8 +16,6 @@ suspend fun broadcastProfileChanged(context: Context) { .putExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE, active) context.sendBroadcastSelf(intent) - - Log.d("Broadcasting $intent") } fun broadcastClashStarted(context: Context) { From 8a4149c8000e7d8313780052f964955100102cf4 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 18 Feb 2020 01:16:41 +0800 Subject: [PATCH 091/358] fix broadcast --- .../com/github/kr328/clash/BaseActivity.kt | 13 ++++++-- .../com/github/kr328/clash/MainActivity.kt | 31 +++++++++++++++++++ .../github/kr328/clash/ProfilesActivity.kt | 6 +--- .../github/kr328/clash/remote/Broadcasts.kt | 14 +++++++-- app/src/main/res/layout/activity_main.xml | 8 ++--- app/src/main/res/values/strings.xml | 3 ++ .../kr328/clash/service/ClashService.kt | 6 ++-- .../com/github/kr328/clash/service/Intents.kt | 8 ++--- .../clash/service/util/BroadcastUtils.kt | 14 ++++++--- 9 files changed, 79 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index ce6cf86de2..ca4ca346c7 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -40,9 +40,15 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() } } - override fun onProfileChanged(active: ClashProfileEntity?) { + override fun onProfileChanged() { launch { - onClashProfileChanged(active) + onClashProfileChanged() + } + } + + override fun onProfileLoaded(profileEntity: ClashProfileEntity) { + launch { + onClashProfileLoaded(profileEntity) } } } @@ -60,7 +66,8 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() open suspend fun onClashStarted() {} open suspend fun onClashStopped(reason: String?) {} - open suspend fun onClashProfileChanged(active: ClashProfileEntity?) {} + open suspend fun onClashProfileChanged() {} + open suspend fun onClashProfileLoaded(profile: ClashProfileEntity) {} override fun setContentView(layoutResID: Int) { val base = CoordinatorLayout(this).apply { diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index ee5ee5d118..44e621ae56 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -5,9 +5,12 @@ import android.content.Intent import android.net.VpnService import android.os.Bundle import android.view.View +import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.utils.asBytesString import com.github.kr328.clash.remote.withClash +import com.github.kr328.clash.remote.withProfile import com.github.kr328.clash.service.ClashService +import com.github.kr328.clash.service.data.ClashProfileEntity import com.github.kr328.clash.service.util.intent import com.github.kr328.clash.service.util.startForegroundServiceCompat import kotlinx.android.synthetic.main.activity_main.* @@ -86,6 +89,10 @@ class MainActivity : BaseActivity() { makeSnackbarException(getString(R.string.clash_start_failure), reason) } + override suspend fun onClashProfileLoaded(profile: ClashProfileEntity) { + updateClashStatus() + } + private fun startBandwidthPolling() { if (bandwidthJob != null) return @@ -131,5 +138,29 @@ class MainActivity : BaseActivity() { proxies.visibility = View.GONE } + + launch { + val general = withClash { + queryGeneral() + } + val active = withProfile { + queryActiveProfile() + } + + val modeResId = when ( general.mode ) { + General.Mode.DIRECT -> R.string.direct_mode + General.Mode.GLOBAL -> R.string.global_mode + General.Mode.RULE -> R.string.rule_mode + } + + val profileString = + if ( active == null ) + getText(R.string.not_selected) + else + getString(R.string.format_profile_activated, active.name) + + proxies.summary = getText(modeResId) + profiles.summary = profileString + } } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 0519e17219..2044432f4a 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -79,11 +79,7 @@ class ProfilesActivity : BaseActivity(), ProfileAdapter.Callback, ProfilesMenu.C super.onActivityResult(requestCode, resultCode, data) } - override suspend fun onClashProfileChanged(active: ClashProfileEntity?) { - super.onClashProfileChanged(active) - - Log.d("Broadcast received") - + override suspend fun onClashProfileChanged() { reloadProfiles() } diff --git a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt index d5b8a7433a..ac6e9120c6 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/Broadcasts.kt @@ -17,7 +17,8 @@ object Broadcasts { interface Receiver { fun onStarted() fun onStopped(cause: String?) - fun onProfileChanged(active: ClashProfileEntity?) + fun onProfileChanged() + fun onProfileLoaded(profileEntity: ClashProfileEntity) } var clashRunning: Boolean = false @@ -45,8 +46,16 @@ object Broadcasts { } Intents.INTENT_ACTION_PROFILE_CHANGED -> receivers.forEach { - it.onProfileChanged(intent.getParcelableExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE)) + it.onProfileChanged() } + Intents.INTENT_ACTION_PROFILE_LOADED -> { + val profile = intent + .getParcelableExtra(Intents.INTENT_EXTRA_PROFILE) ?: return + + receivers.forEach { + it.onProfileLoaded(profile) + } + } } } } @@ -66,6 +75,7 @@ object Broadcasts { addAction(Intents.INTENT_ACTION_PROFILE_CHANGED) addAction(Intents.INTENT_ACTION_CLASH_STOPPED) addAction(Intents.INTENT_ACTION_CLASH_STARTED) + addAction(Intents.INTENT_ACTION_PROFILE_LOADED) }) val pong = application.contentResolver.call( diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 30af45041e..cdf67a84a6 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -16,12 +16,12 @@ @@ -32,7 +32,7 @@ android:textColor="?attr/colorPrimary" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginStart="25dp" /> + android:layout_marginStart="22.5dp" /> Proxy Direct Mode + Rule Mode + Global Mode Profiles Not selected + %s Activated Logs Settings 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 ef033c9fc6..eca7856d99 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 @@ -92,9 +92,9 @@ class ClashService : BaseService() { super.onDestroy() } - private suspend fun reloadProfile() = withContext(Dispatchers.IO) { + private suspend fun reloadProfile() { val active = ClashDatabase.getInstance(service).openClashProfileDao() - .queryActiveProfile() ?: return@withContext stopSelf("Empty active profile") + .queryActiveProfile() ?: return stopSelf("Empty active profile") try { Clash.loadProfile( @@ -108,6 +108,8 @@ class ClashService : BaseService() { } notification.setProfile(active.name) + + broadcastProfileLoaded(this, active) } catch (e: Exception) { stopSelf("Load profile failure") } diff --git a/service/src/main/java/com/github/kr328/clash/service/Intents.kt b/service/src/main/java/com/github/kr328/clash/service/Intents.kt index e1d487dbff..626e815b35 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Intents.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Intents.kt @@ -1,8 +1,6 @@ package com.github.kr328.clash.service object Intents { - const val INTENT_ACTION_BIND_TUN_SERVICE = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.bind.tun" const val INTENT_ACTION_CLASH_STARTED = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.started" const val INTENT_ACTION_CLASH_STOPPED = @@ -13,11 +11,13 @@ object Intents { "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.enqueue.request" const val INTENT_ACTION_PROFILE_SETUP = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.setup" + const val INTENT_ACTION_PROFILE_LOADED = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.loaded" const val INTENT_EXTRA_CLASH_STOP_REASON = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason" - const val INTENT_EXTRA_PROFILE_ACTIVE = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.active.name" + const val INTENT_EXTRA_PROFILE = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile" const val INTENT_EXTRA_PROFILE_REQUEST = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.profile.request" } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt index e9b5c2a240..37e6899213 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt @@ -2,18 +2,24 @@ package com.github.kr328.clash.service.util import android.content.Context import android.content.Intent -import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.Intents import com.github.kr328.clash.service.data.ClashDatabase +import com.github.kr328.clash.service.data.ClashProfileEntity +import com.github.kr328.clash.service.data.ClashProfileProxyEntity fun Context.sendBroadcastSelf(intent: Intent) { this.sendBroadcast(intent.setPackage(this.packageName)) } -suspend fun broadcastProfileChanged(context: Context) { - val active = ClashDatabase.getInstance(context).openClashProfileDao().queryActiveProfile() +fun broadcastProfileChanged(context: Context) { val intent = Intent(Intents.INTENT_ACTION_PROFILE_CHANGED) - .putExtra(Intents.INTENT_EXTRA_PROFILE_ACTIVE, active) + + context.sendBroadcastSelf(intent) +} + +fun broadcastProfileLoaded(context: Context, profileEntity: ClashProfileEntity) { + val intent = Intent(Intents.INTENT_ACTION_PROFILE_LOADED) + .putExtra(Intents.INTENT_EXTRA_PROFILE, profileEntity) context.sendBroadcastSelf(intent) } From 3e7d7f5d60fc6b8dd0f4acf01e4986fcd538805c Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 18 Feb 2020 15:16:43 +0800 Subject: [PATCH 092/358] add dns patch --- .../com/github/kr328/clash/LogcatService.kt | 4 +- app/src/main/res/layout/activity_main.xml | 2 +- app/src/main/res/values/strings.xml | 2 +- core/src/main/golang/bridge/general.go | 10 +-- core/src/main/golang/bridge/profiles.go | 18 ++++ core/src/main/golang/bridge/proxies.go | 6 +- core/src/main/golang/bridge/tun.go | 28 ++++++ core/src/main/golang/clash | 2 +- core/src/main/golang/go.sum | 13 +++ core/src/main/golang/profile/download.go | 2 +- core/src/main/golang/profile/load.go | 85 +++++++++---------- core/src/main/golang/profile/patch.go | 45 ++++++++++ core/src/main/golang/tun/tun.go | 4 +- .../java/com/github/kr328/clash/core/Clash.kt | 21 ++++- .../kr328/clash/service/ClashNotification.kt | 14 ++- .../kr328/clash/service/ClashService.kt | 9 +- .../com/github/kr328/clash/service/Intents.kt | 2 + .../github/kr328/clash/service/Settings.kt | 2 + .../github/kr328/clash/service/TunService.kt | 33 +++++-- .../service/net/DefaultNetworkChannel.kt | 64 ++++++++------ .../clash/service/util/BroadcastUtils.kt | 4 + 21 files changed, 271 insertions(+), 99 deletions(-) create mode 100644 core/src/main/golang/profile/patch.go diff --git a/app/src/main/java/com/github/kr328/clash/LogcatService.kt b/app/src/main/java/com/github/kr328/clash/LogcatService.kt index ed2d1afd43..5a64cff54c 100644 --- a/app/src/main/java/com/github/kr328/clash/LogcatService.kt +++ b/app/src/main/java/com/github/kr328/clash/LogcatService.kt @@ -95,6 +95,8 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface { } override fun onDestroy() { + logChannel.close() + cancel() stopForeground(true) @@ -231,7 +233,7 @@ class LogcatService : Service(), CoroutineScope by MainScope(), IInterface { .setSmallIcon(R.drawable.ic_notification) .setColor(getColor(R.color.colorAccentService)) .setContentTitle(getString(R.string.clash_logcat)) - .setContentText(getString(R.string.capturing_clash_log)) + .setContentText(getString(R.string.running)) .setContentIntent( PendingIntent.getActivity( this, diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index cdf67a84a6..6c428a9527 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -16,7 +16,7 @@ Cancel Clash Logcat - Capturing clash core log + Capturing logs History Enabled Log Viewer diff --git a/core/src/main/golang/bridge/general.go b/core/src/main/golang/bridge/general.go index 312768660f..07a77b61cf 100644 --- a/core/src/main/golang/bridge/general.go +++ b/core/src/main/golang/bridge/general.go @@ -16,9 +16,9 @@ func QueryGeneral() *TunnelGeneral { result := &TunnelGeneral{} g := executor.GetGeneral() - t := tunnel.Instance() + m := tunnel.Mode() - result.Mode = t.Mode().String() + result.Mode = m.String() result.HTTPPort = g.Port result.SocksPort = g.SocksPort result.RedirectPort = g.RedirPort @@ -29,10 +29,10 @@ func QueryGeneral() *TunnelGeneral { func SetProxyMode(mode string) { switch mode { case "Direct": - tunnel.Instance().SetMode(tunnel.Direct) + tunnel.SetMode(tunnel.Direct) case "Global": - tunnel.Instance().SetMode(tunnel.Global) + tunnel.SetMode(tunnel.Global) case "Rule": - tunnel.Instance().SetMode(tunnel.Rule) + tunnel.SetMode(tunnel.Rule) } } diff --git a/core/src/main/golang/bridge/profiles.go b/core/src/main/golang/bridge/profiles.go index 2df452af99..3b17dc4902 100644 --- a/core/src/main/golang/bridge/profiles.go +++ b/core/src/main/golang/bridge/profiles.go @@ -1,9 +1,27 @@ package bridge import ( + "strings" + "github.com/kr328/cfa/profile" ) +func ResetDnsAppend(dns string) { + if len(dns) == 0 { + profile.NameServersAppend = make([]string, 0) + } else { + profile.NameServersAppend = strings.Split(dns, ",") + } +} + +func SetDnsOverrideEnabled(enabled bool) { + if enabled { + profile.DnsPatch = profile.OptionalDnsPatch + } else { + profile.DnsPatch = nil + } +} + func LoadProfileFile(path, baseDir string, callback DoneCallback) { go func() { call(profile.LoadFromFile(path, baseDir), callback) diff --git a/core/src/main/golang/bridge/proxies.go b/core/src/main/golang/bridge/proxies.go index 118c31c1d3..bcb3752a0c 100644 --- a/core/src/main/golang/bridge/proxies.go +++ b/core/src/main/golang/bridge/proxies.go @@ -51,7 +51,7 @@ func StartUrlTest(group string, callback DoneCallback) { go func() { defer callback.Done() - p := tunnel.Instance().Proxies()[group] + p := tunnel.Proxies()[group] pa, ok := p.(*outbound.Proxy) if !ok { @@ -88,7 +88,7 @@ func StartUrlTest(group string, callback DoneCallback) { } func QueryAllProxyGroups(collection ProxyGroupCollection) { - ps := tunnel.Instance().Proxies() + ps := tunnel.Proxies() for _, p := range ps { pa, ok := p.(*outbound.Proxy) @@ -144,7 +144,7 @@ func QueryAllProxyGroups(collection ProxyGroupCollection) { } func SetSelectedProxy(name, proxy string) bool { - p := tunnel.Instance().Proxies()[name] + p := tunnel.Proxies()[name] if p == nil { return false } diff --git a/core/src/main/golang/bridge/tun.go b/core/src/main/golang/bridge/tun.go index 770474ecb1..a081e30882 100644 --- a/core/src/main/golang/bridge/tun.go +++ b/core/src/main/golang/bridge/tun.go @@ -1,15 +1,43 @@ package bridge import ( + "net" + "syscall" + + "github.com/Dreamacro/clash/component/dialer" "github.com/kr328/cfa/tun" ) type TunCallback interface { + OnCreateSocket(fd int) OnStop() } var callback TunCallback +func init() { + dialer.DialerHook = onNewDialer + dialer.ListenConfigHook = onNewListenConfig +} + +func onNewDialer(dialer *net.Dialer) { + dialer.Control = onNewSocket +} + +func onNewListenConfig(listen *net.ListenConfig) { + listen.Control = onNewSocket +} + +func onNewSocket(network, address string, c syscall.RawConn) error { + if cb := callback; cb != nil { + c.Control(func(fd uintptr) { + cb.OnCreateSocket(int(fd)) + }) + } + + return nil +} + func StartTunDevice(fd, mtu int, dns string, cb TunCallback) error { callback = cb diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash index 1de786635c..e0e7605d1a 160000 --- a/core/src/main/golang/clash +++ b/core/src/main/golang/clash @@ -1 +1 @@ -Subproject commit 1de786635ce775b7053ae68dd73ff916bfb146eb +Subproject commit e0e7605d1a5a93559281e8fea0a51043b71fabbe diff --git a/core/src/main/golang/go.sum b/core/src/main/golang/go.sum index be4383c0e8..daffa0415f 100644 --- a/core/src/main/golang/go.sum +++ b/core/src/main/golang/go.sum @@ -19,6 +19,8 @@ github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs= github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= +github.com/go-chi/chi v4.0.3+incompatible h1:gakN3pDJnzZN5jqFV2TEdF66rTfKeITyR8qu6ekICEY= +github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= github.com/go-chi/cors v1.0.0 h1:e6x8k7uWbUwYs+aXDoiUzeQFT6l0cygBYyNhD7/1Tg0= github.com/go-chi/cors v1.0.0/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I= github.com/go-chi/render v1.0.1 h1:4/5tis2cKaNdnv9zFLfXzcquC9HbeZgCnxGnKrltBS8= @@ -50,6 +52,8 @@ github.com/miekg/dns v1.1.24 h1:6G8Eop/HM8hpagajbn0rFQvAKZWiiCa8P6N2I07+wwI= github.com/miekg/dns v1.1.24/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.26 h1:gPxPSwALAeHJSjarOs00QjVdV9QoBvc1D2ujQUr5BzU= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= +github.com/miekg/dns v1.1.27 h1:aEH/kqUzUxGJ/UHcEKdJY+ugH6WEzsEBBSPa8zuy1aM= +github.com/miekg/dns v1.1.27/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= github.com/oschwald/geoip2-golang v1.2.1 h1:3iz+jmeJc6fuCyWeKgtXSXu7+zvkxJbHFXkMT5FVebU= github.com/oschwald/geoip2-golang v1.2.1/go.mod h1:0LTTzix/Ao1uMvOhAV4iLU0Lz7eCrP94qZWBTDKf0iE= github.com/oschwald/geoip2-golang v1.3.0 h1:D+Hsdos1NARPbzZ2aInUHZL+dApIzo8E0ErJVsWcku8= @@ -85,6 +89,8 @@ golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1 h1:anGSYQpPhQwXlwsu5wmfq0 golang.org/x/crypto v0.0.0-20191128160524-b544559bb6d1/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6 h1:Sy5bstxEqwwbYs6n0/pBuxKENqOeZUgD45Gp3Q3pqLg= +golang.org/x/crypto v0.0.0-20200214034016-1d94cc7ab1c6/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56 h1:estk1glOnSVeJ9tdEZZc5mAMDZk5lNJNyJ6DvrBkTEU= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= @@ -95,6 +101,7 @@ golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d h1:LlA9R5JFi974qK4gm9FRK1 golang.org/x/mobile v0.0.0-20191210151939-1a1fef82734d/go.mod h1:p895TfNkDgPEmEQrNiOtIl3j98d/tGU95djDj7NfyjQ= golang.org/x/mod v0.1.0 h1:sfUMP1Gu8qASkorDVjnMuvgJzwFbTZSeXFiGBYAVdl4= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -108,6 +115,8 @@ golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663 h1:Dd5RoEW+yQi+9DMybroBctIdy golang.org/x/net v0.0.0-20191207000613-e7e4b65ae663/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2 h1:CCH4IOTTfewWjGOlSp+zGcjutRKlBEZQ6wTn8ozI/nI= +golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= @@ -133,8 +142,10 @@ golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190909214602-067311248421 h1:NmmWqJbt02YJHmp4A4gBXvsXXIzzixjzE1y6PKUyIjk= golang.org/x/tools v0.0.0-20190909214602-067311248421/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/eapache/channels.v1 v1.1.0 h1:5bGAyKKvyCTWjSj7mhefG6Lc68VyN4MH1v8/7OoeeB4= @@ -143,3 +154,5 @@ gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/core/src/main/golang/profile/download.go b/core/src/main/golang/profile/download.go index 54e5baf812..99ec0cf673 100644 --- a/core/src/main/golang/profile/download.go +++ b/core/src/main/golang/profile/download.go @@ -26,7 +26,7 @@ var client = &http.Client{ client, server := net.Pipe() - tunnel.Instance().Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP, constant.TCP)) + tunnel.Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP, constant.TCP)) go func() { if ctx == nil || ctx.Done() == nil { diff --git a/core/src/main/golang/profile/load.go b/core/src/main/golang/profile/load.go index c889fe7e62..ea38bc7283 100644 --- a/core/src/main/golang/profile/load.go +++ b/core/src/main/golang/profile/load.go @@ -3,9 +3,7 @@ package profile import ( "fmt" "io/ioutil" - "net" - "github.com/Dreamacro/clash/component/fakeip" "github.com/Dreamacro/clash/config" "github.com/Dreamacro/clash/constant" "github.com/Dreamacro/clash/dns" @@ -34,6 +32,31 @@ Rule: - 'MATCH,DIRECT' ` +func init() { + defaultNameServers := []string{ + "223.5.5.5", + "119.29.29.29", + "1.1.1.1", + "208.67.222.222", + } + + OptionalDnsPatch = &config.RawDNS{ + Enable: true, + IPv6: true, + NameServer: defaultNameServers, + Fallback: []string{}, + FallbackFilter: config.RawFallbackFilter{ + GeoIP: false, + IPCIDR: []string{}, + }, + Listen: ":0", + EnhancedMode: dns.FAKEIP, + FakeIPRange: "198.18.0.0/16", + FakeIPFilter: []string{}, + DefaultNameserver: defaultNameServers, + } +} + // LoadDefault - load default configure func LoadDefault() { defaultC, err := parseConfig([]byte(defaultConfig), constant.Path.HomeDir()) @@ -42,6 +65,9 @@ func LoadDefault() { return } + DnsPatch = nil + NameServersAppend = make([]string, 0) + executor.ApplyConfig(defaultC, true) tun.ResetDnsRedirect() @@ -59,49 +85,11 @@ func LoadFromFile(path, baseDir string) error { return err } - executor.ApplyConfig(cfg, true) - - if dns.DefaultResolver == nil && cfg.DNS.Enable { - c := cfg.DNS - - r := dns.New(dns.Config{ - Main: c.NameServer, - Fallback: c.Fallback, - IPv6: c.IPv6, - EnhancedMode: c.EnhancedMode, - Pool: c.FakeIPRange, - FallbackFilter: dns.FallbackFilter{ - GeoIP: c.FallbackFilter.GeoIP, - IPCIDR: c.FallbackFilter.IPCIDR, - }, - }) - - dns.DefaultResolver = r + for _, ns := range cfg.DNS.NameServer { + log.Infoln("DNS: %s", ns.Addr) } - if dns.DefaultResolver == nil { - _, ipnet, _ := net.ParseCIDR("198.18.0.1/16") - pool, _ := fakeip.New(ipnet, 1000, nil) - - var defaultDNSResolver = dns.New(dns.Config{ - Main: []dns.NameServer{ - dns.NameServer{Net: "tcp", Addr: "1.1.1.1:53"}, - dns.NameServer{Net: "tcp", Addr: "208.67.222.222:53"}, - dns.NameServer{Net: "", Addr: "119.29.29.29:53"}, - dns.NameServer{Net: "", Addr: "223.5.5.5:53"}, - }, - Fallback: make([]dns.NameServer, 0), - IPv6: false, - EnhancedMode: dns.FAKEIP, - Pool: pool, - FallbackFilter: dns.FallbackFilter{ - GeoIP: false, - IPCIDR: make([]*net.IPNet, 0), - }, - }) - - dns.DefaultResolver = defaultDNSResolver - } + executor.ApplyConfig(cfg, true) tun.ResetDnsRedirect() @@ -120,5 +108,14 @@ func parseConfig(data []byte, baseDir string) (*config.Config, error) { raw.ExternalController = "" raw.Rule = append([]string{fmt.Sprintf("IP-CIDR,%s,REJECT", tunAddress)}, raw.Rule...) - return config.ParseRawConfig(raw, baseDir) + patchRawConfig(raw) + + cfg, err := config.ParseRawConfig(raw, baseDir) + if err != nil { + return nil, err + } + + patchConfig(cfg) + + return cfg, nil } diff --git a/core/src/main/golang/profile/patch.go b/core/src/main/golang/profile/patch.go new file mode 100644 index 0000000000..f50c46e498 --- /dev/null +++ b/core/src/main/golang/profile/patch.go @@ -0,0 +1,45 @@ +package profile + +import ( + "github.com/Dreamacro/clash/component/fakeip" + "github.com/Dreamacro/clash/config" +) + +var ( + OptionalDnsPatch *config.RawDNS + DnsPatch *config.RawDNS + NameServersAppend []string + + cachedPool *fakeip.Pool +) + +func patchRawConfig(rawConfig *config.RawConfig) { + if d := DnsPatch; d != nil { + rawConfig.DNS = *d + } else if d := OptionalDnsPatch; d != nil { + if !rawConfig.DNS.Enable { + rawConfig.DNS = *d + } + } + + if append := NameServersAppend; len(append) > 0 { + d := &rawConfig.DNS + nameservers := make([]string, len(append)+len(d.NameServer)) + copy(nameservers, append) + copy(nameservers[len(append):], d.NameServer) + + d.NameServer = nameservers + } +} + +func patchConfig(config *config.Config) { + if config.DNS.FakeIPRange != nil { + if c := cachedPool; c != nil { + if config.DNS.FakeIPRange.Gateway().String() == c.Gateway().String() { + config.DNS.FakeIPRange = c + } + } else { + cachedPool = config.DNS.FakeIPRange + } + } +} diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go index 9f85a21594..5b0e3c6f2a 100644 --- a/core/src/main/golang/tun/tun.go +++ b/core/src/main/golang/tun/tun.go @@ -4,7 +4,7 @@ import ( "strconv" "sync" - "github.com/Dreamacro/clash/dns" + "github.com/Dreamacro/clash/component/resolver" "github.com/Dreamacro/clash/log" "github.com/Dreamacro/clash/proxy/tun" ) @@ -56,5 +56,5 @@ func ResetDnsRedirect() { return } - (*tunInstance).ReCreateDNSServer(dns.DefaultResolver, dnsAddress) + (*tunInstance).ReCreateDNSServer(resolver.DefaultResolver, dnsAddress) } diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt index 1e570cf30d..4a14c64880 100644 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ b/core/src/main/java/com/github/kr328/clash/core/Clash.kt @@ -2,6 +2,7 @@ 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 @@ -47,15 +48,31 @@ object Clash { fd: Int, mtu: Int, dns: String, - onStop: () -> Unit + onNewSocket: (Int) -> Boolean, + onTunStop: () -> Unit ) { - Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns, onStop) + Bridge.startTunDevice(fd.toLong(), mtu.toLong(), dns, object: TunCallback { + override fun onCreateSocket(fd: Long) { + onNewSocket(fd.toInt()) + } + override fun onStop() { + onTunStop() + } + }) } fun stopTunDevice() { Bridge.stopTunDevice() } + fun appendDns(dns: List) { + Bridge.resetDnsAppend(dns.joinToString(",")) + } + + fun setDnsOverrideEnabled(enabled: Boolean) { + Bridge.setDnsOverrideEnabled(enabled) + } + fun loadProfile(path: File, baseDir: File): CompletableDeferred { return DoneCallbackImpl().apply { Bridge.loadProfileFile(path.absolutePath, baseDir.absolutePath, this) diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 0de773aa75..52d6b4c747 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -13,6 +13,7 @@ import android.os.PowerManager import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import com.github.kr328.clash.core.Clash +import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.core.utils.asBytesString import com.github.kr328.clash.core.utils.asSpeedString import kotlinx.coroutines.* @@ -105,10 +106,17 @@ class ClashNotification(private val context: ClashService, enableRefresh: Boolea private fun startTicker(): Job { return launch { - while (isActive) { - tickerChannel.send(Unit) + Log.d("Clash Notification Started") - delay(1000) + try { + while (isActive) { + tickerChannel.send(Unit) + + delay(1000) + } + } + finally { + Log.d("Clash Notification Stopped") } } } 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 eca7856d99..03d44f87dc 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 @@ -25,7 +25,7 @@ class ClashService : BaseService() { private var stopReason: String? = null private val reloadChannel = Channel(Channel.CONFLATED) private val settings: Settings by lazy { Settings(ClashManager(this, this)) } - private val profileObserver = object : BroadcastReceiver() { + private val reloadReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.`package` != packageName) return @@ -64,7 +64,10 @@ class ClashService : BaseService() { if (startVpn) startService(TunService::class.intent) - registerReceiver(profileObserver, IntentFilter(Intents.INTENT_ACTION_PROFILE_CHANGED)) + registerReceiver(reloadReceiver, IntentFilter().apply { + addAction(Intents.INTENT_ACTION_PROFILE_CHANGED) + addAction(Intents.INTENT_ACTION_NETWORK_CHANGED) + }) reloadChannel.offer(Unit) @@ -85,7 +88,7 @@ class ClashService : BaseService() { broadcastClashStopped(this, stopReason) - unregisterReceiver(profileObserver) + unregisterReceiver(reloadReceiver) isServiceRunning = false diff --git a/service/src/main/java/com/github/kr328/clash/service/Intents.kt b/service/src/main/java/com/github/kr328/clash/service/Intents.kt index 626e815b35..4dedd7da39 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Intents.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Intents.kt @@ -13,6 +13,8 @@ object Intents { "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.setup" const val INTENT_ACTION_PROFILE_LOADED = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.loaded" + const val INTENT_ACTION_NETWORK_CHANGED = + "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.network.changed" const val INTENT_EXTRA_CLASH_STOP_REASON = "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.stop.reason" diff --git a/service/src/main/java/com/github/kr328/clash/service/Settings.kt b/service/src/main/java/com/github/kr328/clash/service/Settings.kt index d63411611c..b07e289ab0 100644 --- a/service/src/main/java/com/github/kr328/clash/service/Settings.kt +++ b/service/src/main/java/com/github/kr328/clash/service/Settings.kt @@ -11,6 +11,8 @@ class Settings(private val clashManager: IClashManager) { val ACCESS_CONTROL_PACKAGES = PackageListSetting("access_control_packages", emptyList()) val DNS_HIJACKING = BooleanSetting("dns_hijacking", true) val NOTIFICATION_REFRESH = BooleanSetting("notification_refresh", true) + val AUTO_ADD_SYSTEM_DNS = BooleanSetting("auto_add_system_dns", true) + val OVERRIDE_DNS = BooleanSetting("override_dns", true) } fun put(key: String, value: String) { 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 36d966de29..28f2393aee 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 @@ -5,7 +5,10 @@ import android.os.Build import com.github.kr328.clash.core.Clash import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.net.DefaultNetworkChannel +import com.github.kr328.clash.service.util.broadcastNetworkChanged import kotlinx.coroutines.* +import kotlinx.coroutines.channels.broadcast +import java.net.InetAddress class TunService : VpnService(), CoroutineScope by MainScope() { companion object { @@ -40,9 +43,9 @@ class TunService : VpnService(), CoroutineScope by MainScope() { Log.i("TunService.startTun ${fd.fd}") - Clash.startTunDevice(fd.fd, VPN_MTU, dnsAddress) { - stopSelf() - } + Clash.setDnsOverrideEnabled(settings.get(Settings.OVERRIDE_DNS)) + + Clash.startTunDevice(fd.fd, VPN_MTU, dnsAddress, this::protect, this::stopSelf) fd.close() } @@ -64,7 +67,24 @@ class TunService : VpnService(), CoroutineScope by MainScope() { } while (isActive) { - setUnderlyingNetworks(defaultNetworkChannel.receive()?.let { arrayOf(it) }) + val d = defaultNetworkChannel.receive() + + if ( d == null ) { + setUnderlyingNetworks(null) + continue + } + + setUnderlyingNetworks(arrayOf(d.first)) + + if ( settings.get(Settings.AUTO_ADD_SYSTEM_DNS) ) { + withContext(Dispatchers.Default) { + Clash.appendDns(d.second.dnsServers + .map(InetAddress::getHostName) + .filter(String::isNotBlank)) + } + } + + broadcastNetworkChanged(this@TunService) } } } @@ -107,12 +127,10 @@ class TunService : VpnService(), CoroutineScope by MainScope() { addDisallowedApplication(app) } } - addDisallowedApplication(packageName) } Settings.ACCESS_CONTROL_MODE_WHITELIST -> { for (app in settings.get(Settings.ACCESS_CONTROL_PACKAGES).toSet() - - resources.getStringArray(R.array.default_disallow_application) - - setOf(packageName)) { + resources.getStringArray(R.array.default_disallow_application)) { runCatching { addAllowedApplication(app) }.onFailure { @@ -129,7 +147,6 @@ class TunService : VpnService(), CoroutineScope by MainScope() { Log.w("Package $app not found") } } - addDisallowedApplication(packageName) } else -> throw IllegalArgumentException("Invalid mode") } diff --git a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt index a2eb7075c6..9145530f04 100644 --- a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt +++ b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt @@ -2,40 +2,37 @@ package com.github.kr328.clash.service.net import android.content.Context -import android.net.ConnectivityManager -import android.net.Network -import android.net.NetworkCapabilities -import android.net.NetworkRequest -import com.github.kr328.clash.core.utils.Log -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers +import android.net.* +import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.sync.Mutex class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) : - CoroutineScope by scope, Channel by Channel(Channel.CONFLATED) { + CoroutineScope by scope, Channel?> by Channel(Channel.CONFLATED) { + private var currentNetwork: Network? = null + private val detectDelayLock = Mutex() private val connectivity = context.getSystemService(ConnectivityManager::class.java)!! private val callback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { - launch { - send(rebuildNetworkList()) - } + sendDefaultNetwork(true) } override fun onLost(network: Network) { - launch { - send(rebuildNetworkList()) - } + sendDefaultNetwork(true) } override fun onCapabilitiesChanged( network: Network, networkCapabilities: NetworkCapabilities ) { - launch { - send(rebuildNetworkList()) - } + if ( network == currentNetwork && + !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ) + sendDefaultNetwork(true) + } + + override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { + if ( network == currentNetwork ) + sendDefaultNetwork(false) } } @@ -47,7 +44,30 @@ class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) : connectivity.unregisterNetworkCallback(callback) } - private suspend fun rebuildNetworkList(): Network? = withContext(Dispatchers.Default) { + private fun sendDefaultNetwork(ignoreSameNetwork: Boolean) { + if (detectDelayLock.tryLock()) { + launch { + delay(1000) + + val network = detectDefaultNetwork() + if ( ignoreSameNetwork && network == currentNetwork ) + return@launch + + currentNetwork = network + + val linkProperties = network?.let { connectivity.getLinkProperties(it) } + + if ( network != null && linkProperties != null ) + send(network to linkProperties) + else + send(null) + + detectDelayLock.unlock() + } + } + } + + private suspend fun detectDefaultNetwork(): Network? = withContext(Dispatchers.Default) { return@withContext try { connectivity.allNetworks .asSequence() @@ -71,10 +91,6 @@ class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) : else 0 } - .map { - Log.i("Network ${it.first}") - it - } .map { it.second } diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt index 37e6899213..b438263730 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt @@ -24,6 +24,10 @@ fun broadcastProfileLoaded(context: Context, profileEntity: ClashProfileEntity) context.sendBroadcastSelf(intent) } +fun broadcastNetworkChanged(context: Context) { + context.sendBroadcastSelf(Intent(Intents.INTENT_ACTION_NETWORK_CHANGED)) +} + fun broadcastClashStarted(context: Context) { context.sendBroadcastSelf(Intent(Intents.INTENT_ACTION_CLASH_STARTED)) } From 34697da10c7e082aec6a023a2fb9949f109bb30b Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 18 Feb 2020 00:30:59 +0800 Subject: [PATCH 093/358] new method to detect default network --- .../kr328/clash/service/ClashService.kt | 11 +- .../github/kr328/clash/service/TunService.kt | 2 + .../service/net/DefaultNetworkChannel.kt | 130 ++++++++---------- 3 files changed, 69 insertions(+), 74 deletions(-) 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 03d44f87dc..3a0974f45f 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 @@ -11,6 +11,7 @@ import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.util.* import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.sync.Mutex class ClashService : BaseService() { companion object { @@ -20,6 +21,7 @@ class ClashService : BaseService() { var isServiceRunning = false } + private val loadLock = Mutex() private val service = this private lateinit var notification: ClashNotification private var stopReason: String? = null @@ -96,10 +98,13 @@ class ClashService : BaseService() { } private suspend fun reloadProfile() { - val active = ClashDatabase.getInstance(service).openClashProfileDao() - .queryActiveProfile() ?: return stopSelf("Empty active profile") + if ( !loadLock.tryLock() ) + return try { + val active = ClashDatabase.getInstance(service).openClashProfileDao() + .queryActiveProfile() ?: return stopSelf("Empty active profile") + Clash.loadProfile( resolveProfile(active.id), resolveBase(active.id) @@ -115,6 +120,8 @@ class ClashService : BaseService() { broadcastProfileLoaded(this, active) } catch (e: Exception) { stopSelf("Load profile failure") + } finally { + loadLock.unlock() } } 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 28f2393aee..2576720ad1 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 @@ -69,6 +69,8 @@ class TunService : VpnService(), CoroutineScope by MainScope() { while (isActive) { val d = defaultNetworkChannel.receive() + Log.i("Network changed to ${d?.second}") + if ( d == null ) { setUnderlyingNetworks(null) continue diff --git a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt index 9145530f04..91643f1581 100644 --- a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt +++ b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt @@ -3,36 +3,27 @@ package com.github.kr328.clash.service.net import android.content.Context import android.net.* +import com.github.kr328.clash.core.utils.Log import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.sync.Mutex class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) : CoroutineScope by scope, Channel?> by Channel(Channel.CONFLATED) { - private var currentNetwork: Network? = null - private val detectDelayLock = Mutex() private val connectivity = context.getSystemService(ConnectivityManager::class.java)!! private val callback = object : ConnectivityManager.NetworkCallback() { - override fun onAvailable(network: Network) { - sendDefaultNetwork(true) - } + var current: Network? = null override fun onLost(network: Network) { - sendDefaultNetwork(true) - } - - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - if ( network == currentNetwork && - !networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ) - sendDefaultNetwork(true) + if ( current == network ) + offer(null) } override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { - if ( network == currentNetwork ) - sendDefaultNetwork(false) + if ( linkProperties.routes.any { it.isDefaultRoute } ) { + offer(network to linkProperties) + current = network + } } } @@ -44,59 +35,54 @@ class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) : connectivity.unregisterNetworkCallback(callback) } - private fun sendDefaultNetwork(ignoreSameNetwork: Boolean) { - if (detectDelayLock.tryLock()) { - launch { - delay(1000) - - val network = detectDefaultNetwork() - if ( ignoreSameNetwork && network == currentNetwork ) - return@launch - - currentNetwork = network - - val linkProperties = network?.let { connectivity.getLinkProperties(it) } - - if ( network != null && linkProperties != null ) - send(network to linkProperties) - else - send(null) - - detectDelayLock.unlock() - } - } - } - - private suspend fun detectDefaultNetwork(): Network? = withContext(Dispatchers.Default) { - return@withContext try { - connectivity.allNetworks - .asSequence() - .mapNotNull { network -> - connectivity.getNetworkCapabilities(network)?.let { it to network } - } - .filterNot { - it.first.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || - !it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - } - .sortedBy { - when { - it.first.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 0 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 1 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> 2 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN) -> 3 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 4 - else -> 5 - } + if (it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) - -1000 - else - 0 - } - .map { - it.second - } - .firstOrNull() - } catch (e: Exception) { - null - } - } +// private fun sendDefaultNetwork() { +// if (detectDelayLock.tryLock()) { +// launch { +// delay(1000) +// +// val network = detectDefaultNetwork() +// val linkProperties = network?.let { connectivity.getLinkProperties(it) } +// +// if ( network != null && linkProperties != null ) +// send(network to linkProperties) +// else +// send(null) +// +// detectDelayLock.unlock() +// } +// } +// } +// +// private suspend fun detectDefaultNetwork(): Network? = withContext(Dispatchers.Default) { +// return@withContext try { +// connectivity.allNetworks +// .asSequence() +// .mapNotNull { network -> +// connectivity.getNetworkCapabilities(network)?.let { it to network } +// } +// .filterNot { +// it.first.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || +// !it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) +// } +// .sortedBy { +// when { +// it.first.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 0 +// it.first.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 1 +// it.first.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> 2 +// it.first.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN) -> 3 +// it.first.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 4 +// else -> 5 +// } + if (it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) +// -1000 +// else +// 0 +// } +// .map { +// it.second +// } +// .firstOrNull() +// } catch (e: Exception) { +// null +// } +// } } \ No newline at end of file From bfa3c8375b7f589a53b7b4e8b54b08963b6cfaba Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Tue, 18 Feb 2020 16:50:36 +0800 Subject: [PATCH 094/358] new method to detect default network --- .../kr328/clash/service/ClashNotification.kt | 3 +- .../kr328/clash/service/ClashService.kt | 6 +- .../github/kr328/clash/service/TunService.kt | 15 +- .../service/data/ClashProfileProxyEntity.kt | 4 +- .../service/net/DefaultNetworkChannel.kt | 135 ++++++++++-------- .../clash/service/util/BroadcastUtils.kt | 2 - 6 files changed, 91 insertions(+), 74 deletions(-) diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt index 52d6b4c747..e40a3e9e4b 100644 --- a/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt +++ b/service/src/main/java/com/github/kr328/clash/service/ClashNotification.kt @@ -114,8 +114,7 @@ class ClashNotification(private val context: ClashService, enableRefresh: Boolea delay(1000) } - } - finally { + } finally { Log.d("Clash Notification Stopped") } } 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 3a0974f45f..36ba67eb3e 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 @@ -9,8 +9,10 @@ import android.os.IBinder import com.github.kr328.clash.core.Clash import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.util.* -import kotlinx.coroutines.* +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex class ClashService : BaseService() { @@ -98,7 +100,7 @@ class ClashService : BaseService() { } private suspend fun reloadProfile() { - if ( !loadLock.tryLock() ) + if (!loadLock.tryLock()) return try { 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 2576720ad1..c36b8dfee9 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 @@ -7,7 +7,6 @@ import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.service.net.DefaultNetworkChannel import com.github.kr328.clash.service.util.broadcastNetworkChanged import kotlinx.coroutines.* -import kotlinx.coroutines.channels.broadcast import java.net.InetAddress class TunService : VpnService(), CoroutineScope by MainScope() { @@ -71,18 +70,22 @@ class TunService : VpnService(), CoroutineScope by MainScope() { Log.i("Network changed to ${d?.second}") - if ( d == null ) { + if (d == null) { setUnderlyingNetworks(null) continue } setUnderlyingNetworks(arrayOf(d.first)) - if ( settings.get(Settings.AUTO_ADD_SYSTEM_DNS) ) { + if (settings.get(Settings.AUTO_ADD_SYSTEM_DNS)) { withContext(Dispatchers.Default) { - Clash.appendDns(d.second.dnsServers - .map(InetAddress::getHostName) - .filter(String::isNotBlank)) + val dnsServers = d.second?.dnsServers ?: emptyList() + + Clash.appendDns( + dnsServers + .map(InetAddress::getHostName) + .filter(String::isNotBlank) + ) } } 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 c706430fc5..4c37090c7d 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,6 +1,8 @@ package com.github.kr328.clash.service.data -import androidx.room.* +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey @Entity( tableName = "profile_select_proxies", diff --git a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt index 91643f1581..ad0dd30a6c 100644 --- a/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt +++ b/service/src/main/java/com/github/kr328/clash/service/net/DefaultNetworkChannel.kt @@ -1,29 +1,41 @@ -// from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/net/DefaultNetworkListener.kt package com.github.kr328.clash.service.net import android.content.Context import android.net.* -import com.github.kr328.clash.core.utils.Log import kotlinx.coroutines.* import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.sync.Mutex class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) : - CoroutineScope by scope, Channel?> by Channel(Channel.CONFLATED) { + CoroutineScope by scope, + Channel?> by Channel(Channel.CONFLATED) { + private val sendLock = Mutex() private val connectivity = context.getSystemService(ConnectivityManager::class.java)!! private val callback = object : ConnectivityManager.NetworkCallback() { - var current: Network? = null + private val capabilitiesCache = mutableMapOf() + + override fun onAvailable(network: Network) { + sendDefaultNetwork() + } override fun onLost(network: Network) { - if ( current == network ) - offer(null) + sendDefaultNetwork() + + capabilitiesCache.remove(network) } - override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { - if ( linkProperties.routes.any { it.isDefaultRoute } ) { - offer(network to linkProperties) - current = network - } + override fun onCapabilitiesChanged( + network: Network, + networkCapabilities: NetworkCapabilities? + ) { + val cap = capabilitiesCache[network] + + if (cap?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + != networkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + ) + sendDefaultNetwork() + + capabilitiesCache[network] = networkCapabilities } } @@ -35,54 +47,55 @@ class DefaultNetworkChannel(val context: Context, scope: CoroutineScope) : connectivity.unregisterNetworkCallback(callback) } -// private fun sendDefaultNetwork() { -// if (detectDelayLock.tryLock()) { -// launch { -// delay(1000) -// -// val network = detectDefaultNetwork() -// val linkProperties = network?.let { connectivity.getLinkProperties(it) } -// -// if ( network != null && linkProperties != null ) -// send(network to linkProperties) -// else -// send(null) -// -// detectDelayLock.unlock() -// } -// } -// } -// -// private suspend fun detectDefaultNetwork(): Network? = withContext(Dispatchers.Default) { -// return@withContext try { -// connectivity.allNetworks -// .asSequence() -// .mapNotNull { network -> -// connectivity.getNetworkCapabilities(network)?.let { it to network } -// } -// .filterNot { -// it.first.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || -// !it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) -// } -// .sortedBy { -// when { -// it.first.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 0 -// it.first.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 1 -// it.first.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> 2 -// it.first.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN) -> 3 -// it.first.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 4 -// else -> 5 -// } + if (it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) -// -1000 -// else -// 0 -// } -// .map { -// it.second -// } -// .firstOrNull() -// } catch (e: Exception) { -// null -// } -// } + private fun sendDefaultNetwork() { + if (!sendLock.tryLock()) + return + + launch { + delay(1000) + + val network = detectDefaultNetwork() + val link = network?.let(connectivity::getLinkProperties) + + if (network != null) + send(network to link) + else + send(null) + + sendLock.unlock() + } + } + + private suspend fun detectDefaultNetwork() = withContext(Dispatchers.Default) { + try { + connectivity.allNetworks + .asSequence() + .mapNotNull { network -> + connectivity.getNetworkCapabilities(network)?.let { it to network } + } + .filterNot { + it.first.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || + !it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) + } + .sortedBy { + when { + it.first.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 0 + it.first.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 1 + it.first.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> 2 + it.first.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN) -> 3 + it.first.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 4 + else -> 5 + } + if (it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) + -1000 + else + 0 + } + .map { + it.second + } + .firstOrNull() + } catch (e: Exception) { + null + } + } } \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt index b438263730..136b5a9719 100644 --- a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt +++ b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt @@ -3,9 +3,7 @@ package com.github.kr328.clash.service.util import android.content.Context import android.content.Intent import com.github.kr328.clash.service.Intents -import com.github.kr328.clash.service.data.ClashDatabase import com.github.kr328.clash.service.data.ClashProfileEntity -import com.github.kr328.clash.service.data.ClashProfileProxyEntity fun Context.sendBroadcastSelf(intent: Intent) { this.sendBroadcast(intent.setPackage(this.packageName)) From f2d99a50a78ef25661681d528a286a3d20cfc133 Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Wed, 19 Feb 2020 02:21:48 +0800 Subject: [PATCH 095/358] add settings --- app/build.gradle | 5 +- app/src/main/AndroidManifest.xml | 27 ++++++ .../com/github/kr328/clash/BaseActivity.kt | 29 +++++- .../github/kr328/clash/LogViewerActivity.kt | 3 - .../com/github/kr328/clash/LogsActivity.kt | 2 - .../com/github/kr328/clash/MainActivity.kt | 4 + .../com/github/kr328/clash/OnBootReceiver.kt | 11 +++ .../github/kr328/clash/ProfilesActivity.kt | 1 - .../com/github/kr328/clash/ProxiesActivity.kt | 76 ++++++++------- .../github/kr328/clash/SettingsActivity.kt | 44 +++++++++ .../kr328/clash/SettingsBehaviorActivity.kt | 21 +++++ .../kr328/clash/SettingsInterfaceActivity.kt | 17 ++++ .../kr328/clash/SettingsNetworkActivity.kt | 23 +++++ .../kr328/clash/preference/UiPreferences.kt | 71 -------------- .../kr328/clash/preference/UiSettings.kt | 27 ++++++ .../github/kr328/clash/remote/ClashClient.kt | 2 +- .../clash/settings/BaseSettingFragment.kt | 36 +++++++ .../kr328/clash/settings/BehaviorFragment.kt | 47 ++++++++++ .../kr328/clash/settings/InterfaceFragment.kt | 48 ++++++++++ .../kr328/clash/settings/NetworkFragment.kt | 33 +++++++ .../kr328/clash/settings/SettingsDataStore.kt | 93 +++++++++++++++++++ app/src/main/res/drawable/ic_interface.xml | 9 ++ app/src/main/res/drawable/ic_network.xml | 9 ++ .../res/drawable/ic_settings_applications.xml | 11 +++ app/src/main/res/layout/activity_fragment.xml | 24 +++++ app/src/main/res/layout/activity_settings.xml | 23 +++++ app/src/main/res/values/arrays.xml | 34 +++++++ app/src/main/res/values/strings.xml | 34 +++++++ app/src/main/res/values/styles.xml | 1 + app/src/main/res/xml/settings_behavior.xml | 16 ++++ app/src/main/res/xml/settings_interface.xml | 16 ++++ app/src/main/res/xml/settings_network.xml | 47 ++++++++++ build.gradle | 4 + .../kr328/clash/design/common/Option.kt | 9 ++ .../main/res/layout/view_setting_option.xml | 48 ++++++---- service/build.gradle | 1 + service/src/main/AndroidManifest.xml | 5 + .../kr328/clash/service/ClashService.kt | 5 +- .../github/kr328/clash/service/Constants.kt | 2 + .../clash/service/ServiceSettingsProvider.kt | 11 +++ .../github/kr328/clash/service/Settings.kt | 84 ----------------- .../github/kr328/clash/service/TunService.kt | 30 +++--- .../service/net/DefaultNetworkChannel.kt | 33 +++++-- .../clash/service/settings/BaseSettings.kt | 61 ++++++++++++ .../clash/service/settings/ServiceSettings.kt | 32 +++++++ 45 files changed, 921 insertions(+), 248 deletions(-) create mode 100644 app/src/main/java/com/github/kr328/clash/OnBootReceiver.kt create mode 100644 app/src/main/java/com/github/kr328/clash/SettingsActivity.kt create mode 100644 app/src/main/java/com/github/kr328/clash/SettingsBehaviorActivity.kt create mode 100644 app/src/main/java/com/github/kr328/clash/SettingsInterfaceActivity.kt create mode 100644 app/src/main/java/com/github/kr328/clash/SettingsNetworkActivity.kt delete mode 100644 app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt create mode 100644 app/src/main/java/com/github/kr328/clash/preference/UiSettings.kt create mode 100644 app/src/main/java/com/github/kr328/clash/settings/BaseSettingFragment.kt create mode 100644 app/src/main/java/com/github/kr328/clash/settings/BehaviorFragment.kt create mode 100644 app/src/main/java/com/github/kr328/clash/settings/InterfaceFragment.kt create mode 100644 app/src/main/java/com/github/kr328/clash/settings/NetworkFragment.kt create mode 100644 app/src/main/java/com/github/kr328/clash/settings/SettingsDataStore.kt create mode 100644 app/src/main/res/drawable/ic_interface.xml create mode 100644 app/src/main/res/drawable/ic_network.xml create mode 100644 app/src/main/res/drawable/ic_settings_applications.xml create mode 100644 app/src/main/res/layout/activity_fragment.xml create mode 100644 app/src/main/res/layout/activity_settings.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/xml/settings_behavior.xml create mode 100644 app/src/main/res/xml/settings_interface.xml create mode 100644 app/src/main/res/xml/settings_network.xml create mode 100644 service/src/main/java/com/github/kr328/clash/service/ServiceSettingsProvider.kt delete mode 100644 service/src/main/java/com/github/kr328/clash/service/Settings.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/settings/BaseSettings.kt create mode 100644 service/src/main/java/com/github/kr328/clash/service/settings/ServiceSettings.kt diff --git a/app/build.gradle b/app/build.gradle index 1d48ac9430..fe2fa11fa7 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,17 +40,16 @@ dependencies { implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.lifecycle:lifecycle-common-java8:2.2.0" implementation 'androidx.browser:browser:1.2.0' - implementation 'androidx.preference:preference:1.1.0' implementation 'androidx.recyclerview:recyclerview:1.1.0' implementation 'androidx.core:core-ktx:1.3.0-alpha01' implementation 'androidx.fragment:fragment-ktx:1.2.1' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4' implementation "androidx.room:room-runtime:$room_version" - implementation "androidx.preference:preference-ktx:1.1.0" - implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" implementation "org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0" implementation "com.google.android.material:material:1.2.0-alpha04" + implementation "moe.shizuku.preference:preference-appcompat:4.2.0" + implementation "moe.shizuku.preference:preference-simplemenu-appcompat:4.2.0" implementation "com.microsoft.appcenter:appcenter-analytics:$app_center_version" implementation "com.microsoft.appcenter:appcenter-crashes:$app_center_version" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 311194921e..56ea3bdf33 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -63,9 +63,36 @@ android:exported="false" android:configChanges="uiMode" android:launchMode="singleTask"/> + + + + + + + + + diff --git a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt index ca4ca346c7..92f3e88b20 100644 --- a/app/src/main/java/com/github/kr328/clash/BaseActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/BaseActivity.kt @@ -10,9 +10,10 @@ import android.view.* import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.widget.Toolbar import androidx.coordinatorlayout.widget.CoordinatorLayout -import com.github.kr328.clash.preference.UiPreferences +import com.github.kr328.clash.preference.UiSettings import com.github.kr328.clash.remote.Broadcasts import com.github.kr328.clash.service.data.ClashProfileEntity import com.google.android.material.snackbar.Snackbar @@ -60,9 +61,10 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() val rootView: View get() = overrideRootView ?: window.decorView var menu: Menu? = null - lateinit var uiPreference: UiPreferences + lateinit var uiSettings: UiSettings private set lateinit var language: String + lateinit var darkMode: String open suspend fun onClashStarted() {} open suspend fun onClashStopped(reason: String?) {} @@ -93,9 +95,9 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() override fun attachBaseContext(newBase: Context?) { val base = newBase ?: return super.attachBaseContext(newBase) - uiPreference = UiPreferences(base) + uiSettings = UiSettings(base) - language = uiPreference.get(UiPreferences.LANGUAGE) + language = uiSettings.get(UiSettings.LANGUAGE) val languageOverride = language.split("-") if (language.isEmpty()) @@ -115,14 +117,20 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + resetDarkMode() + resetLightNavigationBar() } override fun onStart() { super.onStart() - if (language != uiPreference.get(UiPreferences.LANGUAGE)) + if (language != uiSettings.get(UiSettings.LANGUAGE)) + recreate() + if (darkMode != uiSettings.get(UiSettings.DARK_MODE)) { + resetDarkMode() recreate() + } Broadcasts.register(receiver) } @@ -185,6 +193,17 @@ abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() }.show() } + private fun resetDarkMode() { + when ( uiSettings.get(UiSettings.DARK_MODE).also { darkMode = it } ) { + UiSettings.DARK_MODE_AUTO -> + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + UiSettings.DARK_MODE_DARK -> + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES + UiSettings.DARK_MODE_LIGHT -> + delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_NO + } + } + private fun resetLightNavigationBar() { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return diff --git a/app/src/main/java/com/github/kr328/clash/LogViewerActivity.kt b/app/src/main/java/com/github/kr328/clash/LogViewerActivity.kt index 1aa487f1f9..e2dcf923bc 100644 --- a/app/src/main/java/com/github/kr328/clash/LogViewerActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/LogViewerActivity.kt @@ -16,9 +16,6 @@ import kotlinx.android.synthetic.main.activity_log_viewer.* import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex import java.io.File -import java.io.FileReader -import java.io.IOException -import java.lang.Exception import kotlin.streams.toList class LogViewerActivity : BaseActivity() { diff --git a/app/src/main/java/com/github/kr328/clash/LogsActivity.kt b/app/src/main/java/com/github/kr328/clash/LogsActivity.kt index 4ed2dbbce2..a2a83cdc31 100644 --- a/app/src/main/java/com/github/kr328/clash/LogsActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/LogsActivity.kt @@ -9,7 +9,6 @@ import android.util.TypedValue import android.view.ViewGroup import androidx.annotation.ColorInt import androidx.appcompat.app.AlertDialog -import androidx.core.content.FileProvider import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import com.github.kr328.clash.adapter.LogFileAdapter @@ -22,7 +21,6 @@ import com.github.kr328.clash.utils.logsDir import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.snackbar.Snackbar import kotlinx.android.synthetic.main.activity_logs.* -import kotlinx.android.synthetic.main.activity_main.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext diff --git a/app/src/main/java/com/github/kr328/clash/MainActivity.kt b/app/src/main/java/com/github/kr328/clash/MainActivity.kt index 44e621ae56..13716c5489 100644 --- a/app/src/main/java/com/github/kr328/clash/MainActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/MainActivity.kt @@ -54,6 +54,10 @@ class MainActivity : BaseActivity() { logs.setOnClickListener { startActivity(LogsActivity::class.intent) } + + settings.setOnClickListener { + startActivity(SettingsActivity::class.intent) + } } override fun onStart() { diff --git a/app/src/main/java/com/github/kr328/clash/OnBootReceiver.kt b/app/src/main/java/com/github/kr328/clash/OnBootReceiver.kt new file mode 100644 index 0000000000..571a01a1e7 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/OnBootReceiver.kt @@ -0,0 +1,11 @@ +package com.github.kr328.clash + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + +class OnBootReceiver: BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt index 2044432f4a..e4c10ca9ae 100644 --- a/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProfilesActivity.kt @@ -5,7 +5,6 @@ import android.net.Uri import android.os.Bundle import androidx.recyclerview.widget.LinearLayoutManager import com.github.kr328.clash.adapter.ProfileAdapter -import com.github.kr328.clash.core.utils.Log import com.github.kr328.clash.remote.withProfile import com.github.kr328.clash.service.Intents import com.github.kr328.clash.service.ProfileBackgroundService diff --git a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt index c2cf1e17f8..c9530c2904 100644 --- a/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ProxiesActivity.kt @@ -10,7 +10,7 @@ import com.github.kr328.clash.adapter.ProxyAdapter import com.github.kr328.clash.adapter.ProxyChipAdapter import com.github.kr328.clash.core.model.General import com.github.kr328.clash.core.model.Proxy -import com.github.kr328.clash.preference.UiPreferences +import com.github.kr328.clash.preference.UiSettings import com.github.kr328.clash.remote.withClash import com.github.kr328.clash.utils.PrefixMerger import com.github.kr328.clash.utils.ProxySorter @@ -24,7 +24,7 @@ import kotlinx.coroutines.withContext class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { private val scrollBinding = ScrollBinding(this, this) private val doScrollToLastProxy by lazy { - val selected = uiPreference.get(UiPreferences.PROXY_LAST_SELECT_GROUP) + val selected = uiSettings.get(UiSettings.PROXY_LAST_SELECT_GROUP) launch { scrollBinding.scrollMaster(selected) @@ -88,11 +88,9 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { } override fun onStop() { - uiPreference.edit { - put( - UiPreferences.PROXY_LAST_SELECT_GROUP, - (chipList.adapter!! as ProxyChipAdapter).selected - ) + uiSettings.commit { + put(UiSettings.PROXY_LAST_SELECT_GROUP, + (chipList.adapter!! as ProxyChipAdapter).selected) } super.onStop() @@ -136,40 +134,40 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { } } R.id.groupDefault -> { - uiPreference.edit { - put(UiPreferences.PROXY_GROUP_SORT, UiPreferences.PROXY_SORT_DEFAULT) + uiSettings.commit { + put(UiSettings.PROXY_GROUP_SORT, UiSettings.PROXY_SORT_DEFAULT) } } R.id.groupName -> { - uiPreference.edit { - put(UiPreferences.PROXY_GROUP_SORT, UiPreferences.PROXY_SORT_NAME) + uiSettings.commit { + put(UiSettings.PROXY_GROUP_SORT, UiSettings.PROXY_SORT_NAME) } } R.id.groupDelay -> { - uiPreference.edit { - put(UiPreferences.PROXY_GROUP_SORT, UiPreferences.PROXY_SORT_DELAY) + uiSettings.commit { + put(UiSettings.PROXY_GROUP_SORT, UiSettings.PROXY_SORT_DELAY) } } R.id.proxyDefault -> { - uiPreference.edit { - put(UiPreferences.PROXY_PROXY_SORT, UiPreferences.PROXY_SORT_DEFAULT) + uiSettings.commit { + put(UiSettings.PROXY_PROXY_SORT, UiSettings.PROXY_SORT_DEFAULT) } } R.id.proxyName -> { - uiPreference.edit { - put(UiPreferences.PROXY_PROXY_SORT, UiPreferences.PROXY_SORT_NAME) + uiSettings.commit { + put(UiSettings.PROXY_PROXY_SORT, UiSettings.PROXY_SORT_NAME) } } R.id.proxyDelay -> { - uiPreference.edit { - put(UiPreferences.PROXY_PROXY_SORT, UiPreferences.PROXY_SORT_DELAY) + uiSettings.commit { + put(UiSettings.PROXY_PROXY_SORT, UiSettings.PROXY_SORT_DELAY) } } R.id.utilsMergePrefix -> { item.isChecked = !item.isChecked - uiPreference.edit { - put(UiPreferences.PROXY_MERGE_PREFIX, item.isChecked) + uiSettings.commit { + put(UiSettings.PROXY_MERGE_PREFIX, item.isChecked) } refreshList() @@ -206,25 +204,25 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { General.Mode.RULE -> findItem(R.id.modeRule).isChecked = true } - when (uiPreference.get(UiPreferences.PROXY_GROUP_SORT)) { - UiPreferences.PROXY_SORT_DEFAULT -> + when (uiSettings.get(UiSettings.PROXY_GROUP_SORT)) { + UiSettings.PROXY_SORT_DEFAULT -> findItem(R.id.groupDefault).isChecked = true - UiPreferences.PROXY_SORT_NAME -> + UiSettings.PROXY_SORT_NAME -> findItem(R.id.groupName).isChecked = true - UiPreferences.PROXY_SORT_DELAY -> + UiSettings.PROXY_SORT_DELAY -> findItem(R.id.proxyDelay).isChecked = true } - when (uiPreference.get(UiPreferences.PROXY_PROXY_SORT)) { - UiPreferences.PROXY_SORT_DEFAULT -> + when (uiSettings.get(UiSettings.PROXY_PROXY_SORT)) { + UiSettings.PROXY_SORT_DEFAULT -> findItem(R.id.proxyDefault).isChecked = true - UiPreferences.PROXY_SORT_NAME -> + UiSettings.PROXY_SORT_NAME -> findItem(R.id.proxyName).isChecked = true - UiPreferences.PROXY_SORT_DELAY -> + UiSettings.PROXY_SORT_DELAY -> findItem(R.id.proxyDelay).isChecked = true } findItem(R.id.utilsMergePrefix).isChecked = - uiPreference.get(UiPreferences.PROXY_MERGE_PREFIX) + uiSettings.get(UiSettings.PROXY_MERGE_PREFIX) } } } @@ -239,7 +237,7 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { } val prefixDeferred = async { - if (uiPreference.get(UiPreferences.PROXY_MERGE_PREFIX)) { + if (uiSettings.get(UiSettings.PROXY_MERGE_PREFIX)) { proxies.map { async { PrefixMerger.merge(it.proxies.map { p -> it.name to p.name }) { it.second } } }.flatMap { @@ -251,22 +249,22 @@ class ProxiesActivity : BaseActivity(), ScrollBinding.Callback { } val sortDeferred = async { - val groupSort = when (uiPreference.get(UiPreferences.PROXY_GROUP_SORT)) { - UiPreferences.PROXY_SORT_DEFAULT -> + val groupSort = when (uiSettings.get(UiSettings.PROXY_GROUP_SORT)) { + UiSettings.PROXY_SORT_DEFAULT -> ProxySorter.Order.DEFAULT - UiPreferences.PROXY_SORT_NAME -> + UiSettings.PROXY_SORT_NAME -> ProxySorter.Order.NAME_INCREASE - UiPreferences.PROXY_SORT_DELAY -> + UiSettings.PROXY_SORT_DELAY -> ProxySorter.Order.DELAY_INCREASE else -> throw IllegalArgumentException() } - val proxySort = when (uiPreference.get(UiPreferences.PROXY_PROXY_SORT)) { - UiPreferences.PROXY_SORT_DEFAULT -> + val proxySort = when (uiSettings.get(UiSettings.PROXY_PROXY_SORT)) { + UiSettings.PROXY_SORT_DEFAULT -> ProxySorter.Order.DEFAULT - UiPreferences.PROXY_SORT_NAME -> + UiSettings.PROXY_SORT_NAME -> ProxySorter.Order.NAME_INCREASE - UiPreferences.PROXY_SORT_DELAY -> + UiSettings.PROXY_SORT_DELAY -> ProxySorter.Order.DELAY_INCREASE else -> throw IllegalArgumentException() } diff --git a/app/src/main/java/com/github/kr328/clash/SettingsActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingsActivity.kt new file mode 100644 index 0000000000..5144d7614c --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/SettingsActivity.kt @@ -0,0 +1,44 @@ +package com.github.kr328.clash + +import android.location.SettingInjectorService +import android.os.Bundle +import com.github.kr328.clash.service.util.intent +import kotlinx.android.synthetic.main.activity_settings.* + +class SettingsActivity: BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_settings) + setSupportActionBar(toolbar) + + commonUi.build { + option( + icon = getDrawable(R.drawable.ic_settings_applications), + title = getString(R.string.behavior)) { + paddingHeight = true + + onClick { + startActivity(SettingsBehaviorActivity::class.intent) + } + } + option( + icon = getDrawable(R.drawable.ic_network), + title = getString(R.string.network)) { + paddingHeight = true + + onClick { + startActivity(SettingsNetworkActivity::class.intent) + } + } + option( + icon = getDrawable(R.drawable.ic_interface), + title = getString(R.string.interface_)) { + paddingHeight = true + + onClick { + startActivity(SettingsInterfaceActivity::class.intent) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/SettingsBehaviorActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingsBehaviorActivity.kt new file mode 100644 index 0000000000..cec01a4122 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/SettingsBehaviorActivity.kt @@ -0,0 +1,21 @@ +package com.github.kr328.clash + +import android.os.Bundle +import com.github.kr328.clash.service.settings.ServiceSettings +import com.github.kr328.clash.settings.BehaviorFragment +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SettingsBehaviorActivity: BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_fragment) + setSupportActionBar(findViewById(R.id.toolbar)) + + supportFragmentManager.beginTransaction() + .replace(R.id.fragment, BehaviorFragment()) + .commit() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/SettingsInterfaceActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingsInterfaceActivity.kt new file mode 100644 index 0000000000..2613f55b20 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/SettingsInterfaceActivity.kt @@ -0,0 +1,17 @@ +package com.github.kr328.clash + +import android.os.Bundle +import com.github.kr328.clash.settings.InterfaceFragment + +class SettingsInterfaceActivity: BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_fragment) + setSupportActionBar(findViewById(R.id.toolbar)) + + supportFragmentManager.beginTransaction() + .replace(R.id.fragment, InterfaceFragment()) + .commit() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/SettingsNetworkActivity.kt b/app/src/main/java/com/github/kr328/clash/SettingsNetworkActivity.kt new file mode 100644 index 0000000000..767259dba5 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/SettingsNetworkActivity.kt @@ -0,0 +1,23 @@ +package com.github.kr328.clash + +import android.os.Bundle +import com.github.kr328.clash.preference.UiSettings +import com.github.kr328.clash.service.settings.ServiceSettings +import com.github.kr328.clash.settings.NetworkFragment +import kotlinx.android.synthetic.main.activity_settings.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class SettingsNetworkActivity : BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.activity_fragment) + setSupportActionBar(findViewById(R.id.toolbar)) + + supportFragmentManager.beginTransaction() + .replace(R.id.fragment, NetworkFragment()) + .commit() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt b/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt deleted file mode 100644 index 33762da742..0000000000 --- a/app/src/main/java/com/github/kr328/clash/preference/UiPreferences.kt +++ /dev/null @@ -1,71 +0,0 @@ -package com.github.kr328.clash.preference - -import android.content.Context -import android.content.SharedPreferences - -class UiPreferences(context: Context) { - companion object { - private const val FILE_NAME = "ui" - - const val PROXY_SORT_DEFAULT = "default" - const val PROXY_SORT_NAME = "name" - const val PROXY_SORT_DELAY = "delay" - - const val LANGUAGE_DEFAULT = "" - const val LANGUAGE_EN = "en" - const val LANGUAGE_ZH_CN = "zh-CN" - - val PROXY_GROUP_SORT = StringEntry("proxy_group_sort", PROXY_SORT_DEFAULT) - val PROXY_PROXY_SORT = StringEntry("proxy_proxy_sort", PROXY_SORT_DEFAULT) - val PROXY_LAST_SELECT_GROUP = StringEntry("proxy_last_select_group", "") - val PROXY_MERGE_PREFIX = BooleanEntry("proxy_merge_prefix", true) - val LANGUAGE = StringEntry("language", "") - } - - interface Entry { - fun get(sharedPreferences: SharedPreferences): T - fun put(editor: SharedPreferences.Editor, value: T) - } - - class StringEntry(private val key: String, private val defaultValue: String) : - Entry { - override fun get(sharedPreferences: SharedPreferences): String { - return sharedPreferences.getString(key, defaultValue)!! - } - - override fun put(editor: SharedPreferences.Editor, value: String) { - editor.putString(key, value) - } - } - - class BooleanEntry(private val key: String, private val defaultValue: Boolean = false) : - Entry { - override fun get(sharedPreferences: SharedPreferences): Boolean { - return sharedPreferences.getBoolean(key, defaultValue) - } - - override fun put(editor: SharedPreferences.Editor, value: Boolean) { - editor.putBoolean(key, value) - } - } - - class Editor(private val editor: SharedPreferences.Editor) { - fun > put(e: E, value: T) { - e.put(editor, value) - } - } - - private val sharedPreferences = context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE) - - fun > get(e: E): T { - return e.get(sharedPreferences) - } - - fun edit(block: Editor.() -> Unit) { - val editor = sharedPreferences.edit() - - Editor(editor).apply(block) - - editor.apply() - } -} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/preference/UiSettings.kt b/app/src/main/java/com/github/kr328/clash/preference/UiSettings.kt new file mode 100644 index 0000000000..db7d78334d --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/preference/UiSettings.kt @@ -0,0 +1,27 @@ +package com.github.kr328.clash.preference + +import android.content.Context +import com.github.kr328.clash.service.settings.BaseSettings + +class UiSettings(context: Context): + BaseSettings(context.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)) { + companion object { + private const val FILE_NAME = "ui" + + const val PROXY_SORT_DEFAULT = "default" + const val PROXY_SORT_NAME = "name" + const val PROXY_SORT_DELAY = "delay" + + const val DARK_MODE_AUTO = "auto" + const val DARK_MODE_DARK = "dark" + const val DARK_MODE_LIGHT = "light" + + val ENABLE_VPN = BooleanEntry("enable_vpn", true) + val PROXY_GROUP_SORT = StringEntry("proxy_group_sort", PROXY_SORT_DEFAULT) + val PROXY_PROXY_SORT = StringEntry("proxy_proxy_sort", PROXY_SORT_DEFAULT) + val PROXY_LAST_SELECT_GROUP = StringEntry("proxy_last_select_group", "") + val PROXY_MERGE_PREFIX = BooleanEntry("proxy_merge_prefix", true) + val LANGUAGE = StringEntry("language", "") + val DARK_MODE = StringEntry("dark_mode", DARK_MODE_AUTO) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt index cf3bc360ff..edbfae803b 100644 --- a/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt +++ b/app/src/main/java/com/github/kr328/clash/remote/ClashClient.kt @@ -10,7 +10,7 @@ import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext -class ClashClient(private val service: IClashManager) { +class ClashClient(val service: IClashManager) { suspend fun setSelectProxy(name: String, proxy: String): Boolean = withContext(Dispatchers.IO) { return@withContext service.setSelectProxy(name, proxy) } diff --git a/app/src/main/java/com/github/kr328/clash/settings/BaseSettingFragment.kt b/app/src/main/java/com/github/kr328/clash/settings/BaseSettingFragment.kt new file mode 100644 index 0000000000..27dfd51e36 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/settings/BaseSettingFragment.kt @@ -0,0 +1,36 @@ +package com.github.kr328.clash.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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 fun onCreateDataStore(): SettingsDataStore + abstract val xmlResourceId: Int + + protected val service: ServiceSettings by lazy { ServiceSettings(requireActivity()) } + protected val ui: UiSettings by lazy { UiSettings(requireActivity()) } + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + preferenceManager.preferenceDataStore = onCreateDataStore() + + setPreferencesFromResource(xmlResourceId, rootKey) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val result = super.onCreateView(inflater, container, savedInstanceState) + + setDivider(null) + setDividerHeight(0) + + return result + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/settings/BehaviorFragment.kt b/app/src/main/java/com/github/kr328/clash/settings/BehaviorFragment.kt new file mode 100644 index 0000000000..00465bca59 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/settings/BehaviorFragment.kt @@ -0,0 +1,47 @@ +package com.github.kr328.clash.settings + +import android.content.pm.PackageManager +import com.github.kr328.clash.OnBootReceiver +import com.github.kr328.clash.R +import com.github.kr328.clash.service.settings.ServiceSettings +import com.github.kr328.clash.service.util.componentName + +class BehaviorFragment: BaseSettingFragment() { + companion object { + private const val KEY_START_ON_BOOT = "start_on_boot" + private const val KEY_SHOW_TRAFFIC = "show_traffic" + } + + override fun onCreateDataStore(): SettingsDataStore { + return SettingsDataStore().apply { + on(KEY_START_ON_BOOT, StartOnBootSource()) + on(KEY_SHOW_TRAFFIC, ServiceSettings.NOTIFICATION_REFRESH.asSource(service)) + } + } + + override val xmlResourceId: Int + get() = R.xml.settings_behavior + + private inner class StartOnBootSource: SettingsDataStore.Source { + override fun set(value: Any?) { + val v = value as Boolean? ?: return + + val status = if ( v ) + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + else + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + + requireActivity().packageManager.setComponentEnabledSetting( + OnBootReceiver::class.componentName, + status, + PackageManager.DONT_KILL_APP) + } + + override fun get(): Any? { + val status = requireActivity().packageManager + .getComponentEnabledSetting(OnBootReceiver::class.componentName) + + return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/settings/InterfaceFragment.kt b/app/src/main/java/com/github/kr328/clash/settings/InterfaceFragment.kt new file mode 100644 index 0000000000..8a1093a951 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/settings/InterfaceFragment.kt @@ -0,0 +1,48 @@ +package com.github.kr328.clash.settings + +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import com.github.kr328.clash.R +import com.github.kr328.clash.preference.UiSettings + +class InterfaceFragment: BaseSettingFragment() { + companion object { + private const val KEY_DARK_MODE = "dark_mode" + private const val KEY_LANGUAGE = "language" + } + + override fun onCreateDataStore(): SettingsDataStore { + return SettingsDataStore().apply { + on(KEY_DARK_MODE, DarkModeSource()) + on(KEY_LANGUAGE, UiSettings.LANGUAGE.asSource(ui)) + } + } + + override val xmlResourceId: Int + get() = R.xml.settings_interface + + private inner class DarkModeSource: SettingsDataStore.Source { + override fun set(value: Any?) { + ui.commit { + put(UiSettings.DARK_MODE, value as String) + } + + requireActivity().recreate() + } + + override fun get(): Any? { + return when ( activity.delegate.localNightMode ) { + AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM -> + UiSettings.DARK_MODE_AUTO + AppCompatDelegate.MODE_NIGHT_YES -> + UiSettings.DARK_MODE_DARK + AppCompatDelegate.MODE_NIGHT_NO -> + UiSettings.DARK_MODE_LIGHT + else -> UiSettings.DARK_MODE_AUTO + } + } + + private val activity: AppCompatActivity + get() = requireActivity() as AppCompatActivity + } +} \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/settings/NetworkFragment.kt b/app/src/main/java/com/github/kr328/clash/settings/NetworkFragment.kt new file mode 100644 index 0000000000..6369e17058 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/settings/NetworkFragment.kt @@ -0,0 +1,33 @@ +package com.github.kr328.clash.settings + +import com.github.kr328.clash.R +import com.github.kr328.clash.preference.UiSettings +import com.github.kr328.clash.service.settings.ServiceSettings + +class NetworkFragment: BaseSettingFragment() { + companion object { + private const val KEY_ENABLE_VPN_SERVICE = "enable_vpn_service" + private const val KEY_IPV6 = "ipv6" + private const val BYPASS_PRIVATE_NETWORK = "bypass_private_network" + private const val KEY_DNS_HIJACKING = "dns_hijacking" + private const val KEY_DNS_OVERRIDE = "dns_override" + private const val KEY_APPEND_SYS_DNS = "append_system_dns" + private const val KEY_ACCESS_CONTROL_MODE = "access_control_mode" + } + + override fun onCreateDataStore(): SettingsDataStore { + return SettingsDataStore().apply { + on(KEY_ENABLE_VPN_SERVICE, UiSettings.ENABLE_VPN.asSource(ui)) + on(KEY_IPV6, ServiceSettings.IPV6_SUPPORT.asSource(service)) + on(BYPASS_PRIVATE_NETWORK, ServiceSettings.BYPASS_PRIVATE_NETWORK.asSource(service)) + on(KEY_DNS_HIJACKING, ServiceSettings.DNS_HIJACKING.asSource(service)) + on(KEY_DNS_OVERRIDE, ServiceSettings.OVERRIDE_DNS.asSource(service)) + on(KEY_APPEND_SYS_DNS, ServiceSettings.AUTO_ADD_SYSTEM_DNS.asSource(service)) + on(KEY_ACCESS_CONTROL_MODE, ServiceSettings.ACCESS_CONTROL_MODE.asSource(service)) + } + } + + override val xmlResourceId: Int + get() = R.xml.settings_network +} + diff --git a/app/src/main/java/com/github/kr328/clash/settings/SettingsDataStore.kt b/app/src/main/java/com/github/kr328/clash/settings/SettingsDataStore.kt new file mode 100644 index 0000000000..c0c5070175 --- /dev/null +++ b/app/src/main/java/com/github/kr328/clash/settings/SettingsDataStore.kt @@ -0,0 +1,93 @@ +package com.github.kr328.clash.settings + +import com.github.kr328.clash.service.settings.BaseSettings +import moe.shizuku.preference.PreferenceDataStore + +class SettingsDataStore: PreferenceDataStore() { + interface Source { + fun set(value: Any?) + fun get(): Any? + } + + private val sources: MutableMap = mutableMapOf() + + fun on(key: String, source: Source) { + sources[key] = source + } + + inline fun BaseSettings.Entry.asSource(settings: BaseSettings): Source { + return object: Source { + override fun set(value: Any?) { + val v = value ?: throw NullPointerException() + + settings.commit { + put(this@asSource, v as T) + } + } + + override fun get(): Any? { + return settings.get(this@asSource) + } + } + } + + override fun getBoolean(key: String?, defValue: Boolean): Boolean { + val source = sources[key] ?: return defValue + + return (source.get() as Boolean?) ?: defValue + } + + override fun putLong(key: String?, value: Long) { + val source = sources[key] ?: throw NullPointerException() + + source.set(value) + } + + override fun putInt(key: String?, value: Int) { + val source = sources[key] ?: throw NullPointerException() + + source.set(value) + } + + override fun getInt(key: String?, defValue: Int): Int { + val source = sources[key] ?: return defValue + + return (source.get() as Int?) ?: defValue + } + + override fun putBoolean(key: String?, value: Boolean) { + val source = sources[key] ?: throw NullPointerException() + + source.set(value) + } + + override fun getLong(key: String?, defValue: Long): Long { + val source = sources[key] ?: return defValue + + return (source.get() as Long?) ?: defValue + } + + override fun getFloat(key: String?, defValue: Float): Float { + val source = sources[key] ?: return defValue + + return (source.get() as Float?) ?: defValue + } + + override fun putFloat(key: String?, value: Float) { + val source = sources[key] ?: throw NullPointerException() + + source.set(value) + } + + override fun getString(key: String?, defValue: String?): String? { + val source = sources[key] ?: return defValue + + return (source.get() as String?) ?: defValue + } + + override fun putString(key: String?, value: String?) { + val source = sources[key] ?: throw NullPointerException() + + source.set(value) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_interface.xml b/app/src/main/res/drawable/ic_interface.xml new file mode 100644 index 0000000000..562e40cc64 --- /dev/null +++ b/app/src/main/res/drawable/ic_interface.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_network.xml b/app/src/main/res/drawable/ic_network.xml new file mode 100644 index 0000000000..7246730ea0 --- /dev/null +++ b/app/src/main/res/drawable/ic_network.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_settings_applications.xml b/app/src/main/res/drawable/ic_settings_applications.xml new file mode 100644 index 0000000000..831b708369 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_applications.xml @@ -0,0 +1,11 @@ + + + diff --git a/app/src/main/res/layout/activity_fragment.xml b/app/src/main/res/layout/activity_fragment.xml new file mode 100644 index 0000000000..1739d6eeeb --- /dev/null +++ b/app/src/main/res/layout/activity_fragment.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml new file mode 100644 index 0000000000..c6fe250493 --- /dev/null +++ b/app/src/main/res/layout/activity_settings.xml @@ -0,0 +1,23 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000000..07ef2ed32f --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,34 @@ + + + + Allow all apps + Only allowing selected apps + Disallow selected apps + + + Auto + Dark + Light + + + Auto + English + Simplified Chinese + + + + auto + dark + light + + + access_control_mode_all + access_control_mode_blacklist + access_control_mode_whitelist + + + + en + zh-rCN + + \ 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 ea8b05446f..f5db0ed960 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -104,4 +104,38 @@ %s will be deleted Open Log File Failure File Exported + + Behavior + Network + Interface + + Boot + Start on Boot + Start slash on system boot + + Notification + Show Traffic + Auto refresh traffic in notification + + Route System Traffic + Routing all system traffic via VpnService + + VPN Service + IPv6 + Enable IPv6 support (not recommend) + Bypass Private Network + Bypass private network addresses + DNS Hijacking + Handle all dns packet + DNS Config Override + Force use builtin DNS config + Append System DNS + Auto add system dns to clash + Access Control + Access Control Mode + Access Control Packages + Configure access permission for apps + + Language + Dark Mode diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 6ea316f9bb..697f191236 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -7,6 +7,7 @@ @color/colorPrimaryClashBlue @color/backgroundColor @bool/lightStatusBar + @style/PreferenceThemeOverlay - - - diff --git a/app/src/main/res/xml/full_backup_content.xml b/app/src/main/res/xml/full_backup_content.xml deleted file mode 100644 index 9ce200ab4b..0000000000 --- a/app/src/main/res/xml/full_backup_content.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/settings_behavior.xml b/app/src/main/res/xml/settings_behavior.xml deleted file mode 100644 index 599fb87421..0000000000 --- a/app/src/main/res/xml/settings_behavior.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/settings_interface.xml b/app/src/main/res/xml/settings_interface.xml deleted file mode 100644 index 144a7b67ab..0000000000 --- a/app/src/main/res/xml/settings_interface.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/xml/settings_network.xml b/app/src/main/res/xml/settings_network.xml deleted file mode 100644 index ff80f31d10..0000000000 --- a/app/src/main/res/xml/settings_network.xml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - - - - - - - - - diff --git a/build.gradle.kts b/build.gradle.kts deleted file mode 100644 index 42560dfce3..0000000000 --- a/build.gradle.kts +++ /dev/null @@ -1,30 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -buildscript { - val gKotlinVersion: String by project - - repositories { - google() - jcenter() - } - dependencies { - classpath("com.android.tools.build:gradle:4.0.0") - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$gKotlinVersion") - classpath("org.jetbrains.kotlin:kotlin-serialization:$gKotlinVersion") - } -} - -allprojects { - repositories { - google() - jcenter() - - maven { - url = java.net.URI("https://dl.bintray.com/rikkaw/Libraries") - } - } -} - -task("clean", type = Delete::class) { - delete(rootProject.buildDir) -} diff --git a/common/build.gradle.kts b/common/build.gradle.kts deleted file mode 100644 index 4639217c72..0000000000 --- a/common/build.gradle.kts +++ /dev/null @@ -1,61 +0,0 @@ -plugins { - id("com.android.library") - id("kotlin-android") - id("kotlin-android-extensions") -} - -val gCompileSdkVersion: String by project -val gBuildToolsVersion: String by project - -val gMinSdkVersion: String by project -val gTargetSdkVersion: String by project - -val gVersionCode: String by project -val gVersionName: String by project - -val gKotlinVersion: String by project -val gKotlinCoroutineVersion: String by project -val gAndroidKtxVersion: String by project -val gKotlinSerializationVersion: String by project - -android { - compileSdkVersion(gCompileSdkVersion) - buildToolsVersion(gBuildToolsVersion) - - defaultConfig { - minSdkVersion(gMinSdkVersion) - targetSdkVersion(gTargetSdkVersion) - - versionCode = gVersionCode.toInt() - versionName = gVersionName - - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - named("release") { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" - } -} - -dependencies { - implementation("androidx.core:core-ktx:$gAndroidKtxVersion") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion") -} - -repositories { - mavenCentral() -} diff --git a/common/consumer-rules.pro b/common/consumer-rules.pro deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/proguard-rules.pro b/common/proguard-rules.pro deleted file mode 100644 index f1b424510d..0000000000 --- a/common/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml deleted file mode 100644 index 3a0f4f4573..0000000000 --- a/common/src/main/AndroidManifest.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - diff --git a/common/src/main/java/com/github/kr328/clash/common/Constants.kt b/common/src/main/java/com/github/kr328/clash/common/Constants.kt deleted file mode 100644 index da8b05cf54..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/Constants.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.kr328.clash.common - -object Constants { - const val TAG = "ClashForAndroid" -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/Global.kt b/common/src/main/java/com/github/kr328/clash/common/Global.kt deleted file mode 100644 index 3de7c5b618..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/Global.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.kr328.clash.common - -import android.app.Application -import android.content.Intent - -object Global { - var openMainIntent: () -> Intent = { Intent() } - var openProfileIntent: (Long) -> Intent = { Intent() } - - lateinit var application: Application - private set - - fun init(application: Application) { - Global.application = application - } -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/Permissions.kt b/common/src/main/java/com/github/kr328/clash/common/Permissions.kt deleted file mode 100644 index 6bb39a1779..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/Permissions.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.github.kr328.clash.common - -object Permissions { - val PERMISSION_RECEIVE_BROADCASTS: String - get() = Global.application.packageName + ".permission.RECEIVE_BROADCASTS" -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/ids/Intents.kt b/common/src/main/java/com/github/kr328/clash/common/ids/Intents.kt deleted file mode 100644 index e448c2e7bc..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/ids/Intents.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.kr328.clash.common.ids - -import com.github.kr328.clash.common.BuildConfig - -object Intents { - const val INTENT_ACTION_CLASH_STARTED = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.STARTED" - const val INTENT_ACTION_CLASH_STOPPED = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.STOPPED" - const val INTENT_ACTION_CLASH_REQUEST_STOP = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.clash.REQUEST_STOP" - const val INTENT_ACTION_PROFILE_CHANGED = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.CHANGED" - const val INTENT_ACTION_PROFILE_REQUEST_UPDATE = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.REQUEST_UPDATE" - const val INTENT_ACTION_PROFILE_LOADED = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.profile.LOADED" - const val INTENT_ACTION_NETWORK_CHANGED = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.action.network.CHANGED" - - const val INTENT_EXTRA_CLASH_STOP_REASON = - "${BuildConfig.LIBRARY_PACKAGE_NAME}.intent.extra.clash.STOP_REASON" -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/ids/NotificationChannels.kt b/common/src/main/java/com/github/kr328/clash/common/ids/NotificationChannels.kt deleted file mode 100644 index b10d0a6e36..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/ids/NotificationChannels.kt +++ /dev/null @@ -1,7 +0,0 @@ -package com.github.kr328.clash.common.ids - -object NotificationChannels { - const val CLASH_STATUS = "clash_status_channel" - const val PROFILE_STATUS = "profile_status_channel" - const val PROFILE_RESULT = "profile_result_channel" -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/ids/NotificationIds.kt b/common/src/main/java/com/github/kr328/clash/common/ids/NotificationIds.kt deleted file mode 100644 index cf38054373..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/ids/NotificationIds.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.kr328.clash.common.ids - -object NotificationIds { - const val CLASH_STATUS = 1 - const val PROFILE_STATUS = 2 - private val PROFILE_RESULT = 10000..20000 - - fun generateProfileResultId(profileId: Long): Int { - val bound = PROFILE_RESULT.last - PROFILE_RESULT.first - return (profileId % bound + PROFILE_RESULT.first).toInt() - } -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/ids/PendingIds.kt b/common/src/main/java/com/github/kr328/clash/common/ids/PendingIds.kt deleted file mode 100644 index c46cc537d4..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/ids/PendingIds.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.kr328.clash.common.ids - -object PendingIds { - const val CLASH_VPN = 1 - - fun generateProfileResultId(profileId: Long): Int { - return NotificationIds.generateProfileResultId(profileId) - } -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/serialization/MergedParcels.kt b/common/src/main/java/com/github/kr328/clash/common/serialization/MergedParcels.kt deleted file mode 100644 index 5250ac5f91..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/serialization/MergedParcels.kt +++ /dev/null @@ -1,274 +0,0 @@ -package com.github.kr328.clash.common.serialization - -import android.os.Parcel -import kotlinx.serialization.* -import kotlinx.serialization.modules.EmptyModule -import kotlinx.serialization.modules.SerialModule - -object MergedParcels: SerialFormat { - fun dump(serializer: SerializationStrategy, obj: T, parcel: Parcel) { - val data = Parcel.obtain() - val encoder = ParcelsEncoder(data) - - try { - serializer.serialize(encoder, obj) - - data.setDataPosition(0) - - parcel.writeStringList(encoder.getStringList()) - parcel.appendFrom(data, 0, data.dataSize()) - } finally { - data.recycle() - } - } - - fun load(deserializer: DeserializationStrategy, parcel: Parcel): T { - val strings = mutableListOf().apply { parcel.readStringList(this) } - - return deserializer.deserialize(ParcelsDecoder(strings, parcel)) - } - - private class ParcelsEncoder(private val parcel: Parcel) : - Encoder, CompositeEncoder { - private val strings = mutableMapOf() - private var stringIndex = 0 - - fun getStringList(): List { - val result = mutableListOf() - strings.map { it.value to it.key } - .sortedBy { it.first } - .forEach { result.add(it.second) } - return result - } - - override val context: SerialModule - get() = EmptyModule - - override fun beginCollection( - descriptor: SerialDescriptor, - collectionSize: Int, - vararg typeSerializers: KSerializer<*> - ): CompositeEncoder { - encodeInt(collectionSize) - return super.beginCollection(descriptor, collectionSize, *typeSerializers) - } - - override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) = - encodeBoolean(value) - - override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) = - encodeByte(value) - - override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) = - encodeChar(value) - - override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = - encodeDouble(value) - - override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) = - encodeFloat(value) - - override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) = - encodeInt(value) - - override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) = - encodeLong(value) - - override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) = - encodeShort(value) - - override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) = - encodeString(value) - - override fun encodeUnitElement(descriptor: SerialDescriptor, index: Int) = - encodeUnit() - - override fun endStructure(descriptor: SerialDescriptor) {} - - override fun encodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T? - ) = encodeNullableSerializableValue(serializer, value) - - override fun encodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T - ) = encodeSerializableValue(serializer, value) - - override fun beginStructure( - descriptor: SerialDescriptor, - vararg typeSerializers: KSerializer<*> - ): CompositeEncoder = this - - override fun encodeBoolean(value: Boolean) = - parcel.writeByte(if (value) 1 else 0) - - override fun encodeByte(value: Byte) = - parcel.writeByte(value) - - override fun encodeChar(value: Char) = - parcel.writeInt(value.toInt()) - - override fun encodeDouble(value: Double) = - parcel.writeDouble(value) - - override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = - parcel.writeInt(index) - - override fun encodeFloat(value: Float) = - parcel.writeFloat(value) - - override fun encodeInt(value: Int) = - parcel.writeInt(value) - - override fun encodeLong(value: Long) = - parcel.writeLong(value) - - override fun encodeNotNullMark() = - encodeBoolean(true) - - override fun encodeNull() = - encodeBoolean(false) - - override fun encodeShort(value: Short) = - parcel.writeInt(value.toInt()) - - override fun encodeUnit() {} - override fun encodeString(value: String) { - val index = strings.computeIfAbsent(value) { - stringIndex++ - } - - parcel.writeInt(index) - } - } - - class ParcelsDecoder(private val strings: List, private val parcel: Parcel) : Decoder, - CompositeDecoder { - override val context: SerialModule - get() = EmptyModule - override val updateMode: UpdateMode - get() = UpdateMode.BANNED - - override fun decodeSequentially() = - true - - override fun decodeElementIndex(descriptor: SerialDescriptor) = - CompositeDecoder.UNKNOWN_NAME - - override fun decodeCollectionSize(descriptor: SerialDescriptor) = - decodeInt() - - override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = - decodeBoolean() - - override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) = - decodeByte() - - override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) = - decodeChar() - - override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = - decodeDouble() - - override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) = - decodeFloat() - - override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = - decodeInt() - - override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = - decodeShort() - - override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) = - decodeLong() - - override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = - decodeString() - - override fun decodeUnitElement(descriptor: SerialDescriptor, index: Int) = - decodeUnit() - - override fun endStructure(descriptor: SerialDescriptor) {} - - override fun decodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy - ) = decodeNullableSerializableValue(deserializer) - - override fun decodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy - ) = decodeSerializableValue(deserializer) - - override fun updateNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - old: T? - ) = updateNullableSerializableValue(deserializer, old) - - override fun updateSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - old: T - ) = updateSerializableValue(deserializer, old) - - override fun beginStructure( - descriptor: SerialDescriptor, - vararg typeParams: KSerializer<*> - ): CompositeDecoder = this - - override fun decodeBoolean() = - parcel.readByte() != 0.toByte() - - override fun decodeByte() = - parcel.readByte() - - override fun decodeChar() = - parcel.readInt().toChar() - - override fun decodeDouble() = - parcel.readDouble() - - override fun decodeEnum(enumDescriptor: SerialDescriptor) = - parcel.readInt() - - override fun decodeFloat() = - parcel.readFloat() - - override fun decodeInt() = - parcel.readInt() - - override fun decodeLong() = - parcel.readLong() - - override fun decodeNotNullMark() = - decodeBoolean() - - override fun decodeNull() = - null - - override fun decodeShort() = - parcel.readInt().toShort() - - override fun decodeUnit() {} - override fun decodeString(): String { - val index = parcel.readInt() - - return strings[index] - } - - } - - override val context: SerialModule = EmptyModule -} - - diff --git a/common/src/main/java/com/github/kr328/clash/common/serialization/Parcels.kt b/common/src/main/java/com/github/kr328/clash/common/serialization/Parcels.kt deleted file mode 100644 index bfa2a293af..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/serialization/Parcels.kt +++ /dev/null @@ -1,241 +0,0 @@ -package com.github.kr328.clash.common.serialization - -import android.os.Parcel -import kotlinx.serialization.* -import kotlinx.serialization.modules.EmptyModule -import kotlinx.serialization.modules.SerialModule - -object Parcels : SerialFormat { - fun dump(serializer: SerializationStrategy, obj: T, parcel: Parcel) { - serializer.serialize(ParcelsEncoder(parcel), obj) - } - - fun load(deserializer: DeserializationStrategy, parcel: Parcel): T { - return deserializer.deserialize(ParcelsDecoder(parcel)) - } - - private class ParcelsEncoder(private val parcel: Parcel) : - Encoder, CompositeEncoder { - override val context: SerialModule - get() = EmptyModule - - override fun beginCollection( - descriptor: SerialDescriptor, - collectionSize: Int, - vararg typeSerializers: KSerializer<*> - ): CompositeEncoder { - encodeInt(collectionSize) - return super.beginCollection(descriptor, collectionSize, *typeSerializers) - } - - override fun encodeBooleanElement(descriptor: SerialDescriptor, index: Int, value: Boolean) = - encodeBoolean(value) - - override fun encodeByteElement(descriptor: SerialDescriptor, index: Int, value: Byte) = - encodeByte(value) - - override fun encodeCharElement(descriptor: SerialDescriptor, index: Int, value: Char) = - encodeChar(value) - - override fun encodeDoubleElement(descriptor: SerialDescriptor, index: Int, value: Double) = - encodeDouble(value) - - override fun encodeFloatElement(descriptor: SerialDescriptor, index: Int, value: Float) = - encodeFloat(value) - - override fun encodeIntElement(descriptor: SerialDescriptor, index: Int, value: Int) = - encodeInt(value) - - override fun encodeLongElement(descriptor: SerialDescriptor, index: Int, value: Long) = - encodeLong(value) - - override fun encodeShortElement(descriptor: SerialDescriptor, index: Int, value: Short) = - encodeShort(value) - - override fun encodeStringElement(descriptor: SerialDescriptor, index: Int, value: String) = - encodeString(value) - - override fun encodeUnitElement(descriptor: SerialDescriptor, index: Int) = - encodeUnit() - - override fun endStructure(descriptor: SerialDescriptor) {} - - override fun encodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T? - ) = encodeNullableSerializableValue(serializer, value) - - override fun encodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - serializer: SerializationStrategy, - value: T - ) = encodeSerializableValue(serializer, value) - - override fun beginStructure( - descriptor: SerialDescriptor, - vararg typeSerializers: KSerializer<*> - ): CompositeEncoder = this - - override fun encodeBoolean(value: Boolean) = - parcel.writeByte(if (value) 1 else 0) - - override fun encodeByte(value: Byte) = - parcel.writeByte(value) - - override fun encodeChar(value: Char) = - parcel.writeInt(value.toInt()) - - override fun encodeDouble(value: Double) = - parcel.writeDouble(value) - - override fun encodeEnum(enumDescriptor: SerialDescriptor, index: Int) = - parcel.writeInt(index) - - override fun encodeFloat(value: Float) = - parcel.writeFloat(value) - - override fun encodeInt(value: Int) = - parcel.writeInt(value) - - override fun encodeLong(value: Long) = - parcel.writeLong(value) - - override fun encodeNotNullMark() = - encodeBoolean(true) - - override fun encodeNull() = - encodeBoolean(false) - - override fun encodeShort(value: Short) = - parcel.writeInt(value.toInt()) - - override fun encodeString(value: String) = - parcel.writeString(value) - - override fun encodeUnit() {} - } - - class ParcelsDecoder(private val parcel: Parcel) : Decoder, CompositeDecoder { - override val context: SerialModule - get() = EmptyModule - override val updateMode: UpdateMode - get() = UpdateMode.BANNED - - override fun decodeSequentially() = - true - - override fun decodeElementIndex(descriptor: SerialDescriptor) = - CompositeDecoder.UNKNOWN_NAME - - override fun decodeCollectionSize(descriptor: SerialDescriptor) = - decodeInt() - - override fun decodeBooleanElement(descriptor: SerialDescriptor, index: Int) = - decodeBoolean() - - override fun decodeByteElement(descriptor: SerialDescriptor, index: Int) = - decodeByte() - - override fun decodeCharElement(descriptor: SerialDescriptor, index: Int) = - decodeChar() - - override fun decodeDoubleElement(descriptor: SerialDescriptor, index: Int) = - decodeDouble() - - override fun decodeFloatElement(descriptor: SerialDescriptor, index: Int) = - decodeFloat() - - override fun decodeIntElement(descriptor: SerialDescriptor, index: Int) = - decodeInt() - - override fun decodeShortElement(descriptor: SerialDescriptor, index: Int) = - decodeShort() - - override fun decodeLongElement(descriptor: SerialDescriptor, index: Int) = - decodeLong() - - override fun decodeStringElement(descriptor: SerialDescriptor, index: Int) = - decodeString() - - override fun decodeUnitElement(descriptor: SerialDescriptor, index: Int) = - decodeUnit() - - override fun endStructure(descriptor: SerialDescriptor) {} - - override fun decodeNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy - ) = decodeNullableSerializableValue(deserializer) - - override fun decodeSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy - ) = decodeSerializableValue(deserializer) - - override fun updateNullableSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - old: T? - ) = updateNullableSerializableValue(deserializer, old) - - override fun updateSerializableElement( - descriptor: SerialDescriptor, - index: Int, - deserializer: DeserializationStrategy, - old: T - ) = updateSerializableValue(deserializer, old) - - override fun beginStructure( - descriptor: SerialDescriptor, - vararg typeParams: KSerializer<*> - ): CompositeDecoder = this - - override fun decodeBoolean() = - parcel.readByte() != 0.toByte() - - override fun decodeByte() = - parcel.readByte() - - override fun decodeChar() = - parcel.readInt().toChar() - - override fun decodeDouble() = - parcel.readDouble() - - override fun decodeEnum(enumDescriptor: SerialDescriptor) = - parcel.readInt() - - override fun decodeFloat() = - parcel.readFloat() - - override fun decodeInt() = - parcel.readInt() - - override fun decodeLong() = - parcel.readLong() - - override fun decodeNotNullMark() = - decodeBoolean() - - override fun decodeNull() = - null - - override fun decodeShort() = - parcel.readInt().toShort() - - override fun decodeString() = - parcel.readString() ?: throw NullPointerException("String null") - - override fun decodeUnit() {} - } - - override val context: SerialModule = EmptyModule -} - - diff --git a/common/src/main/java/com/github/kr328/clash/common/settings/BaseSettings.kt b/common/src/main/java/com/github/kr328/clash/common/settings/BaseSettings.kt deleted file mode 100644 index dc41d53f1e..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/settings/BaseSettings.kt +++ /dev/null @@ -1,64 +0,0 @@ -package com.github.kr328.clash.common.settings - -import android.content.SharedPreferences - -abstract class BaseSettings(private val preferences: SharedPreferences) { - interface Entry { - fun get(preferences: SharedPreferences): T - fun put(editor: SharedPreferences.Editor, value: T) - } - - class StringEntry(private val key: String, private val defaultValue: String) : - Entry { - override fun get(preferences: SharedPreferences): String { - return preferences.getString(key, defaultValue)!! - } - - override fun put(editor: SharedPreferences.Editor, value: String) { - editor.putString(key, value) - } - } - - class BooleanEntry(private val key: String, private val defaultValue: Boolean) : - Entry { - override fun get(preferences: SharedPreferences): Boolean { - return preferences.getBoolean(key, defaultValue) - } - - override fun put(editor: SharedPreferences.Editor, value: Boolean) { - editor.putBoolean(key, value) - } - } - - class StringSetEntry(private val key: String, private val defaultValue: Set) : - Entry> { - override fun get(preferences: SharedPreferences): Set { - return preferences.getStringSet(key, defaultValue)!! - } - - override fun put(editor: SharedPreferences.Editor, value: Set) { - editor.putStringSet(key, value) - } - } - - class Editor(private val editor: SharedPreferences.Editor) { - fun put(entry: Entry, value: T) { - entry.put(editor, value) - } - } - - fun get(entry: Entry): T { - return entry.get(preferences) - } - - fun commit(async: Boolean = true, block: Editor.() -> Unit) { - val editor = preferences.edit() - - Editor(editor).apply(block) - - if (async) - editor.apply() - else - editor.commit() - } -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/utils/ByteFormatter.kt b/common/src/main/java/com/github/kr328/clash/common/utils/ByteFormatter.kt deleted file mode 100644 index 2842b182a7..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/utils/ByteFormatter.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.kr328.clash.common.utils - -object ByteFormatter { - fun byteToString(bytes: Long): String { - return when { - bytes > 1024 * 1024 * 1024 -> - String.format("%.2f GiB", (bytes.toDouble() / 1024 / 1024 / 1024)) - bytes > 1024 * 1024 -> - String.format("%.2f MiB", (bytes.toDouble() / 1024 / 1024)) - bytes > 1024 -> - String.format("%.2f KiB", (bytes.toDouble() / 1024)) - else -> - "$bytes Bytes" - } - } - - fun byteToStringSecond(bytes: Long): String { - return byteToString(bytes) + "/s" - } -} - -fun Long.asBytesString(): String { - return ByteFormatter.byteToString(this) -} - -fun Long.asSpeedString(): String { - return ByteFormatter.byteToStringSecond(this) -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/utils/ComponentUtils.kt b/common/src/main/java/com/github/kr328/clash/common/utils/ComponentUtils.kt deleted file mode 100644 index c96462fb2b..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/utils/ComponentUtils.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.kr328.clash.common.utils - -import android.content.ComponentName -import android.content.Intent -import com.github.kr328.clash.common.Global -import kotlin.reflect.KClass - -val KClass<*>.componentName: ComponentName - get() = ComponentName.createRelative(Global.application, this.java.name) - -val KClass<*>.intent: Intent - get() = Intent(Global.application, this.java) diff --git a/common/src/main/java/com/github/kr328/clash/common/utils/LanguageUtils.kt b/common/src/main/java/com/github/kr328/clash/common/utils/LanguageUtils.kt deleted file mode 100644 index aabadc782f..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/utils/LanguageUtils.kt +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.kr328.clash.common.utils - -import android.content.Context -import android.content.res.Configuration -import java.util.* - -fun Context.createLanguageConfigurationContext(language: String): Context { - if (language.isBlank()) { - return this - } - - val split = language.split("-") - val locale = if (split.size == 1) - Locale(split[0]) - else - Locale(split[0], split[1]) - - val configuration = Configuration() - - configuration.setLocale(locale) - - return createConfigurationContext(configuration) -} \ No newline at end of file diff --git a/common/src/main/java/com/github/kr328/clash/common/utils/Log.kt b/common/src/main/java/com/github/kr328/clash/common/utils/Log.kt deleted file mode 100644 index 73b53e4904..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/utils/Log.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.kr328.clash.common.utils - -import com.github.kr328.clash.common.Constants.TAG - -object Log { - fun i(message: String, throwable: Throwable? = null) = - android.util.Log.i(TAG, message, throwable) - - fun w(message: String, throwable: Throwable? = null) = - android.util.Log.w(TAG, message, throwable) - - fun e(message: String, throwable: Throwable? = null) = - android.util.Log.e(TAG, message, throwable) - - fun d(message: String, throwable: Throwable? = null) = - android.util.Log.d(TAG, message, throwable) - - fun v(message: String, throwable: Throwable? = null) = - android.util.Log.v(TAG, message, throwable) -} diff --git a/common/src/main/java/com/github/kr328/clash/common/utils/ServiceUtils.kt b/common/src/main/java/com/github/kr328/clash/common/utils/ServiceUtils.kt deleted file mode 100644 index ae12025d2a..0000000000 --- a/common/src/main/java/com/github/kr328/clash/common/utils/ServiceUtils.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.github.kr328.clash.common.utils - -import android.content.Context -import android.content.Intent -import android.os.Build - -fun Context.startForegroundServiceCompat(intent: Intent) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } -} \ No newline at end of file diff --git a/common/src/main/res/values-zh/strings.xml b/common/src/main/res/values-zh/strings.xml deleted file mode 100644 index d516d6ef6e..0000000000 --- a/common/src/main/res/values-zh/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - 接收 Clash 广播 - 接收来自 Clash 内部的广播 - \ No newline at end of file diff --git a/common/src/main/res/values/strings.xml b/common/src/main/res/values/strings.xml deleted file mode 100644 index 85c65d5fcd..0000000000 --- a/common/src/main/res/values/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - Receive Clash Broadcasts - Receive broadcasts of clash services - diff --git a/core/.gitignore b/core/.gitignore deleted file mode 100644 index 3543521e9f..0000000000 --- a/core/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/core/build.gradle.kts b/core/build.gradle.kts deleted file mode 100644 index 99970ba106..0000000000 --- a/core/build.gradle.kts +++ /dev/null @@ -1,101 +0,0 @@ -import android.databinding.tool.ext.toCamelCase - -plugins { - id("com.android.library") - id("kotlin-android") - id("kotlin-android-extensions") - id("kotlinx-serialization") -} - -apply(from = "clash.gradle.kts") - -val gCompileSdkVersion: String by project -val gBuildToolsVersion: String by project - -val gMinSdkVersion: String by project -val gTargetSdkVersion: String by project - -val gVersionCode: String by project -val gVersionName: String by project - -val gKotlinVersion: String by project -val gKotlinCoroutineVersion: String by project -val gKotlinSerializationVersion: String by project -val gAndroidKtxVersion: String by project - -val geoipOutput = buildDir.resolve("outputs/geoip") -val golangSource = file("src/main/golang") -val golangOutput = buildDir.resolve("outputs/golang") -val nativeAbis = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") - -android { - compileSdkVersion(gCompileSdkVersion) - buildToolsVersion(gBuildToolsVersion) - - defaultConfig { - minSdkVersion(gMinSdkVersion) - targetSdkVersion(gTargetSdkVersion) - - versionCode = gVersionCode.toInt() - versionName = gVersionName - - consumerProguardFiles("consumer-rules.pro") - - externalNativeBuild { - cmake { - abiFilters(*nativeAbis.toTypedArray()) - arguments("-DCLASH_OUTPUT=$golangOutput", "-DCLASH_SOURCE=$golangSource") - } - } - } - - buildTypes { - named("release") { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - } - - sourceSets { - named("main") { - assets.srcDir(geoipOutput) - jniLibs.srcDir(golangOutput) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" - } - - externalNativeBuild { - cmake { - path = file("src/main/cpp/CMakeLists.txt") - } - } -} - -dependencies { - implementation(project(":common")) - implementation("androidx.core:core-ktx:$gAndroidKtxVersion") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion") - implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion") -} - -repositories { - mavenCentral() -} - -afterEvaluate { - android.buildTypes.forEach { - val cName = it.name.toCamelCase() - - tasks["externalNativeBuild${cName}"].dependsOn(tasks["compileClashCore"]) - tasks["package${cName}Assets"].dependsOn(tasks["downloadGeoipDatabase"]) - } -} \ No newline at end of file diff --git a/core/clash.gradle.kts b/core/clash.gradle.kts deleted file mode 100644 index fc3baa6a48..0000000000 --- a/core/clash.gradle.kts +++ /dev/null @@ -1,184 +0,0 @@ -import org.apache.tools.ant.taskdefs.condition.Os -import java.io.* -import java.util.* -import java.net.* -import java.time.* - -val gMinSdkVersion: String by project - -val geoipDatabaseUrl = "https://github.com/Dreamacro/maxmind-geoip/releases/latest/download/Country.mmdb" -val geoipInvalidate = Duration.ofDays(7) -val geoipOutput = buildDir.resolve("outputs/geoip") -val golangSource = file("src/main/golang") -val golangOutput = buildDir.resolve("outputs/golang") -val nativeAbis = listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64") - -val String.exe: String - get() { - return if ( Os.isFamily(Os.FAMILY_WINDOWS) ) - "$this.exe" - else - this - } - -fun generateGolangBuildEnvironment(abi: String): Map { - val properties = Properties().apply { - load(FileInputStream(rootProject.file("local.properties"))) - } - - val ndk = properties.getProperty("ndk.dir") - ?: throw GradleScriptException("ndk.dir not found in local.properties", - FileNotFoundException("ndk.dir not found in local.properties")) - - val host = when { - Os.isFamily(Os.FAMILY_WINDOWS) -> - "windows" - Os.isFamily(Os.FAMILY_MAC) -> - "darwin" - Os.isFamily(Os.FAMILY_UNIX) -> - "linux" - else -> - throw GradleScriptException("Unsupported host", FileNotFoundException("Unsupported host")) - } - - val minSdkVersion = gMinSdkVersion.removePrefix("android-") - - val compilerBase = rootProject.file(ndk).resolve("toolchains/llvm/prebuilt/$host-x86_64/bin") - - val cCompiler = when(abi) { - "armeabi-v7a" -> - "armv7a-linux-androideabi$minSdkVersion-clang" - "arm64-v8a" -> - "aarch64-linux-android$minSdkVersion-clang" - "x86" -> - "i686-linux-android$minSdkVersion-clang" - "x86_64" -> - "x86_64-linux-android$minSdkVersion-clang" - else -> - throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi")) - } - - val cppCompiler = when(abi) { - "armeabi-v7a" -> - "armv7a-linux-androideabi$minSdkVersion-clang++" - "arm64-v8a" -> - "aarch64-linux-android$minSdkVersion-clang++" - "x86" -> - "i686-linux-android$minSdkVersion-clang++" - "x86_64" -> - "x86_64-linux-android$minSdkVersion-clang++" - else -> - throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi")) - } - - val linker = when(abi) { - "armeabi-v7a" -> - "arm-linux-androideabi-ld" - "arm64-v8a" -> - "aarch64-linux-android-ld" - "x86" -> - "i686-linux-android-ld" - "x86_64" -> - "x86_64-linux-android-ld" - else -> - throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi")) - } - - val golangArch = when(abi) { - "armeabi-v7a" -> - "arm" - "arm64-v8a" -> - "arm64" - "x86" -> - "386" - "x86_64" -> - "amd64" - else -> - throw GradleScriptException("Unsupported abi $abi", FileNotFoundException("Unsupported abi $abi")) - } - - return mapOf( - "CC" to compilerBase.resolve(cCompiler.exe).absolutePath, - "CXX" to compilerBase.resolve(cppCompiler.exe).absolutePath, - "LD" to compilerBase.resolve(linker.exe).absolutePath, - "GOOS" to "android", - "GOARCH" to golangArch, - "CGO_ENABLED" to "1", - "CFLAGS" to "-O3 -Werror" - ) -} - -fun String.exec(pwd: File = buildDir, env: Map = System.getenv()): String { - val process = ProcessBuilder().run { - if ( Os.isFamily(Os.FAMILY_WINDOWS) ) - command("cmd.exe", "/c", this@exec) - else - command("bash", "-c", this@exec) - - environment().putAll(env) - directory(pwd) - - redirectErrorStream(true) - - start() - } - - val outputStream = ByteArrayOutputStream() - process.inputStream.copyTo(outputStream) - - if ( process.waitFor() != 0 ) { - println(outputStream.toString("utf-8")) - throw GradleScriptException("Exec $this failure", IOException()) - } - - return outputStream.toString("utf-8") -} - -task("compileClashCore") { - onlyIf { - val sourceModified = golangSource.walk() - .filter { - when ( it.extension ) { - "c", "cpp", "h", "go", "mod" -> true - else -> false - } - } - .map { it.lastModified() } - .max() ?: Long.MAX_VALUE - val targetModified = golangOutput.walk() - .filter { it.extension == "so" } - .map { it.lastModified() } - .min() ?: Long.MIN_VALUE - - sourceModified > targetModified - } - - doLast { - nativeAbis.parallelStream().forEach { - val env = generateGolangBuildEnvironment(it) - val out = golangOutput.resolve(it).apply { - mkdirs() - }.resolve("libclash.so") - - "go build --buildmode=c-shared -trimpath -o \"$out\"".exec(pwd = golangSource, env = env) - } - } -} - -task("downloadGeoipDatabase") { - val geoipFile = geoipOutput.resolve("Country.mmdb") - - onlyIf { - System.currentTimeMillis() - geoipFile.lastModified() > geoipInvalidate.toMillis() - } - - doLast { - geoipOutput.mkdirs() - - URL(geoipDatabaseUrl).openConnection().getInputStream().use { input -> - FileOutputStream(geoipFile).use { output -> - input.copyTo(output) - } - } - } -} diff --git a/core/consumer-rules.pro b/core/consumer-rules.pro deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/core/proguard-rules.pro b/core/proguard-rules.pro deleted file mode 100644 index 6e7ffa997e..0000000000 --- a/core/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml deleted file mode 100644 index 8fdfeffc1c..0000000000 --- a/core/src/main/AndroidManifest.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - diff --git a/core/src/main/cpp/CMakeLists.txt b/core/src/main/cpp/CMakeLists.txt deleted file mode 100644 index c63d7c8653..0000000000 --- a/core/src/main/cpp/CMakeLists.txt +++ /dev/null @@ -1,11 +0,0 @@ -cmake_minimum_required(VERSION 3.0) -project(clash_bridge) - -set(CMAKE_C_STANDARD 11) -set(CMAKE_CXX_STANDARD 11) - -include_directories(${CLASH_OUTPUT}/${ANDROID_ABI} ${CLASH_SOURCE}) -link_directories(${CLASH_OUTPUT}/${CMAKE_ANDROID_ARCH_ABI}) -link_libraries(log clash) - -add_library(bridge SHARED main.cpp main.h init.cpp query.cpp patch.cpp tun.cpp defer.cpp log.cpp event_queue.cpp) \ No newline at end of file diff --git a/core/src/main/cpp/defer.cpp b/core/src/main/cpp/defer.cpp deleted file mode 100644 index b40671672e..0000000000 --- a/core/src/main/cpp/defer.cpp +++ /dev/null @@ -1,112 +0,0 @@ -#include "main.h" - -#include - -static std::pair completableFutureWithToken(Master::Context *context) { - uint64_t token = EventQueue::getInstance()->obtainToken(); - jobject completableFuture = context->newGlobalReference(context->newCompletableFuture()); - - EventQueue::getInstance()->registerHandler(COMPLETE, token, [completableFuture](const event_t *event) { - EventQueue::getInstance()->unregisterHandler(COMPLETE, event->token); - - Master::runWithAttached([&](JNIEnv *env) -> int { - Master::runWithContext(env, [&](Master::Context *context) { - if ( strlen(event->payload) == 0 ) { - context->completeCompletableFuture(completableFuture, nullptr); - } else { - context->completeExceptionallyCompletableFuture(completableFuture, context->newClashException(event->payload)); - } - - context->removeGlobalReference(completableFuture); - }); - - return 0; - }); - }); - - return {completableFuture, token}; -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_downloadProfile__ILjava_lang_String_2Ljava_lang_String_2( - JNIEnv *env, jclass clazz, jint fd, jstring base, jstring output) { - UNUSED(clazz); - - return Master::runWithContext(env, [&](Master::Context *context) -> jobject { - const char *b = context->getString(base); - const char *o = context->getString(output); - - auto completableFuture = completableFutureWithToken(context); - - downloadProfileFromFd(fd, b, o, completableFuture.second); - - context->releaseString(base, b); - context->releaseString(output, o); - - return completableFuture.first; - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_downloadProfile__Ljava_lang_String_2Ljava_lang_String_2Ljava_lang_String_2( - JNIEnv *env, jclass clazz, jstring url, jstring base, jstring output) { - UNUSED(clazz); - - return Master::runWithContext(env, [&](Master::Context *context) -> jobject { - const char *u = context->getString(url); - const char *b = context->getString(base); - const char *o = context->getString(output); - - auto completableFuture = completableFutureWithToken(context); - - downloadProfileFromUrl(u, b, o, completableFuture.second); - - context->releaseString(url, u); - context->releaseString(base, b); - context->releaseString(output, o); - - return completableFuture.first; - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_loadProfile(JNIEnv *env, jclass clazz, jstring path, - jstring base) { - UNUSED(clazz); - - return Master::runWithContext(env, [&](Master::Context *context) -> jobject { - const char *p = context->getString(path); - const char *b = context->getString(base); - - auto completableFuture = completableFutureWithToken(context); - - loadProfile(p, b, completableFuture.second); - - context->releaseString(path, p); - context->releaseString(base, b); - - return completableFuture.first; - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_performHealthCheck(JNIEnv *env, jclass clazz, - jstring group) { - UNUSED(clazz); - - return Master::runWithContext(env, [&](Master::Context *context) -> jobject { - const char *g = context->getString(group); - - auto completableFuture = completableFutureWithToken(context); - - performHealthCheck(g, completableFuture.second); - - context->releaseString(group, g); - - return completableFuture.first; - }); -} \ No newline at end of file diff --git a/core/src/main/cpp/event_queue.cpp b/core/src/main/cpp/event_queue.cpp deleted file mode 100644 index 53d9f7f7ee..0000000000 --- a/core/src/main/cpp/event_queue.cpp +++ /dev/null @@ -1,101 +0,0 @@ -#include "event_queue.h" - -static void *handleEventQueue(void *context) { - auto *queue = reinterpret_cast(context); - - while ( !queue->isClosed() ) { - const event_t *e = queue->dequeueEvent(); - - EventQueue::Handler h = queue->findHandler(e->type, e->token); - - h(e); - - answer_event(e); - } - - return nullptr; -} - -EventQueue::EventQueue(): lock(), condition(), closed(false), currentToken(0) { - instance = this; - - pthread_mutex_init(&lock, nullptr); - pthread_cond_init(&condition, nullptr); - - for ( int i = 0 ; i < DEFAULT_EVENT_QUEUE_PROCESSES ; i++ ) { - pthread_t tid = 0; - - if ( pthread_create(&tid, nullptr, &handleEventQueue, this) < 0 ) - abort(); - } -} - -void EventQueue::enqueueEvent(const event_t *event) { - pthread_mutex_lock(&lock); - - queue.push_back(event); - - pthread_cond_signal(&condition); - - pthread_mutex_unlock(&lock); -} - -const event_t *EventQueue::dequeueEvent() { - pthread_mutex_lock(&lock); - - while ( queue.empty() ) - pthread_cond_wait(&condition, &lock); - - auto *result = queue.back(); - - queue.pop_back(); - - pthread_mutex_unlock(&lock); - - return result; -} - -void EventQueue::registerHandler(event_type_t type, uint64_t token, const EventQueue::Handler& handler) { - pthread_mutex_lock(&lock); - - handlers[type][token] = handler; - - pthread_mutex_unlock(&lock); -} - -void EventQueue::unregisterHandler(event_type_t type, uint64_t token) { - pthread_mutex_lock(&lock); - - handlers[type].erase(token); - - pthread_mutex_unlock(&lock); -} - -EventQueue::Handler EventQueue::findHandler(event_type_t type, uint64_t token) { - pthread_mutex_lock(&lock); - - Handler result = handlers[type][token]; - - pthread_mutex_unlock(&lock); - - if (result == nullptr) - return [](const event_t*){}; - - return result; -} - -EventQueue *EventQueue::getInstance() { - return instance; -} - -uint64_t EventQueue::obtainToken() { - pthread_mutex_lock(&lock); - - uint64_t r = currentToken++; - - pthread_mutex_unlock(&lock); - - return r; -} - -EventQueue *EventQueue::instance; \ No newline at end of file diff --git a/core/src/main/cpp/event_queue.h b/core/src/main/cpp/event_queue.h deleted file mode 100644 index 0a9087a4fe..0000000000 --- a/core/src/main/cpp/event_queue.h +++ /dev/null @@ -1,51 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "event.h" - -#define DEFAULT_EVENT_QUEUE_PROCESSES 8 - -class EventQueue { -public: - typedef std::function Handler; - -public: - EventQueue(); - -public: - void enqueueEvent(const event_t *event); - const event_t *dequeueEvent(); - -public: - void registerHandler(event_type_t type, uint64_t token, const Handler& handler); - void unregisterHandler(event_type_t type, uint64_t token); - Handler findHandler(event_type_t type, uint64_t token); - -public: - uint64_t obtainToken(); - -public: - static EventQueue *getInstance(); - -public: - inline bool isClosed() { - return closed; - } - -private: - bool closed; - std::vector queue; - std::map> handlers; - pthread_mutex_t lock; - pthread_cond_t condition; - uint64_t currentToken; - -public: - static EventQueue *instance; -}; \ No newline at end of file diff --git a/core/src/main/cpp/init.cpp b/core/src/main/cpp/init.cpp deleted file mode 100644 index 83aabb1b24..0000000000 --- a/core/src/main/cpp/init.cpp +++ /dev/null @@ -1,30 +0,0 @@ -#include "main.h" - -extern "C" -JNIEXPORT void JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_initialize(JNIEnv *env, jclass clazz, - jbyteArray database, jstring home, - jstring version) { - UNUSED(clazz); - - Master::runWithContext(env, [&](Master::Context *context) { - const_buffer_t databaseBuffer = context->createConstBufferFromByteArray(database); - const char *homeString = context->getString(home); - const char *versionString = context->getString(version); - - initialize(&databaseBuffer, homeString, versionString); - - context->releaseConstBufferFromByteArray(database, databaseBuffer); - context->releaseString(home, homeString); - context->releaseString(version, versionString); - }); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_reset(JNIEnv *env, jclass clazz) { - UNUSED(env); - UNUSED(clazz); - - reset(); -} \ No newline at end of file diff --git a/core/src/main/cpp/log.cpp b/core/src/main/cpp/log.cpp deleted file mode 100644 index 72a41e919a..0000000000 --- a/core/src/main/cpp/log.cpp +++ /dev/null @@ -1,51 +0,0 @@ -#include "libclash.h" -#include "main.h" - -static jobject logCallback; - -extern "C" -JNIEXPORT void JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_setLogCallback(JNIEnv *env, jclass clazz, - jobject callback) { - UNUSED(clazz); - - Master::runWithContext(env, [&](Master::Context *context) { - if ( logCallback != nullptr ) - context->removeGlobalReference(logCallback); - - if ( callback == nullptr ) { - logCallback = nullptr; - return; - } - - logCallback = context->newGlobalReference(callback); - - EventQueue::getInstance()->registerHandler(LOG_RECEIVED, 0, [](const event_t *event) { - Master::runWithAttached([&](JNIEnv *env) { - Master::runWithContext(env, [&](Master::Context *context) { - context->logCallbackMessage(logCallback, event->payload); - }); - - return 0; - }); - }); - }); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_enableLogReport(JNIEnv *env, jclass clazz) { - UNUSED(env); - UNUSED(clazz); - - enableLogReport(); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_disableLogReport(JNIEnv *env, jclass clazz) { - UNUSED(env); - UNUSED(clazz); - - disableLogReport(); -} \ No newline at end of file diff --git a/core/src/main/cpp/main.cpp b/core/src/main/cpp/main.cpp deleted file mode 100644 index 3a9b0e9874..0000000000 --- a/core/src/main/cpp/main.cpp +++ /dev/null @@ -1,257 +0,0 @@ -#include "main.h" - -#include - -Master *Master::master = nullptr; - -template -inline T g(JNIEnv *env, T object) { - return reinterpret_cast(env->NewGlobalRef(object)); -} - -Master::Master(JavaVM *vm, JNIEnv *env): vm(vm) { - master = this; - - cClashException = g(env, env->FindClass("com/github/kr328/clash/core/bridge/ClashException")); - cTraffic = g(env, env->FindClass("com/github/kr328/clash/core/model/Traffic")); - cGeneral = g(env, env->FindClass("com/github/kr328/clash/core/model/General")); - cCompletableFuture = g(env, env->FindClass("java/util/concurrent/CompletableFuture")); - cProxyGroup = g(env, env->FindClass("com/github/kr328/clash/core/model/ProxyGroup")); - cProxy = g(env, env->FindClass("com/github/kr328/clash/core/model/Proxy")); - cLogEvent = g(env, env->FindClass("com/github/kr328/clash/core/event/LogEvent")); - iTunCallback = g(env, env->FindClass("com/github/kr328/clash/core/bridge/TunCallback")); - iLogCallback = g(env, env->FindClass("com/github/kr328/clash/core/bridge/LogCallback")); - cClashExceptionConstructor = env->GetMethodID(cClashException, "", - "(Ljava/lang/String;)V"); - cTrafficConstructor = env->GetMethodID(cTraffic, "", "(JJ)V"); - cGeneralConstructor = env->GetMethodID(cGeneral, "", "(Ljava/lang/String;IIII)V"); - cCompletableFutureConstructor = env->GetMethodID(cCompletableFuture, "", "()V"); - cProxyGroupConstructor = env->GetMethodID(cProxyGroup, "", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Lcom/github/kr328/clash/core/model/Proxy;)V"); - cProxyConstructor = env->GetMethodID(cProxy, "", "(Ljava/lang/String;Ljava/lang/String;J)V"); - cLogEventConstructor = env->GetMethodID(cLogEvent, "", "(Ljava/lang/String;)V"); - mCompletableFutureComplete = env->GetMethodID(cCompletableFuture, "complete", - "(Ljava/lang/Object;)Z"); - mCompletableFutureCompleteExceptionally = env->GetMethodID(cCompletableFuture, - "completeExceptionally", - "(Ljava/lang/Throwable;)Z"); - mTunCallbackOnNewSocket = env->GetMethodID(iTunCallback, "onNewSocket", "(I)V"); - mTunCallbackOnStop = env->GetMethodID(iTunCallback, "onStop", "()V"); - mLogCallbackOnMessage = env->GetMethodID(iLogCallback, "onMessage", "(Lcom/github/kr328/clash/core/event/LogEvent;)V"); - - sDirect = g(env, env->NewStringUTF("Direct")); - sReject = g(env, env->NewStringUTF("Reject")); - sShadowsocks = g(env, env->NewStringUTF("Shadowsocks")); - sSnell = g(env, env->NewStringUTF("Snell")); - sSocks5 = g(env, env->NewStringUTF("Socks5")); - sHttp = g(env, env->NewStringUTF("Http")); - sVmess = g(env, env->NewStringUTF("Vmess")); - sTrojan = g(env, env->NewStringUTF("Trojan")); - sRelay = g(env, env->NewStringUTF("Relay")); - sSelector = g(env, env->NewStringUTF("Selector")); - sFallback = g(env, env->NewStringUTF("Fallback")); - sURLTest = g(env, env->NewStringUTF("URLTest")); - sLoadBalance = g(env, env->NewStringUTF("LoadBalance")); - sUnknown = g(env, env->NewStringUTF("Unknown")); -} - -Master::Context::Context(JNIEnv *env) { - this->env = env; -} - -jthrowable Master::Context::newClashException(const char *reason) { - return reinterpret_cast(env->NewObject(master->cClashException, - master->cClashExceptionConstructor, - env->NewStringUTF(reason))); -} - -void Master::Context::throwThrowable(jthrowable throwable) { - env->Throw(throwable); -} - -jobject Master::Context::newTraffic(jlong upload, jlong download) { - return env->NewObject(master->cTraffic, master->cTrafficConstructor, upload, download); -} - -jobject Master::Context::newGeneral(char const *mode, jint http, jint socks, jint redirect, - jint mixed) { - return env->NewObject(master->cGeneral, master->cGeneralConstructor, - env->NewStringUTF(mode), http, socks, redirect, mixed); -} - -jobject Master::Context::newCompletableFuture() { - return env->NewObject(master->cCompletableFuture, master->cCompletableFutureConstructor); -} - -jobject Master::Context::newGlobalReference(jobject obj) { - return env->NewGlobalRef(obj); -} - -jobject Master::Context::removeGlobalReference(jobject obj) { - env->DeleteGlobalRef(obj); - - return obj; -} - -bool Master::Context::completeCompletableFuture(jobject completable, jobject object) { - return env->CallBooleanMethod(completable, master->mCompletableFutureComplete, object); -} - -bool -Master::Context::completeExceptionallyCompletableFuture(jobject completable, jthrowable throwable) { - return env->CallBooleanMethod(completable, master->mCompletableFutureCompleteExceptionally, - throwable); -} - -const_buffer_t Master::Context::createConstBufferFromByteArray(jbyteArray array) { - return { - .buffer = env->GetByteArrayElements(array, nullptr), - .length = env->GetArrayLength(array) - }; -} - -void Master::Context::releaseConstBufferFromByteArray(jbyteArray array, const_buffer_t &buffer) { - env->ReleaseByteArrayElements(array, const_cast(reinterpret_cast(buffer.buffer)), JNI_ABORT); -} - -const char *Master::Context::getString(jstring str) { - return env->GetStringUTFChars(str, nullptr); -} - -void Master::Context::releaseString(jstring str, const char *c) { - env->ReleaseStringUTFChars(str, c); -} - -void Master::Context::tunCallbackNewSocket(jobject callback, jint fd) { - env->CallVoidMethod(callback, master->mTunCallbackOnNewSocket, fd); -} - -void Master::Context::tunCallbackStop(jobject callback) { - env->CallVoidMethod(callback, master->mTunCallbackOnStop); -} - -jobjectArray Master::Context::createProxyGroupArray(int size, jobject elements[]) { - jobjectArray result = env->NewObjectArray(size, master->cProxyGroup, nullptr); - - for ( int i = 0 ; i < size ; i++ ) - env->SetObjectArrayElement(result, i, elements[i]); - - return result; -} - -jobjectArray Master::Context::createProxyArray(int size, jobject elements[]) { - jobjectArray result = env->NewObjectArray(size, master->cProxy, nullptr); - - for ( int i = 0 ; i < size ; i++ ) - env->SetObjectArrayElement(result, i, elements[i]); - - return result; -} - -jobject Master::Context::createProxy(char const *name, proxy_type_t type, jlong delay) { - jstring ts = nullptr; - - switch (type) { - case Direct: - ts = master->sDirect; - break; - case Reject: - ts = master->sReject; - break; - case Socks5: - ts = master->sSocks5; - break; - case Http: - ts = master->sHttp; - break; - case Shadowsocks: - ts = master->sShadowsocks; - break; - case Vmess: - ts = master->sVmess; - break; - case Snell: - ts = master->sSnell; - break; - case Trojan: - ts = master->sTrojan; - break; - case Selector: - ts = master->sSelector; - break; - case Fallback: - ts = master->sFallback; - break; - case LoadBalance: - ts = master->sLoadBalance; - break; - case URLTest: - ts = master->sURLTest; - break; - case Relay: - ts = master->sRelay; - break; - case Unknown: - ts = master->sUnknown; - break; - default: - ts = master->sUnknown; - } - - return env->NewObject(master->cProxy, master->cProxyConstructor, env->NewStringUTF(name), ts, delay); -} - -jobject Master::Context::createProxyGroup(char const *name, proxy_type_t type, - char const *current, jobjectArray proxies) { - jstring ts = nullptr; - - switch (type) { - case Selector: - ts = master->sSelector; - break; - case Fallback: - ts = master->sFallback; - break; - case LoadBalance: - ts = master->sLoadBalance; - break; - case URLTest: - ts = master->sURLTest; - break; - case Relay: - ts = master->sRelay; - break; - case Unknown: - ts = master->sUnknown; - break; - default: - ts = master->sUnknown; - } - - return env->NewObject(master->cProxyGroup, master->cProxyGroupConstructor, env->NewStringUTF(name), ts, env->NewStringUTF(current), proxies); -} - -void Master::Context::logCallbackMessage(jobject callback, const char *data) { - jobject event = env->NewObject(master->cLogEvent, master->cLogEventConstructor, env->NewStringUTF(data)); - - env->CallVoidMethod(callback, master->mLogCallbackOnMessage, event); -} - -static void enqueue_event(const event_t *e) { - EventQueue::getInstance()->enqueueEvent(e); -} - -JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *unused) { - UNUSED(unused); - - JNIEnv *env = nullptr; - - if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) - return -1; - - new Master(vm, env); - new EventQueue(); - - set_event_handler(&enqueue_event); - - return JNI_VERSION_1_6; -} \ No newline at end of file diff --git a/core/src/main/cpp/main.h b/core/src/main/cpp/main.h deleted file mode 100644 index 24d4376048..0000000000 --- a/core/src/main/cpp/main.h +++ /dev/null @@ -1,129 +0,0 @@ -#include -#include -#include -#include - -#include "libclash.h" -#include "event_queue.h" - -#define UNUSED(v) ((void)v) - -class Master { -public: - class Context; - -public: - Master(JavaVM *vm, JNIEnv *env); - -public: - template static R runWithContext(JNIEnv *env, const std::function& func); - template static R runWithAttached(const std::function &func); - -private: - jclass cClashException; - jclass cTraffic; - jclass cGeneral; - jclass cCompletableFuture; - jclass cProxyGroup; - jclass cProxy; - jclass cLogEvent; - jclass iTunCallback; - jclass iLogCallback; - jmethodID cClashExceptionConstructor; - jmethodID cTrafficConstructor; - jmethodID cGeneralConstructor; - jmethodID cCompletableFutureConstructor; - jmethodID cProxyGroupConstructor; - jmethodID cProxyConstructor; - jmethodID cLogEventConstructor; - jmethodID mCompletableFutureComplete; - jmethodID mCompletableFutureCompleteExceptionally; - jmethodID mTunCallbackOnNewSocket; - jmethodID mTunCallbackOnStop; - jmethodID mLogCallbackOnMessage; - -private: - jstring sDirect; - jstring sReject; - jstring sShadowsocks; - jstring sSnell; - jstring sSocks5; - jstring sHttp; - jstring sVmess; - jstring sTrojan; - jstring sRelay; - jstring sSelector; - jstring sFallback; - jstring sURLTest; - jstring sLoadBalance; - jstring sUnknown; - -private: - JavaVM *vm; - -private: - static Master *master; - -private: - friend class Context; -}; - -class Master::Context { -public: -public: - Context(JNIEnv *env); - -public: - jthrowable newClashException(const char *message); - jobject newTraffic(jlong upload, jlong download); - jobject newGeneral(char const *mode, jint http, jint socks, jint redirect, jint mixed); - jobject newCompletableFuture(); - -public: - void throwThrowable(jthrowable throwable); - -public: - jobject newGlobalReference(jobject obj); - jobject removeGlobalReference(jobject obj); - -public: - bool completeCompletableFuture(jobject completable, jobject object); - bool completeExceptionallyCompletableFuture(jobject completable, jthrowable throwable); - void tunCallbackNewSocket(jobject callback, jint fd); - void tunCallbackStop(jobject callback); - void logCallbackMessage(jobject callback, const char *data); - -public: - jobject createProxy(char const *name, proxy_type_t type, jlong delay); - jobject createProxyGroup(char const *name, proxy_type_t type, char const *current, jobjectArray proxies); - jobjectArray createProxyArray(int size, jobject elements[]); - jobjectArray createProxyGroupArray(int size, jobject elements[]); - const_buffer_t createConstBufferFromByteArray(jbyteArray array); - void releaseConstBufferFromByteArray(jbyteArray array, const_buffer_t &buffer); - const char *getString(jstring str); - void releaseString(jstring str, const char *c); - -private: - JNIEnv *env; -}; - -template -R Master::runWithContext(JNIEnv *env, const std::function& func) { - Master::Context context(env); - - return func(&context); -} - -template R Master::runWithAttached(const std::function &func) { - Master *m = Master::master; - - JNIEnv *env; - - m->vm->AttachCurrentThread(&env, nullptr); - - R result = func(env); - - m->vm->DetachCurrentThread(); - - return result; -} \ No newline at end of file diff --git a/core/src/main/cpp/patch.cpp b/core/src/main/cpp/patch.cpp deleted file mode 100644 index 45ce87197a..0000000000 --- a/core/src/main/cpp/patch.cpp +++ /dev/null @@ -1,70 +0,0 @@ -#include "main.h" - -#include - -extern "C" -JNIEXPORT void JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_setProxyMode(JNIEnv *env, jclass clazz, - jstring proxy_mode) { - Master::runWithContext(env, [&](Master::Context *context) { - const char *m = context->getString(proxy_mode); - int mode; - - if ( strcmp(m, "Direct") == 0 ) - mode = MODE_DIRECT; - else if ( strcmp(m, "Global") == 0 ) - mode = MODE_GLOBAL; - else if ( strcmp(m, "Rule") == 0 ) - mode = MODE_RULE; - else if ( strcmp(m, "Script") == 0 ) - mode = MODE_SCRIPT; - else - mode = MODE_UNKNOWN; - - setProxyMode(mode); - }); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_setDnsOverride(JNIEnv *env, jclass clazz, - jboolean override_dns, - jstring append_dns) { - Master::runWithContext(env, [&](Master::Context *context) { - const char *appendDns = context->getString(append_dns); - int override = 1; - - if ( override_dns ) - override = 1; - else - override = 0; - - dns_override_t dns = { - .override_dns = override, - .append_dns = appendDns - }; - - setDnsOverride(&dns); - - context->releaseString(append_dns, appendDns); - }); -} - -extern "C" -JNIEXPORT jboolean JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_setSelector(JNIEnv *env, jclass clazz, jstring group, - jstring selected) { - UNUSED(clazz); - - return Master::runWithContext(env, [&](Master::Context *context) -> bool { - const char *g = context->getString(group); - const char *s = context->getString(selected); - - int r = setSelector(g, s); - - context->releaseString(group, g); - context->releaseString(selected, s); - - return r == 0; - }); -} \ No newline at end of file diff --git a/core/src/main/cpp/query.cpp b/core/src/main/cpp/query.cpp deleted file mode 100644 index 68c36a4ced..0000000000 --- a/core/src/main/cpp/query.cpp +++ /dev/null @@ -1,109 +0,0 @@ -#include "main.h" - -extern "C" -JNIEXPORT jobject JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_queryGeneral(JNIEnv *env, jclass clazz) { - UNUSED(clazz); - - return Master::runWithContext(env, [&](Master::Context *context) -> jobject { - general_t general; - - queryGeneral(&general); - - const char *mode = nullptr; - - switch (general.mode) { - case MODE_DIRECT: - mode = "Direct"; - break; - case MODE_GLOBAL: - mode = "Global"; - break; - case MODE_RULE: - mode = "Rule"; - break; - case MODE_SCRIPT: - mode = "Script"; - break; - } - - return context->newGeneral(mode, - general.http_port, general.socks_port, - general.redirect_port, general.mixed_port); - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_queryBandwidth(JNIEnv *env, jclass clazz) { - UNUSED(clazz); - - return Master::runWithContext(env, [&](Master::Context *context) -> jobject { - traffic_t traffic; - - queryBandwidth(&traffic); - - return context->newTraffic(traffic.upload, traffic.download); - }); -} - -extern "C" -JNIEXPORT jobject JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_querySpeed(JNIEnv *env, jclass clazz) { - UNUSED(clazz); - - return Master::runWithContext(env, [&](Master::Context *context) -> jobject { - traffic_t traffic; - - querySpeed(&traffic); - - return context->newTraffic(traffic.upload, traffic.download); - }); -} - -extern "C" -JNIEXPORT jobjectArray JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_queryProxyGroups(JNIEnv *env, jclass clazz) { - UNUSED(clazz); - - return Master::runWithContext(env, [&](Master::Context *context) -> jobjectArray { - proxy_group_list_t *list = queryProxyGroups(); - auto *jgroups = new jobject[list->size]; - - for (int group_index = 0 ; group_index < list->size ; group_index++ ) { - char const * now = ""; - proxy_group_t *group = list->groups[group_index]; - auto *jproxies = new jobject[group->proxies_size]; - const char *group_name = &list->string_pool[group->base.name_index]; - - for ( int proxy_index = 0 ; proxy_index < group->proxies_size ; proxy_index++ ) { - proxy_t *proxy = &group->proxies[proxy_index]; - const char *name = &list->string_pool[proxy->name_index]; - - jproxies[proxy_index] = context->createProxy(name, proxy->proxy_type, proxy->delay); - - if ( proxy_index == group->now ) - now = name; - } - - jgroups[group_index] = context->createProxyGroup( - group_name, - group->base.proxy_type, - now, - context->createProxyArray(group->proxies_size, jproxies)); - - delete[] jproxies; - - free(group); - } - - jobjectArray result = context->createProxyGroupArray(list->size, jgroups); - - delete[] jgroups; - - free(list->string_pool); - free(list); - - return result; - }); -} \ No newline at end of file diff --git a/core/src/main/cpp/tun.cpp b/core/src/main/cpp/tun.cpp deleted file mode 100644 index c7b9a70465..0000000000 --- a/core/src/main/cpp/tun.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "main.h" - -#include "event_queue.h" - -extern "C" -JNIEXPORT void JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_startTunDevice(JNIEnv *env, jclass clazz, jint fd, - jint mtu, jstring gateway, - jstring mirror, jstring dns, - jobject callback) { - UNUSED(clazz); - - Master::runWithContext(env, [&](Master::Context *context) { - const char *gatewayString = context->getString(gateway); - const char *mirrorString = context->getString(mirror); - const char *dnsString = context->getString(dns); - - jobject callbackGlobal = context->newGlobalReference(callback); - uint64_t token = EventQueue::getInstance()->obtainToken(); - - EventQueue::getInstance()->registerHandler(NEW_SOCKET, token, [callbackGlobal](const event_t *e) { - Master::runWithAttached([&](JNIEnv *env) -> int { - Master::runWithContext(env, [&](Master::Context *context) { - context->tunCallbackNewSocket(callbackGlobal, static_cast(strtol(e->payload, nullptr, 10))); - }); - - return 0; - }); - }); - - EventQueue::getInstance()->registerHandler(TUN_STOP, token, [callbackGlobal](const event_t *e) { - auto queue = EventQueue::getInstance(); - - queue->unregisterHandler(NEW_SOCKET, e->token); - queue->unregisterHandler(TUN_STOP, e->token); - - Master::runWithAttached([&](JNIEnv *env) -> int { - Master::runWithContext(env, [&](Master::Context *context) { - context->tunCallbackStop(callbackGlobal); - context->removeGlobalReference(callbackGlobal); - }); - - return 0; - }); - }); - - char *exception = startTunDevice(fd, mtu, gatewayString, mirrorString, dnsString, token); - - context->releaseString(gateway, gatewayString); - context->releaseString(mirror, mirrorString); - context->releaseString(dns, dnsString); - - if ( exception != nullptr ) { - context->throwThrowable(context->newClashException(exception)); - context->removeGlobalReference(callbackGlobal); - free(exception); - } - }); -} - -extern "C" -JNIEXPORT void JNICALL -Java_com_github_kr328_clash_core_bridge_Bridge_stopTunDevice(JNIEnv *env, jclass clazz) { - stopTunDevice(); -} \ No newline at end of file diff --git a/core/src/main/golang/buffer.h b/core/src/main/golang/buffer.h deleted file mode 100644 index c12f34dedb..0000000000 --- a/core/src/main/golang/buffer.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#include - -#if __cplusplus -extern "C" { -#endif - -typedef struct buffer_t { - void *buffer; - int length; -} buffer_t; - -typedef struct const_buffer_t { - const void *buffer; - int length; -} const_buffer_t; - -typedef const char *const_string_t; - -#if __cplusplus -}; -#endif \ No newline at end of file diff --git a/core/src/main/golang/clash b/core/src/main/golang/clash deleted file mode 160000 index 4c8a71ecee..0000000000 --- a/core/src/main/golang/clash +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 4c8a71ecee16cda24a75807b88a8bcb8e7c6e3f5 diff --git a/core/src/main/golang/config.go b/core/src/main/golang/config.go deleted file mode 100644 index 6fc47c36bb..0000000000 --- a/core/src/main/golang/config.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -//#include "config.h" -//#include "buffer.h" -import "C" - -import ( - "github.com/kr328/cfa/config" - "strings" -) - -//export setDnsOverride -func setDnsOverride(override *C.dns_override_t) { - overrideDns := override.override_dns != 0 - appendDns := C.GoString(override.append_dns) - - if overrideDns { - config.DnsPatch = config.OptionalDnsPatch - } else { - config.DnsPatch = nil - } - - if len(appendDns) == 0 { - config.NameServersAppend = make([]string, 0) - } else { - config.NameServersAppend = strings.Split(appendDns, ",") - } -} diff --git a/core/src/main/golang/config.h b/core/src/main/golang/config.h deleted file mode 100644 index 92599ab01d..0000000000 --- a/core/src/main/golang/config.h +++ /dev/null @@ -1,14 +0,0 @@ -#pragma once - -#if __cplusplus -extern "C" { -#endif - -typedef struct dns_override_t { - int override_dns; - const char *append_dns; -} dns_override_t; - -#if __cplusplus -}; -#endif \ No newline at end of file diff --git a/core/src/main/golang/config/defaults.go b/core/src/main/golang/config/defaults.go deleted file mode 100644 index 7ae9b4f236..0000000000 --- a/core/src/main/golang/config/defaults.go +++ /dev/null @@ -1,15 +0,0 @@ -package config - -var defaultFakeIPFilter = []string{ - // stun services - "+.stun.*.*", - "+.stun.*.*.*", - "+.stun.*.*.*.*", - - // Google Voices - "lens.l.google.com", - "stun.l.google.com", - - // Nintendo Switch - "*.n.n.srv.nintendo.net", -} diff --git a/core/src/main/golang/config/fetch.go b/core/src/main/golang/config/fetch.go deleted file mode 100644 index fc8da0cffb..0000000000 --- a/core/src/main/golang/config/fetch.go +++ /dev/null @@ -1,106 +0,0 @@ -package config - -import ( - "context" - "errors" - "github.com/kr328/cfa/utils" - "io/ioutil" - "net" - "net/http" - "net/url" - "os" - "syscall" - - "github.com/Dreamacro/clash/adapters/inbound" - "github.com/Dreamacro/clash/component/socks5" - "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/tunnel" -) - -var ApplicationVersion = "Unknown" - -const defaultFileMode = 0600 - -var client = &http.Client{ - Transport: &http.Transport{ - DialContext: func(ctx context.Context, network, address string) (net.Conn, error) { - if network != "tcp" && network != "tcp4" && network != "tcp6" { - return nil, errors.New("Unsupported network type " + network) - } - - client, server := net.Pipe() - - tunnel.Add(inbound.NewSocket(socks5.ParseAddr(address), server, constant.HTTP)) - - return client, nil - }, - }, -} - -func fetchRemote(sUrl string) ([]byte, error) { - uri, err := url.Parse(sUrl) - if err != nil { - return nil, err - } - - request, err := http.NewRequest("GET", uri.String(), nil) - if err != nil { - return nil, err - } - - request.Header.Set("User-Agent", "ClashForAndroid/"+ApplicationVersion) - if user := uri.User; user != nil { - password, _ := user.Password() - request.SetBasicAuth(user.Username(), password) - } - - response, err := client.Do(request) - if err != nil { - return nil, err - } - - defer utils.CloseSilent(response.Body) - - data, err := ioutil.ReadAll(response.Body) - if err != nil { - return nil, err - } - - return data, nil -} - -func fetchLocal(fd int) ([]byte, error) { - _ = syscall.SetNonblock(fd, true) - - file := os.NewFile(uintptr(fd), "/dev/null") - defer utils.CloseSilent(file) - - return ioutil.ReadAll(file) -} - -func DownloadUrl(url, output, baseDir string) error { - data, err := fetchRemote(url) - if err != nil { - return err - } - - return save(data, output, baseDir) -} - -func DownloadFd(fd int, output, baseDir string) error { - data, err := fetchLocal(fd) - if err != nil { - return err - } - - return save(data, output, baseDir) -} - -func save(data []byte, output, baseDir string) error { - _, err := parseConfig(data, baseDir) - if err != nil { - return err - } - - return ioutil.WriteFile(output, data, defaultFileMode) -} diff --git a/core/src/main/golang/config/load.go b/core/src/main/golang/config/load.go deleted file mode 100644 index f71612af3e..0000000000 --- a/core/src/main/golang/config/load.go +++ /dev/null @@ -1,75 +0,0 @@ -package config - -import ( - "errors" - "io/ioutil" - - "github.com/Dreamacro/clash/config" - "github.com/Dreamacro/clash/hub/executor" - "github.com/Dreamacro/clash/log" - "github.com/kr328/cfa/tun" -) - -// LoadDefault - load default configure -func LoadDefault() { - DnsPatch = nil - NameServersAppend = make([]string, 0) - - defaultC, err := config.Parse([]byte{}) - if err != nil { - log.Warnln("Load Default Failure " + err.Error()) - return - } - - executor.ApplyConfig(defaultC, true) - - tun.InitialResolver() -} - -// LoadFromFile - load file -func LoadFromFile(path, baseDir string) error { - data, err := ioutil.ReadFile(path) - if err != nil { - return err - } - - cfg, err := parseConfig(data, baseDir) - if err != nil { - return err - } - - for _, ns := range cfg.DNS.NameServer { - log.Infoln("DNS: %s", ns.Addr) - } - - executor.ApplyConfig(cfg, true) - - tun.InitialResolver() - - log.Infoln("Profile " + path + " loaded") - - return nil -} - -func parseConfig(data []byte, baseDir string) (*config.Config, error) { - raw, err := config.UnmarshalRawConfig(data) - if err != nil { - return nil, err - } - - patchRawConfig(raw) - - if len(raw.Proxy) == 0 && len(raw.ProxyProvider) == 0 && - len(raw.ProxyOld) == 0 && len(raw.ProxyProviderOld) == 0 { - return nil, errors.New("Empty Profile") - } - - cfg, err := config.ParseRawConfig(raw, baseDir) - if err != nil { - return nil, err - } - - patchConfig(cfg) - - return cfg, nil -} diff --git a/core/src/main/golang/config/patch.go b/core/src/main/golang/config/patch.go deleted file mode 100644 index 32266f4509..0000000000 --- a/core/src/main/golang/config/patch.go +++ /dev/null @@ -1,93 +0,0 @@ -package config - -import ( - "github.com/Dreamacro/clash/component/fakeip" - "github.com/Dreamacro/clash/config" - "github.com/Dreamacro/clash/dns" - "net/url" -) - -var ( - OptionalDnsPatch *config.RawDNS - DnsPatch *config.RawDNS - NameServersAppend []string - - cachedPool *fakeip.Pool -) - -func init() { - defaultNameServers := []string{ - "223.5.5.5", - "119.29.29.29", - "1.1.1.1", - "208.67.222.222", - } - - OptionalDnsPatch = &config.RawDNS{ - Enable: true, - IPv6: true, - NameServer: defaultNameServers, - Fallback: []string{}, - FallbackFilter: config.RawFallbackFilter{ - GeoIP: false, - IPCIDR: []string{}, - }, - Listen: ":0", - EnhancedMode: dns.FAKEIP, - FakeIPRange: "198.18.0.0/16", - FakeIPFilter: defaultFakeIPFilter, - DefaultNameserver: defaultNameServers, - } -} - -func patchRawConfig(rawConfig *config.RawConfig) { - rawConfig.DNS.FakeIPRange = "198.18.0.0/16" - rawConfig.Experimental.Interface = "" - rawConfig.ExternalUI = "" - rawConfig.ExternalController = "" - - if d := DnsPatch; d != nil { - rawConfig.DNS = *d - } else if d := OptionalDnsPatch; d != nil { - if !rawConfig.DNS.Enable { - rawConfig.DNS = *d - } - } - - if nameServersAppend := NameServersAppend; len(nameServersAppend) > 0 { - d := &rawConfig.DNS - nameServers := make([]string, len(nameServersAppend)+len(d.NameServer)) - copy(nameServers, nameServersAppend) - copy(nameServers[len(nameServersAppend):], d.NameServer) - - d.NameServer = nameServers - } - - providers := rawConfig.ProxyProvider - - if len(rawConfig.ProxyProvider) == 0 { - providers = rawConfig.ProxyProviderOld - } - - for _, provider := range providers { - path, ok := provider["path"].(string) - if !ok { - continue - } - - provider["path"] = url.QueryEscape(path) - } -} - -func patchConfig(config *config.Config) { - if config.DNS.FakeIPRange != nil { - if c := cachedPool; c != nil { - if config.DNS.FakeIPRange.Gateway().String() == c.Gateway().String() { - c.OverrideHostFrom(config.DNS.FakeIPRange) - config.DNS.FakeIPRange = c - } - } else { - cachedPool = config.DNS.FakeIPRange - } - } -} diff --git a/core/src/main/golang/defer.go b/core/src/main/golang/defer.go deleted file mode 100644 index 25a2e82d81..0000000000 --- a/core/src/main/golang/defer.go +++ /dev/null @@ -1,112 +0,0 @@ -package main - -import ( - "github.com/Dreamacro/clash/adapters/outbound" - "github.com/Dreamacro/clash/adapters/outboundgroup" - "github.com/Dreamacro/clash/adapters/provider" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" - "github.com/kr328/cfa/config" - "sync" -) - -//#include "buffer.h" -//#include "event.h" -import "C" - -//export downloadProfileFromFd -func downloadProfileFromFd(fd int, base C.const_string_t, output C.const_string_t, callbackId uint64) { - b := C.GoString(base) - o := C.GoString(output) - - go func() { - err := config.DownloadFd(fd, o, b) - if err != nil { - sendEvent(C.COMPLETE, callbackId, err.Error()) - } else { - sendEvent(C.COMPLETE, callbackId, "") - } - }() -} - -//export downloadProfileFromUrl -func downloadProfileFromUrl(url C.const_string_t, base C.const_string_t, output C.const_string_t, callbackId uint64) { - u := C.GoString(url) - b := C.GoString(base) - o := C.GoString(output) - - go func() { - err := config.DownloadUrl(u, o, b) - if err != nil { - sendEvent(C.COMPLETE, callbackId, err.Error()) - } else { - sendEvent(C.COMPLETE, callbackId, "") - } - }() -} - -//export loadProfile -func loadProfile(path C.const_string_t, base C.const_string_t, callbackId uint64) { - p := C.GoString(path) - b := C.GoString(base) - - go func() { - err := config.LoadFromFile(p, b) - if err != nil { - sendEvent(C.COMPLETE, callbackId, err.Error()) - } else { - sendEvent(C.COMPLETE, callbackId, "") - } - }() -} - -//export performHealthCheck -func performHealthCheck(group C.const_string_t, callbackId uint64) { - g := C.GoString(group) - - go func() { - p := tunnel.Proxies()[g] - if p == nil { - sendEvent(C.COMPLETE, callbackId, "No such proxy group") - - log.Warnln("Perform health check failure: %s not found", g) - - return - } - - pw, ok := p.(*outbound.Proxy) - if !ok { - sendEvent(C.COMPLETE, callbackId, "Invalid group") - - log.Warnln("Perform health check failure: %s not valid group", g) - - return - } - - adapter, ok := pw.ProxyAdapter.(outboundgroup.ProxyGroup) - if !ok { - sendEvent(C.COMPLETE, callbackId, "Invalid group") - - log.Warnln("Perform health check failure: %s not valid group", g) - - return - } - - providers := adapter.GetProxyProviders() - wg := &sync.WaitGroup{} - - wg.Add(len(providers)) - - for _, p := range providers { - go func(p provider.ProxyProvider) { - p.HealthCheck() - - wg.Done() - }(p) - } - - wg.Wait() - - sendEvent(C.COMPLETE, callbackId, "") - }() -} \ No newline at end of file diff --git a/core/src/main/golang/event.c b/core/src/main/golang/event.c deleted file mode 100644 index 521fe38781..0000000000 --- a/core/src/main/golang/event.c +++ /dev/null @@ -1,29 +0,0 @@ -#include "event.h" - -#include -#include - -extern void answerEvent(int64_t id); - -static event_handler_t event_handler; - -void set_event_handler(event_handler_t handler) { - event_handler = handler; -} - -void send_event(event_t *event, const void *payload, size_t payload_length) { - event_handler_t h = event_handler; - if ( h != NULL ) { - memcpy(event->payload, payload, payload_length); - h(event); - } - else { - answer_event(event); - } -} - -void answer_event(const event_t *event) { - answerEvent(event->id); - - free((void*)event); -} \ No newline at end of file diff --git a/core/src/main/golang/event.go b/core/src/main/golang/event.go deleted file mode 100644 index 186a9938dc..0000000000 --- a/core/src/main/golang/event.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -//#include "event.h" -import "C" -import ( - "sync" - "unsafe" -) - -type EventWaiter struct { - result chan struct{} -} - -var idLock = sync.Mutex{} -var currentId = int64(0) -var ids = map[int64]*EventWaiter{} - -//export answerEvent -func answerEvent(id int64) { - idLock.Lock() - defer idLock.Unlock() - - waiter, ok := ids[id] - if ok { - close(waiter.result) - delete(ids, id) - } -} - -func sendEvent(t C.event_type_t, token uint64, payload string) *EventWaiter { - idLock.Lock() - defer idLock.Unlock() - - currentId++ - - id := currentId - r := &EventWaiter{make(chan struct{})} - - ids[id] = r - - p := append([]byte(payload), 0) - e := allocCEvent(len(p)) - - e.id = C.int64_t(id) - e._type = t - e.token = C.int64_t(token) - - C.send_event(e, unsafe.Pointer(&p[0]), C.size_t(len(p))) - - return r -} - -func allocCEvent(payloadLength int) *C.event_t { - return (*C.event_t)(C.malloc(C.sizeof_event_t + C.size_t(payloadLength))) -} - diff --git a/core/src/main/golang/event.h b/core/src/main/golang/event.h deleted file mode 100644 index 99701ad14e..0000000000 --- a/core/src/main/golang/event.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include - -#if __cplusplus -extern "C" { -#endif - -typedef enum { - NEW_SOCKET, TUN_STOP, COMPLETE, LOG_RECEIVED -} event_type_t; - -typedef struct event_t { - int64_t id; - int64_t token; - event_type_t type; - char payload[]; -} event_t; - -typedef void (*event_handler_t)(const event_t *event); - -void set_event_handler(event_handler_t handler); -void send_event(event_t *event, const void *payload, size_t payload_length); -void answer_event(const event_t *event); - -#if __cplusplus -}; -#endif \ No newline at end of file diff --git a/core/src/main/golang/general.go b/core/src/main/golang/general.go deleted file mode 100644 index 75a6e35d5c..0000000000 --- a/core/src/main/golang/general.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -//#include "buffer.h" -//#include "general.h" -import "C" - -import ( - "github.com/Dreamacro/clash/proxy" - "github.com/Dreamacro/clash/tunnel" -) - -//export setProxyMode -func setProxyMode(mode C.int) { - switch mode { - case C.MODE_DIRECT: - tunnel.SetMode(tunnel.Direct) - case C.MODE_GLOBAL: - tunnel.SetMode(tunnel.Global) - case C.MODE_RULE: - tunnel.SetMode(tunnel.Rule) - } -} - -//export queryGeneral -func queryGeneral(general *C.general_t) { - m := tunnel.Mode() - ports := proxy.GetPorts() - - switch m { - case tunnel.Direct: - general.mode = C.MODE_DIRECT - case tunnel.Global: - general.mode = C.MODE_GLOBAL - case tunnel.Rule: - general.mode = C.MODE_RULE - } - - general.http_port = C.int(ports.Port) - general.socks_port = C.int(ports.SocksPort) - general.mixed_port = C.int(ports.MixedPort) - general.redirect_port = C.int(ports.RedirPort) -} \ No newline at end of file diff --git a/core/src/main/golang/general.h b/core/src/main/golang/general.h deleted file mode 100644 index 87b60e00c6..0000000000 --- a/core/src/main/golang/general.h +++ /dev/null @@ -1,23 +0,0 @@ -#pragma once - -#if __cplusplus -extern "C" { -#endif - -static const int MODE_UNKNOWN = -1; -static const int MODE_DIRECT = 0; -static const int MODE_GLOBAL = 1; -static const int MODE_RULE = 2; -static const int MODE_SCRIPT = 3; - -typedef struct general_t { - int mode; - int http_port; - int socks_port; - int redirect_port; - int mixed_port; -} general_t; - -#if __cplusplus -}; -#endif \ No newline at end of file diff --git a/core/src/main/golang/go.mod b/core/src/main/golang/go.mod deleted file mode 100644 index 42041ea2cf..0000000000 --- a/core/src/main/golang/go.mod +++ /dev/null @@ -1,11 +0,0 @@ -module github.com/kr328/cfa - -go 1.14 - -require ( - github.com/Dreamacro/clash v0.0.0 // local - github.com/kr328/tun2socket v0.0.0-20200613032901-7ffeefc227e3 - github.com/miekg/dns v1.1.29 -) - -replace github.com/Dreamacro/clash => ./clash diff --git a/core/src/main/golang/go.sum b/core/src/main/golang/go.sum deleted file mode 100644 index a5e98ad529..0000000000 --- a/core/src/main/golang/go.sum +++ /dev/null @@ -1,45 +0,0 @@ -github.com/Dreamacro/go-shadowsocks2 v0.1.5/go.mod h1:LSXCjyHesPY3pLjhwff1mQX72ItcBT/N2xNC685cYeU= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= -github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-chi/cors v1.1.1/go.mod h1:K2Yje0VW/SJzxiyMYu6iPQYa7hMjQX2i/F491VChg1I= -github.com/go-chi/render v1.0.1/go.mod h1:pq4Rr7HbnsdaeHagklXub+p6Wd16Af5l9koip1OvJns= -github.com/gofrs/uuid v3.3.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr328/tun2socket v0.0.0-20200613032901-7ffeefc227e3/go.mod h1:FWfSixjrLgtK+dHkDoN6lHMNhvER24gnjUZd/wt8Z9o= -github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= -github.com/oschwald/geoip2-golang v1.4.0/go.mod h1:8QwxJvRImBH+Zl6Aa6MaIcs5YdlZSTKtzmPGzQqi9ng= -github.com/oschwald/maxminddb-golang v1.6.0/go.mod h1:DUJFucBg2cvqx42YmDa/+xHvb0elJtOm3o4aFQ/nb/w= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191224085550-c709ea063b76/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/eapache/channels.v1 v1.1.0/go.mod h1:BHIBujSvu9yMTrTYbTCjDD43gUhtmaOtTWDe7sTv1js= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/core/src/main/golang/log.go b/core/src/main/golang/log.go deleted file mode 100644 index 5ef314db24..0000000000 --- a/core/src/main/golang/log.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -//#include "event.h" -import "C" -import ( - "fmt" - "github.com/Dreamacro/clash/log" - "sync" -) - -var logLocker sync.Mutex -var closeChan chan struct{} - -//export enableLogReport -func enableLogReport() { - logLocker.Lock() - defer logLocker.Unlock() - - - if closeChan != nil { - close(closeChan) - } - - closeChan = make(chan struct{}) - - go func(closed chan struct{}) { - subscriber := log.Subscribe() - defer log.UnSubscribe(subscriber) - defer log.Infoln("Log broadcast disabled") - - for { - select { - case item := <-subscriber: - msg := item.(*log.Event) - - if msg.LogLevel < log.Level() { - continue - } - - <- sendEvent(C.LOG_RECEIVED, 0, fmt.Sprintf("%s:%s", msg.LogLevel.String(), msg.Payload)).result - case <-closeChan: - return - } - } - }(closeChan) -} - -//export disableLogReport -func disableLogReport() { - logLocker.Lock() - defer logLocker.Unlock() - - if closeChan != nil { - close(closeChan) - - closeChan = nil - } -} diff --git a/core/src/main/golang/main.c b/core/src/main/golang/main.c deleted file mode 100644 index e511e31de6..0000000000 --- a/core/src/main/golang/main.c +++ /dev/null @@ -1,24 +0,0 @@ -#include - -#define TAG "ClashForAndroid" - -void log_info(const char *msg) { - __android_log_write(ANDROID_LOG_INFO, TAG, msg); -} - -void log_error(const char *msg) { - __android_log_write(ANDROID_LOG_ERROR, TAG, msg); -} - -void log_warn(const char *msg) { - __android_log_write(ANDROID_LOG_WARN, TAG, msg); -} - -void log_debug(const char *msg) { - __android_log_write(ANDROID_LOG_DEBUG, TAG, msg); -} - -void log_verbose(const char *msg) { - __android_log_write(ANDROID_LOG_VERBOSE, TAG, msg); -} - diff --git a/core/src/main/golang/main.go b/core/src/main/golang/main.go deleted file mode 100644 index b268848616..0000000000 --- a/core/src/main/golang/main.go +++ /dev/null @@ -1,83 +0,0 @@ -package main - -import ( - "github.com/Dreamacro/clash/component/mmdb" - "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" - "github.com/kr328/cfa/config" - "unsafe" -) - -/* -#cgo CFLAGS: -O3 -#cgo LDFLAGS: -llog -#include "buffer.h" -#include "malloc.h" - -extern void log_info(const char *msg); -extern void log_error(const char *msg); -extern void log_warn(const char *msg); -extern void log_debug(const char *msg); -extern void log_verbose(const char *msg); - */ -import "C" - -func main() { - panic("Only for linking") -} - -func init() { - r := make(chan struct{}) - - go func() { - sub := log.Subscribe() - defer log.UnSubscribe(sub) - - close(r) - - for item := range sub { - msg := item.(*log.Event) - - if msg.LogLevel < log.Level() { - continue - } - - cPayload := C.CString(msg.Payload) - - switch msg.LogLevel { - case log.INFO: - C.log_info(cPayload) - case log.ERROR: - C.log_error(cPayload) - case log.WARNING: - C.log_warn(cPayload) - case log.DEBUG: - C.log_debug(cPayload) - case log.SILENT: - C.log_verbose(cPayload) - } - - C.free(unsafe.Pointer(cPayload)) - } - }() - - <- r -} - -//export initialize -func initialize(database *C.const_buffer_t, home, version C.const_string_t) { - databaseData := C.GoBytes(database.buffer, database.length) - homeData := C.GoString(home) - versionData := C.GoString(version) - - mmdb.LoadFromBytes(databaseData) - constant.SetHomeDir(homeData) - config.ApplicationVersion = versionData -} - -//export reset -func reset() { - config.LoadDefault() - tunnel.DefaultManager.ResetStatistic() -} \ No newline at end of file diff --git a/core/src/main/golang/proxies.go b/core/src/main/golang/proxies.go deleted file mode 100644 index a6f95bcfa5..0000000000 --- a/core/src/main/golang/proxies.go +++ /dev/null @@ -1,198 +0,0 @@ -package main - -import ( - "github.com/Dreamacro/clash/adapters/outbound" - "github.com/Dreamacro/clash/adapters/outboundgroup" - "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/log" - "github.com/Dreamacro/clash/tunnel" - "unsafe" -) - -/* -#cgo CFLAGS: -Werror -#include "buffer.h" -#include "proxies.h" - */ -import "C" - -//export setSelector -func setSelector(group C.const_string_t, selected C.const_string_t) C.int { - g := C.GoString(group) - s := C.GoString(selected) - - p := tunnel.Proxies()[g] - if p == nil { - log.Warnln("Set selector failure: %s not found", g) - - return -1 - } - - pw, ok := p.(*outbound.Proxy) - if !ok { - log.Warnln("Set selector failure: %s not valid group", g) - - return -1 - } - - adapter, ok := pw.ProxyAdapter.(outboundgroup.ProxyGroup) - if !ok { - log.Warnln("Set selector failure: %s not valid group", g) - - return -1 - } - - selector, ok := adapter.(*outboundgroup.Selector) - if !ok { - log.Warnln("Set selector failure: %s not selector", g) - - return -1 - } - - if err := selector.Set(s); err != nil { - log.Warnln("Set selector failure: %s not in %s", s, g) - - return -1 - } - - log.Infoln("Set %s -> %s", g, s) - - return 0 -} - -//export queryProxyGroups -func queryProxyGroups() *C.proxy_group_list_t { - stringPool := make([]byte, 0, 4096) - proxies := tunnel.Proxies() - groups := make([]outboundgroup.ProxyGroup, 0, len(proxies)) - - for _, p := range proxies { - pw, ok := p.(*outbound.Proxy) - if !ok { - continue - } - - adapter, ok := pw.ProxyAdapter.(outboundgroup.ProxyGroup) - if !ok { - continue - } - - groups = append(groups, adapter) - } - - result := allocCProxyGroupList(len(groups)) - groupIndex := 0 - for _, group := range groups { - ps := make([]constant.Proxy, 0) - for _, provider := range group.GetProxyProviders() { - ps = append(ps, provider.Proxies()...) - } - - g := allocCProxyGroup(len(ps)) - - g.base.name_index = C.long(len(stringPool)) - g.base.proxy_type = typeToProxyTypeC(group.Type()) - g.base.delay = 0 - - stringPool = append(stringPool, group.Name()...) - stringPool = append(stringPool, 0) - - proxyIndex := 0 - for _, proxy := range ps { - p := indexCProxyGroupElement(g, proxyIndex) - - p.name_index = C.long(len(stringPool)) - p.proxy_type = typeToProxyTypeC(proxy.Type()) - p.delay = C.long(proxy.LastDelay()) - - stringPool = append(stringPool, proxy.Name()...) - stringPool = append(stringPool, 0) - - if proxy.Name() == group.Now() { - g.now = C.int(proxyIndex) - } - - proxyIndex++ - } - - setCProxyGroupListElement(result, groupIndex, g) - - groupIndex++ - } - - result.string_pool = (*C.char)(C.CBytes(stringPool)) - - return result -} - - - -func typeToProxyTypeC(t constant.AdapterType) C.proxy_type_t { - switch t { - case constant.Direct: - return C.Direct - case constant.Reject: - return C.Reject - - case constant.Shadowsocks: - return C.Shadowsocks - case constant.Snell: - return C.Snell - case constant.Socks5: - return C.Socks5 - case constant.Http: - return C.Http - case constant.Vmess: - return C.Vmess - case constant.Trojan: - return C.Trojan - - case constant.Relay: - return C.Relay - case constant.Selector: - return C.Selector - case constant.Fallback: - return C.Fallback - case constant.URLTest: - return C.URLTest - case constant.LoadBalance: - return C.LoadBalance - - default: - return C.Unknown - } -} - -func allocCProxyGroup(proxiesSize int) *C.proxy_group_t { - result := (*C.proxy_group_t)(C.malloc(C.sizeof_proxy_group_t + C.sizeof_proxy_t * C.size_t(proxiesSize))) - - result.proxies_size = C.int(proxiesSize) - - return result -} - -func allocCProxyGroupList(groupSize int) *C.proxy_group_list_t { - result := (*C.proxy_group_list_t)(C.malloc(C.sizeof_proxy_group_list_t + C.sizeof_long * C.size_t(groupSize))) - - result.size = C.int(groupSize) - - return result -} - -//noinspection GoVetUnsafePointer -func setCProxyGroupListElement(list *C.proxy_group_list_t, index int, element *C.proxy_group_t) { - address := uintptr(unsafe.Pointer(list)) - - offset := address + uintptr(C.sizeof_proxy_group_list_t) + uintptr(index) * uintptr(C.sizeof_long) - - *(**C.proxy_group_t)(unsafe.Pointer(offset)) = element -} - -//noinspection GoVetUnsafePointer -func indexCProxyGroupElement(group *C.proxy_group_t, index int) *C.proxy_t { - address := uintptr(unsafe.Pointer(group)) - - offset := address + uintptr(C.sizeof_proxy_group_t) + uintptr(index) * uintptr(C.sizeof_proxy_t) - - return (*C.proxy_t)(unsafe.Pointer(offset)) -} \ No newline at end of file diff --git a/core/src/main/golang/proxies.h b/core/src/main/golang/proxies.h deleted file mode 100644 index 53e41de9b6..0000000000 --- a/core/src/main/golang/proxies.h +++ /dev/null @@ -1,33 +0,0 @@ -#pragma once - -#if __cplusplus -extern "C" { -#endif - -typedef enum proxy_type_t { - Direct, Reject, Socks5, Http, Shadowsocks, Vmess, Snell, Trojan, Selector, Fallback, LoadBalance, URLTest, Relay, Unknown -} proxy_type_t; - -typedef struct proxy_t { - long name_index; - proxy_type_t proxy_type; - long delay; -} proxy_t; - -typedef struct proxy_group_t { - proxy_t base; - int now; - int proxies_size; - proxy_t proxies[]; -} proxy_group_t; - -typedef struct proxy_group_list_t { - int size; - char *string_pool; - proxy_group_t *groups[]; -} proxy_group_list_t; - -#if __cplusplus -}; -#endif - diff --git a/core/src/main/golang/traffic.go b/core/src/main/golang/traffic.go deleted file mode 100644 index 78e6c07c5e..0000000000 --- a/core/src/main/golang/traffic.go +++ /dev/null @@ -1,24 +0,0 @@ -package main - -/* -#include "traffic.h" - */ -import "C" -import "github.com/Dreamacro/clash/tunnel" - -//export querySpeed -func querySpeed(r *C.traffic_t) { - u, d := tunnel.DefaultManager.Now() - - r.upload = C.int64_t(u) - r.download = C.int64_t(d) -} - -//export queryBandwidth -func queryBandwidth(r *C.traffic_t) { - u := tunnel.DefaultManager.UploadTotal() - d := tunnel.DefaultManager.DownloadTotal() - - r.upload = C.int64_t(u) - r.download = C.int64_t(d) -} \ No newline at end of file diff --git a/core/src/main/golang/traffic.h b/core/src/main/golang/traffic.h deleted file mode 100644 index 6b3cf9b1aa..0000000000 --- a/core/src/main/golang/traffic.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include - -#if __cplusplus -extern "C" { -#endif - -typedef struct traffic_t { - int64_t upload; - int64_t download; -} traffic_t; - -#if __cplusplus -}; -#endif \ No newline at end of file diff --git a/core/src/main/golang/tun.go b/core/src/main/golang/tun.go deleted file mode 100644 index b6dda4cc2f..0000000000 --- a/core/src/main/golang/tun.go +++ /dev/null @@ -1,62 +0,0 @@ -package main - -import ( - "github.com/Dreamacro/clash/component/dialer" - "github.com/kr328/cfa/tun" - "net" - "strconv" - "sync" - "syscall" -) - -//#include "buffer.h" -//#include "event.h" -import "C" - -var tunLock sync.Mutex - -func init() { - c := func(_, _ string, conn syscall.RawConn) error { - return conn.Control(func(fd uintptr) { - <- sendEvent(C.NEW_SOCKET, 0, strconv.Itoa(int(fd))).result - }) - } - - dialer.DialerHook = func(d *net.Dialer) error { - d.Control = c - return nil - } - dialer.ListenConfigHook = func(l *net.ListenConfig) error { - l.Control = c - return nil - } -} - -//export startTunDevice -func startTunDevice(fd, mtu int, gateway, mirror, dns C.const_string_t, callbackId uint64) *C.char { - stopTunDevice() - - tunLock.Lock() - defer tunLock.Unlock() - - g := C.GoString(gateway) - m := C.GoString(mirror) - d := C.GoString(dns) - - err := tun.StartTunDevice(fd, mtu, g, m, d, func() { - sendEvent(C.TUN_STOP, callbackId, "") - }) - if err != nil { - return C.CString(err.Error()) - } - - return nil -} - -//export stopTunDevice -func stopTunDevice() { - tunLock.Lock() - defer tunLock.Unlock() - - tun.StopTunDevice() -} \ No newline at end of file diff --git a/core/src/main/golang/tun/dns.go b/core/src/main/golang/tun/dns.go deleted file mode 100644 index ab450c7131..0000000000 --- a/core/src/main/golang/tun/dns.go +++ /dev/null @@ -1,252 +0,0 @@ -package tun - -import ( - "encoding/binary" - "github.com/Dreamacro/clash/component/resolver" - "github.com/Dreamacro/clash/dns" - "github.com/kr328/cfa/utils" - "github.com/kr328/tun2socket/binding" - "github.com/kr328/tun2socket/redirect" - D "github.com/miekg/dns" - "io" - "net" - "sync" - "time" -) - -const ( - defaultDNSReadTimeout = time.Second * 10 -) - -var lock sync.Mutex -var hijackAddress net.IP -var dnsHandler dns.Handler - -func setHijackAddress(hijackAddr net.IP) { - lock.Lock() - defer lock.Unlock() - - hijackAddress = hijackAddr -} - -func InitialResolver() { - lock.Lock() - defer lock.Unlock() - - rawResolver := resolver.DefaultResolver - if rawResolver == nil { - dnsHandler = nil - return - } - r, ok := rawResolver.(*dns.Resolver) - if !ok || r == nil { - dnsHandler = nil - return - } - - dnsHandler = dns.NewHandler(r) -} - -func hijackTCPDNS(conn net.Conn, endpoint *binding.Endpoint) bool { - if endpoint.Target.Port != 53 { - return false - } - - if dnsHandler == nil { - return false - } - - if !hijackAddress.Equal(net.IPv4zero) && !hijackAddress.Equal(endpoint.Target.IP) { - return false - } - - go func() { - defer utils.CloseSilent(conn) - - for { - if err := conn.SetReadDeadline(time.Now().Add(defaultDNSReadTimeout)); err != nil { - return - } - - var length uint16 - if err := binary.Read(conn, binary.BigEndian, &length); err != nil { - return - } - - data := make([]byte, length) - msg := &D.Msg{} - - _, err := io.ReadFull(conn, data) - if err != nil { - return - } - - if err := msg.Unpack(data); err != nil || len(msg.Question) == 0 { - return - } - - handler := dnsHandler - if handler == nil { - return - } - - handler(&tcpWriter{ - conn: conn, - endpoint: endpoint, - }, msg) - } - }() - - return true -} - -func hijackDNS(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSender, recycle func([]byte)) bool { - if endpoint.Target.Port != 53 { - return false - } - - if dnsHandler == nil { - return false - } - - if !hijackAddress.Equal(net.IPv4zero) && !hijackAddress.Equal(endpoint.Target.IP) { - return false - } - - go func() { - msg := &D.Msg{} - if err := msg.Unpack(payload); err != nil { - return - } - - handler := dnsHandler - handler(&udpWriter{ - endpoint: endpoint, - sender: sender, - }, msg) - - recycle(payload) - }() - - return true -} - -type tcpWriter struct { - conn net.Conn - endpoint *binding.Endpoint -} - -func (r *tcpWriter) LocalAddr() net.Addr { - return &net.TCPAddr{ - IP: r.endpoint.Target.IP, - Port: int(r.endpoint.Target.Port), - Zone: "", - } -} - -func (r *tcpWriter) RemoteAddr() net.Addr { - return &net.TCPAddr{ - IP: r.endpoint.Source.IP, - Port: int(r.endpoint.Source.Port), - Zone: "", - } -} - -func (r *tcpWriter) Write(b []byte) (int, error) { - if len(b) > 65535 { - return 0, io.ErrShortBuffer - } - - var length [2]byte - binary.BigEndian.PutUint16(length[:], uint16(len(b))) - - n, err := (&net.Buffers{length[:], b}).WriteTo(r.conn) - - return int(n), err -} - -func (r *tcpWriter) Close() error { - return nil -} - -func (r *tcpWriter) WriteMsg(d *D.Msg) error { - msg, err := d.Pack() - if err != nil { - return err - } - - _, err = r.Write(msg) - - return err -} - -func (r *tcpWriter) TsigStatus() error { - // Unsupported - return nil -} - -func (r *tcpWriter) TsigTimersOnly(bool) { - // Unsupported -} - -func (r *tcpWriter) Hijack() { - // Unsupported -} - -type udpWriter struct { - endpoint *binding.Endpoint - sender redirect.UDPSender -} - -func (r *udpWriter) LocalAddr() net.Addr { - return &net.UDPAddr{ - IP: r.endpoint.Target.IP, - Port: int(r.endpoint.Target.Port), - Zone: "", - } -} - -func (r *udpWriter) RemoteAddr() net.Addr { - return &net.UDPAddr{ - IP: r.endpoint.Source.IP, - Port: int(r.endpoint.Source.Port), - Zone: "", - } -} - -func (r *udpWriter) WriteMsg(d *D.Msg) error { - msg, err := d.Pack() - if err != nil { - return err - } - - _, err = r.Write(msg) - - return err -} - -func (r *udpWriter) Write(msg []byte) (int, error) { - ep := &binding.Endpoint{ - Source: r.endpoint.Target, - Target: r.endpoint.Source, - } - - return len(msg), r.sender(msg, ep) -} - -func (r *udpWriter) Close() error { - return nil -} - -func (r *udpWriter) TsigStatus() error { - // Unsupported - return nil -} - -func (r *udpWriter) TsigTimersOnly(bool) { - // Unsupported -} - -func (r *udpWriter) Hijack() { - // Unsupported -} diff --git a/core/src/main/golang/tun/log.go b/core/src/main/golang/tun/log.go deleted file mode 100644 index 7eafb1bcd1..0000000000 --- a/core/src/main/golang/tun/log.go +++ /dev/null @@ -1,21 +0,0 @@ -package tun - -import "github.com/Dreamacro/clash/log" - -type ClashLogger struct{} - -func (c *ClashLogger) D(format string, args ...interface{}) { - log.Debugln(format, args...) -} - -func (c *ClashLogger) I(format string, args ...interface{}) { - log.Infoln(format, args...) -} - -func (c *ClashLogger) W(format string, args ...interface{}) { - log.Warnln(format, args...) -} - -func (c *ClashLogger) E(format string, args ...interface{}) { - log.Errorln(format, args...) -} diff --git a/core/src/main/golang/tun/tun.go b/core/src/main/golang/tun/tun.go deleted file mode 100644 index 89167c938e..0000000000 --- a/core/src/main/golang/tun/tun.go +++ /dev/null @@ -1,135 +0,0 @@ -package tun - -import ( - "errors" - adapters "github.com/Dreamacro/clash/adapters/inbound" - "github.com/Dreamacro/clash/component/socks5" - C "github.com/Dreamacro/clash/constant" - "github.com/Dreamacro/clash/tunnel" - "github.com/kr328/tun2socket" - "github.com/kr328/tun2socket/binding" - "github.com/kr328/tun2socket/redirect" - "net" - "os" - "sync" - "syscall" - - "github.com/Dreamacro/clash/log" -) - -const ( - maxUdpPacketSize = 65535 -) - -var adapter *tun2socket.Tun2Socket -var mutex sync.Mutex - -func StartTunDevice(fd, mtu int, gateway, mirror, dnsAddress string, onStop func()) error { - mutex.Lock() - defer mutex.Unlock() - - if adapter != nil { - adapter.Close() - adapter = nil - - log.Infoln("Android tun stopped") - } - - gatewayIP, gatewayNet, err := net.ParseCIDR(gateway) - _, ipv4Loopback, _ := net.ParseCIDR("127.0.0.0/8") - mirrorIP := net.ParseIP(mirror) - - if err != nil || mirrorIP == nil || !gatewayNet.Contains(mirrorIP) { - return errors.New("invalid gateway or mirror") - } - - udpPool := sync.Pool{New: func() interface{} { - return make([]byte, maxUdpPacketSize) - }} - udpRecycle := func(bytes []byte) { - if cap(bytes) == maxUdpPacketSize { - udpPool.Put(bytes[:maxUdpPacketSize]) - } - } - - file := os.NewFile(uintptr(fd), "/dev/tun") - _ = syscall.SetNonblock(fd, true) - - adapter = tun2socket.NewTun2Socket(file, mtu, gatewayIP, mirrorIP.To4()) - - adapter.SetLogger(&ClashLogger{}) - adapter.SetClosedHandler(func() { - StopTunDevice() - - onStop() - }) - adapter.SetAllocator(func(length int) []byte { - if length <= maxUdpPacketSize { - return udpPool.Get().([]byte)[:length] - } - return make([]byte, length) - }) - adapter.SetTCPHandler(func(conn net.Conn, endpoint *binding.Endpoint) { - if gatewayNet.Contains(endpoint.Target.IP) || ipv4Loopback.Contains(endpoint.Target.IP) { - _ = conn.Close() - return - } - - if hijackTCPDNS(conn, endpoint) { - return - } - - addr := socks5.ParseAddrToSocksAddr(&net.TCPAddr{ - IP: endpoint.Target.IP, - Port: int(endpoint.Target.Port), - Zone: "", - }) - - tunnel.Add(adapters.NewSocket(addr, conn, C.SOCKS)) - }) - adapter.SetUDPHandler(func(payload []byte, endpoint *binding.Endpoint, sender redirect.UDPSender) { - if gatewayNet.Contains(endpoint.Target.IP) || ipv4Loopback.Contains(endpoint.Target.IP) { - udpRecycle(payload) - return - } - - if hijackDNS(payload, endpoint, sender, udpRecycle) { - return - } - - addr := socks5.ParseAddrToSocksAddr(&net.TCPAddr{ - IP: endpoint.Target.IP, - Port: int(endpoint.Target.Port), - Zone: "", - }) - pkt := &udpPacket{ - payload: payload, - endpoint: endpoint, - send: sender, - recycle: udpRecycle, - } - - tunnel.AddPacket(adapters.NewPacket(addr, pkt, C.SOCKS)) - }) - - setHijackAddress(net.ParseIP(dnsAddress)) - InitialResolver() - - adapter.Start() - - log.Infoln("Android tun started") - - return nil -} - -func StopTunDevice() { - mutex.Lock() - defer mutex.Unlock() - - if adapter != nil { - adapter.Close() - adapter = nil - - log.Infoln("Android tun stopped") - } -} diff --git a/core/src/main/golang/tun/udp.go b/core/src/main/golang/tun/udp.go deleted file mode 100644 index 0708d87094..0000000000 --- a/core/src/main/golang/tun/udp.go +++ /dev/null @@ -1,56 +0,0 @@ -package tun - -import ( - "errors" - "github.com/kr328/tun2socket/binding" - "github.com/kr328/tun2socket/redirect" - "net" -) - -type udpPacket struct { - payload []byte - endpoint *binding.Endpoint - send redirect.UDPSender - recycle func([]byte) -} - -func (conn *udpPacket) Data() []byte { - return conn.payload -} - -func (conn *udpPacket) WriteBack(b []byte, addr net.Addr) (n int, err error) { - if addr == nil { - addr = &net.UDPAddr{ - IP: conn.endpoint.Target.IP, - Port: int(conn.endpoint.Target.Port), - Zone: "", - } - } - - udpAddr, ok := addr.(*net.UDPAddr) - if !ok { - return 0, errors.New("Invalid udp address") - } - - ep := &binding.Endpoint{ - Source: binding.Address{ - IP: udpAddr.IP, - Port: uint16(udpAddr.Port), - }, - Target: conn.endpoint.Source, - } - - return len(b), conn.send(b, ep) -} - -func (conn *udpPacket) LocalAddr() net.Addr { - return &net.UDPAddr{ - IP: conn.endpoint.Source.IP, - Port: int(conn.endpoint.Source.Port), - Zone: "", - } -} - -func (conn *udpPacket) Drop() { - conn.recycle(conn.payload) -} diff --git a/core/src/main/golang/utils/close.go b/core/src/main/golang/utils/close.go deleted file mode 100644 index 1843773535..0000000000 --- a/core/src/main/golang/utils/close.go +++ /dev/null @@ -1,8 +0,0 @@ -package utils - -import "io" - -func CloseSilent(closer io.Closer) { - _ = closer.Close() -} - diff --git a/core/src/main/java/com/github/kr328/clash/core/Clash.kt b/core/src/main/java/com/github/kr328/clash/core/Clash.kt deleted file mode 100644 index 4e3a0ac2d1..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/Clash.kt +++ /dev/null @@ -1,127 +0,0 @@ -package com.github.kr328.clash.core - -import android.os.ParcelFileDescriptor -import com.github.kr328.clash.common.Global -import com.github.kr328.clash.common.utils.Log -import com.github.kr328.clash.core.bridge.Bridge -import com.github.kr328.clash.core.bridge.TunCallback -import com.github.kr328.clash.core.event.LogEvent -import com.github.kr328.clash.core.model.General -import com.github.kr328.clash.core.model.ProxyGroup -import com.github.kr328.clash.core.model.Traffic -import java.io.File -import java.io.InputStream -import java.util.concurrent.CompletableFuture - -object Clash { - private val logReceivers = mutableMapOf Unit>() - - init { - val context = Global.application - - val bytes = context.assets.open("Country.mmdb") - .use(InputStream::readBytes) - - Bridge.initialize(bytes, context.cacheDir.absolutePath, BuildConfig.VERSION_NAME) - Bridge.reset() - - Bridge.setLogCallback { - synchronized(logReceivers) { - logReceivers.forEach { (_, e) -> e(it) } - } - } - - Log.i("Clash core initialized") - } - - fun start() { - Bridge.reset() - } - - fun stop() { - Bridge.reset() - } - - fun startTunDevice( - fd: Int, - mtu: Int, - gateway: String, - mirror: String, - dns: String, - onNewSocket: (Int) -> Boolean, - onTunStop: () -> Unit - ) { - Bridge.startTunDevice(fd, mtu, gateway, mirror, dns, object: TunCallback { - override fun onNewSocket(socket: Int) { - onNewSocket(socket) - } - override fun onStop() { - onTunStop() - } - }) - } - - fun stopTunDevice() { - Bridge.stopTunDevice() - } - - fun setDnsOverride(dnsOverride: Boolean, appendNameservers: List) { - Bridge.setDnsOverride(dnsOverride, appendNameservers.joinToString(",")) - } - - fun loadProfile(path: File, baseDir: File): CompletableFuture { - return Bridge.loadProfile(path.absolutePath, baseDir.absolutePath).thenApply { Unit } - } - - fun downloadProfile(url: String, output: File, baseDir: File): CompletableFuture { - return Bridge.downloadProfile(url, baseDir.absolutePath, output.absolutePath).thenApply { Unit } - } - - fun downloadProfile(fd: ParcelFileDescriptor, output: File, baseDir: File): CompletableFuture { - return Bridge.downloadProfile(fd.detachFd(), baseDir.absolutePath, output.absolutePath).thenApply { Unit } - } - - fun queryProxyGroups(): List { - return Bridge.queryProxyGroups().toList() - } - - fun setSelector(name: String, selected: String): Boolean { - return Bridge.setSelector(name, selected) - } - - fun performHealthCheck(group: String): CompletableFuture { - return Bridge.performHealthCheck(group).thenApply { Unit } - } - - fun setProxyMode(mode: String) { - Bridge.setProxyMode(mode) - } - - fun queryGeneral(): General { - return Bridge.queryGeneral() - } - - fun querySpeed(): Traffic { - return Bridge.querySpeed() - } - - fun queryBandwidth(): Traffic { - return Bridge.queryBandwidth() - } - - fun registerLogReceiver(key: String, receiver: (LogEvent) -> Unit) { - synchronized(logReceivers) { - if ( logReceivers.isEmpty() ) - Bridge.enableLogReport() - logReceivers[key] = receiver - } - } - - fun unregisterLogReceiver(key: String) { - synchronized(logReceivers) { - logReceivers.remove(key) - if ( logReceivers.isEmpty() ) - Bridge.disableLogReport(); - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.java b/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.java deleted file mode 100644 index f19617f82b..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/bridge/Bridge.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.kr328.clash.core.bridge; - -import androidx.annotation.Keep; - -import com.github.kr328.clash.core.model.General; -import com.github.kr328.clash.core.model.ProxyGroup; -import com.github.kr328.clash.core.model.Traffic; - -import java.util.concurrent.CompletableFuture; - -@Keep -public final class Bridge { - static { - System.loadLibrary("bridge"); - } - - private Bridge() { - } - - public static native void initialize(byte[] database, String home, String version); - public static native void reset(); - - public static native General queryGeneral(); - public static native Traffic querySpeed(); - public static native Traffic queryBandwidth(); - public static native ProxyGroup[] queryProxyGroups(); - - public static native void startTunDevice(int fd, int mtu, String gateway, String mirror, String dns, TunCallback callback) throws ClashException; - public static native void stopTunDevice(); - - public static native void setDnsOverride(boolean overrideDns, String appendNameservers); - public static native void setProxyMode(String mode); - public static native boolean setSelector(String group, String selected); - - public static native CompletableFuture downloadProfile(String url, String base, String output); - public static native CompletableFuture downloadProfile(int fd, String base, String output); - - public static native CompletableFuture loadProfile(String path, String base); - public static native CompletableFuture performHealthCheck(String group); - - public static native void setLogCallback(LogCallback callback); - public static native void enableLogReport(); - public static native void disableLogReport(); -} diff --git a/core/src/main/java/com/github/kr328/clash/core/bridge/ClashException.java b/core/src/main/java/com/github/kr328/clash/core/bridge/ClashException.java deleted file mode 100644 index 334e357da9..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/bridge/ClashException.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.kr328.clash.core.bridge; - -import androidx.annotation.Keep; - -public class ClashException extends Exception { - @Keep - public ClashException(String msg) { - super(msg); - } -} diff --git a/core/src/main/java/com/github/kr328/clash/core/bridge/LogCallback.java b/core/src/main/java/com/github/kr328/clash/core/bridge/LogCallback.java deleted file mode 100644 index d612835f37..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/bridge/LogCallback.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.kr328.clash.core.bridge; - -import androidx.annotation.Keep; - -import com.github.kr328.clash.core.event.LogEvent; - -@Keep -@SuppressWarnings("unused") -public interface LogCallback { - void onMessage(LogEvent event); -} diff --git a/core/src/main/java/com/github/kr328/clash/core/bridge/TunCallback.java b/core/src/main/java/com/github/kr328/clash/core/bridge/TunCallback.java deleted file mode 100644 index 09ee5ad411..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/bridge/TunCallback.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.kr328.clash.core.bridge; - -import androidx.annotation.Keep; - -@Keep -@SuppressWarnings("unused") -public interface TunCallback { - void onNewSocket(int socket); - void onStop(); -} diff --git a/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt b/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt deleted file mode 100644 index 5e78c762c9..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/event/LogEvent.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.github.kr328.clash.core.event - -import android.os.Parcel -import android.os.Parcelable -import androidx.annotation.Keep -import com.github.kr328.clash.common.serialization.Parcels -import kotlinx.serialization.Serializable - -@Serializable -@Suppress("UNUSED") -data class LogEvent( - val level: Level, - val message: String, - val time: Long = System.currentTimeMillis() -) : Parcelable { - private constructor(data: List): this(Level.fromString(data[0]), data[1]) - @Keep - constructor(data: String) : this(data.split(":", limit = 2)) - - companion object { - const val DEBUG_VALUE = "debug" - const val INFO_VALUE = "info" - const val WARN_VALUE = "warning" - const val ERROR_VALUE = "error" - - @JvmField - val CREATOR = object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): LogEvent { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - } - - enum class Level { - DEBUG, - INFO, - WARN, - ERROR, - UNKNOWN; - - companion object { - fun fromString(type: String): Level { - return when (type) { - DEBUG_VALUE -> DEBUG - INFO_VALUE -> INFO - WARN_VALUE -> WARN - ERROR_VALUE -> ERROR - else -> UNKNOWN - } - } - } - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/General.kt b/core/src/main/java/com/github/kr328/clash/core/model/General.kt deleted file mode 100644 index a48a4b8182..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/General.kt +++ /dev/null @@ -1,55 +0,0 @@ -package com.github.kr328.clash.core.model - -import android.os.Parcel -import android.os.Parcelable -import androidx.annotation.Keep -import com.github.kr328.clash.common.serialization.Parcels -import kotlinx.serialization.Serializable - -@Serializable -data class General(val mode: Mode, val http: Int, val socks: Int, val redirect: Int, val mixed: Int) : Parcelable { - @Keep - constructor(mode: String, http: Int, socks: Int, redirect: Int, mixed: Int) : - this(Mode.fromString(mode), http, socks, redirect, mixed) - - @Serializable - enum class Mode(val string: String) { - DIRECT("Direct"), GLOBAL("Global"), RULE("Rule"); - - override fun toString(): String { - return string - } - - companion object { - fun fromString(mode: String): Mode { - return when (mode) { - DIRECT.string -> DIRECT - GLOBAL.string -> GLOBAL - RULE.string -> RULE - else -> throw IllegalArgumentException("Invalid mode $mode") - } - } - } - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } - - companion object { - @JvmField - val CREATOR = object : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): General { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt b/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt deleted file mode 100644 index bb5f32ffd5..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/Proxy.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.github.kr328.clash.core.model - -import androidx.annotation.Keep -import kotlinx.serialization.Serializable - -@Serializable -data class Proxy constructor( - val name: String, - val type: Type, - val delay: Long -) { - @Keep - constructor(name: String, type: String, delay: Long): this(name, Type.fromString(type), delay) - - enum class Type(val text: String, val group: Boolean) { - DIRECT("Direct", false), - REJECT("Reject", false), - - SHADOWSOCKS("Shadowsocks", false), - SNELL("Snell", false), - SOCKS5("Socks5", false), - HTTP("Http", false), - VMESS("Vmess", false), - TROJAN("Trojan", false), - - RELAY("Relay", true), - SELECT("Selector", true), - FALLBACK("Fallback", true), - URL_TEST("URLTest", true), - LOAD_BALANCE("LoadBalance", true), - - UNKNOWN("Unknown", false); - - override fun toString(): String { - return text - } - - companion object { - fun fromString(type: String): Type { - return when (type) { - DIRECT.text -> DIRECT - REJECT.text -> REJECT - SHADOWSOCKS.text -> SHADOWSOCKS - SNELL.text -> SNELL - SOCKS5.text -> SOCKS5 - HTTP.text -> HTTP - VMESS.text -> VMESS - TROJAN.text -> TROJAN - RELAY.text -> RELAY - SELECT.text -> SELECT - FALLBACK.text -> FALLBACK - URL_TEST.text -> URL_TEST - LOAD_BALANCE.text -> LOAD_BALANCE - UNKNOWN.text -> UNKNOWN - else -> UNKNOWN - } - } - } - } -} diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt deleted file mode 100644 index 7fd5685c4f..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroup.kt +++ /dev/null @@ -1,16 +0,0 @@ -package com.github.kr328.clash.core.model - -import androidx.annotation.Keep -import kotlinx.serialization.Serializable - -@Serializable -data class ProxyGroup( - val name: String, - val type: Proxy.Type, - val current: String, - val proxies: List -) { - @Keep - constructor(name: String, type: String, current: String, proxies: Array) : - this(name, Proxy.Type.fromString(type), current, proxies.toList()) -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupWrapper.kt b/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupWrapper.kt deleted file mode 100644 index 121e83a6a7..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/ProxyGroupWrapper.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.kr328.clash.core.model - -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.common.serialization.MergedParcels -import kotlinx.serialization.Serializable - -@Serializable -data class ProxyGroupWrapper(val list: List) : Parcelable { - override fun writeToParcel(parcel: Parcel, flags: Int) { - MergedParcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ProxyGroupWrapper { - return MergedParcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file diff --git a/core/src/main/java/com/github/kr328/clash/core/model/Traffic.kt b/core/src/main/java/com/github/kr328/clash/core/model/Traffic.kt deleted file mode 100644 index a34b654472..0000000000 --- a/core/src/main/java/com/github/kr328/clash/core/model/Traffic.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.kr328.clash.core.model - -import androidx.annotation.Keep - -data class Traffic @Keep constructor(val upload: Long, val download: Long) \ No newline at end of file diff --git a/design/.gitignore b/design/.gitignore deleted file mode 100644 index 796b96d1c4..0000000000 --- a/design/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/design/build.gradle.kts b/design/build.gradle.kts deleted file mode 100644 index 2720d7c045..0000000000 --- a/design/build.gradle.kts +++ /dev/null @@ -1,58 +0,0 @@ -plugins { - id("com.android.library") - id("kotlin-android") - id("kotlin-android-extensions") -} - -val gCompileSdkVersion: String by project -val gBuildToolsVersion: String by project - -val gMinSdkVersion: String by project -val gTargetSdkVersion: String by project - -val gVersionCode: String by project -val gVersionName: String by project - -val gKotlinVersion: String by project -val gAndroidKtxVersion: String by project -val gAppCompatVersion: String by project -val gMaterialDesignVersion: String by project - -android { - compileSdkVersion(gCompileSdkVersion) - buildToolsVersion(gBuildToolsVersion) - - defaultConfig { - minSdkVersion(gMinSdkVersion) - targetSdkVersion(gTargetSdkVersion) - - versionCode = gVersionCode.toInt() - versionName = gVersionName - - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - named("release") { - isMinifyEnabled = false - proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" - } -} - -dependencies { - implementation(project(":common")) - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion") - implementation("androidx.appcompat:appcompat:$gAppCompatVersion") - implementation("androidx.core:core-ktx:$gAndroidKtxVersion") - implementation("com.google.android.material:material:$gMaterialDesignVersion") -} diff --git a/design/consumer-rules.pro b/design/consumer-rules.pro deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/design/proguard-rules.pro b/design/proguard-rules.pro deleted file mode 100644 index f1b424510d..0000000000 --- a/design/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/design/src/main/AndroidManifest.xml b/design/src/main/AndroidManifest.xml deleted file mode 100644 index c6fb966459..0000000000 --- a/design/src/main/AndroidManifest.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/design/src/main/java/com/github/kr328/clash/design/common/Base.kt b/design/src/main/java/com/github/kr328/clash/design/common/Base.kt deleted file mode 100644 index 8242b1b489..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/common/Base.kt +++ /dev/null @@ -1,43 +0,0 @@ -package com.github.kr328.clash.design.common - -import android.content.Context -import android.os.Bundle -import android.view.View - -@Suppress("MemberVisibilityCanBePrivate") -abstract class Base(val screen: CommonUiScreen) { - val context: Context - get() = screen.layout.context - - var id: String? = null - - var dependOn: Base? = null - - var isEnabled: Boolean = true - get() = field && (dependOn?.isEnabled ?: true) - set(value) { - field = value - screen.postReapplyAttribute() - } - var isHidden: Boolean = false - get() = field || (dependOn?.isHidden ?: false) - set(value) { - field = value - - reapplyAttribute() - - screen.postReapplyAttribute() - } - - fun reapplyAttribute() { - applyAttribute(isEnabled, isHidden) - } - - abstract val view: View - abstract fun saveState(bundle: Bundle) - 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 - } -} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/Category.kt b/design/src/main/java/com/github/kr328/clash/design/common/Category.kt deleted file mode 100644 index 184da8892f..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/common/Category.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.kr328.clash.design.common - -import android.os.Bundle -import android.view.LayoutInflater -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) - - private val vText: TextView = view.findViewById(R.id.text) - private val vTopSeparator: View = view.findViewById(R.id.topSeparator) - private val vBottomSeparator: View = view.findViewById(R.id.bottomSeparator) - - var text: CharSequence - get() = vText.text - set(value) { - vText.text = value - } - - var showTopSeparator: Boolean - get() = vTopSeparator.visibility == View.VISIBLE - set(value) { - vTopSeparator.visibility = - 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 - } - - override fun saveState(bundle: Bundle) {} - override fun restoreState(bundle: Bundle) {} -} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt b/design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt deleted file mode 100644 index 0b2149fd72..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/common/CommonUiBuilder.kt +++ /dev/null @@ -1,87 +0,0 @@ -package com.github.kr328.clash.design.common - -import android.content.Context -import android.graphics.drawable.Drawable - -class CommonUiBuilder(val screen: CommonUiScreen) { - val context: Context - get() = screen.layout.context - - fun textInput( - title: String = "", - hint: CharSequence = "", - icon: Drawable? = null, - content: String = "", - id: String? = null, - dependOn: String? = null, - setup: TextInput.() -> Unit = {} - ) { - val textInput = TextInput(screen) - - textInput.title = title - textInput.hint = hint - textInput.icon = icon - textInput.content = content - textInput.id = id - textInput.dependOn = dependOn?.run { screen.requireElement(this) } - - screen.addElement(textInput) - - textInput.setup() - } - - fun option( - title: String = "", - summary: String = "", - icon: Drawable? = null, - id: String? = null, - dependOn: String? = null, - setup: Option.() -> Unit = {} - ) { - val option = Option(screen) - - option.title = title - option.summary = summary - option.icon = icon - option.id = id - option.dependOn = dependOn?.run { screen.requireElement(this) } - - setup(option) - - screen.addElement(option) - } - - fun category( - text: String = "", - showTopSeparator: Boolean = false, - showBottomSeparator: Boolean = false, - id: String? = null, - setup: Category.() -> Unit = {} - ) { - val category = Category(screen) - - category.text = text - category.showTopSeparator = showTopSeparator - category.showBottomSeparator = showBottomSeparator - category.id = id - - setup(category) - - screen.addElement(category) - } - - fun tips( - title: String = "", - icon: Drawable? = null, - setup: Tips.() -> Unit = {} - ) { - val tips = Tips(screen) - - tips.title = title - tips.icon = icon - - setup(tips) - - screen.addElement(tips) - } -} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/CommonUiScreen.kt b/design/src/main/java/com/github/kr328/clash/design/common/CommonUiScreen.kt deleted file mode 100644 index ba11291acc..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/common/CommonUiScreen.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.github.kr328.clash.design.common - -import android.os.Bundle -import android.os.Handler -import com.github.kr328.clash.design.view.CommonUiLayout - -class CommonUiScreen(val layout: CommonUiLayout) { - private val handler = Handler() - val elements = mutableListOf() - - fun clear() { - elements.clear() - layout.removeAllViews() - } - - inline fun getElement(id: String): T? { - return elements.singleOrNull { it.id == id } as T? - } - - inline fun requireElement(id: String): T { - return requireNotNull(getElement(id)) - } - - fun addElement(element: Base) { - layout.addView(element.view) - elements.add(element) - } - - fun postReapplyAttribute() { - handler.post { - elements.forEach { - it.reapplyAttribute() - } - } - } - - fun saveState(bundle: Bundle) { - elements.forEach { - it.saveState(bundle) - } - } - - fun restoreState(bundle: Bundle?) { - if (bundle == null) - return - - elements.forEach { - it.restoreState(bundle) - } - } -} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/Option.kt b/design/src/main/java/com/github/kr328/clash/design/common/Option.kt deleted file mode 100644 index 735c88dcfe..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/common/Option.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.github.kr328.clash.design.common - -import android.graphics.drawable.Drawable -import android.os.Bundle -import android.view.LayoutInflater -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) - - private val vIcon: View = view.findViewById(android.R.id.icon) - private val vTitle: TextView = view.findViewById(android.R.id.title) - private val vSummary: TextView = view.findViewById(android.R.id.summary) - private val vPadding: View = view.findViewById(R.id.heightPadding) - - private var click: () -> Unit = {} - - var icon: Drawable? - get() = vIcon.background - set(value) { - vIcon.background = value - } - var title: CharSequence - get() = vTitle.text - set(value) { - vTitle.text = value - } - var summary: CharSequence - get() = vSummary.text - set(value) { - vSummary.text = value - if (value.isEmpty()) - vSummary.visibility = View.GONE - else - vSummary.visibility = View.VISIBLE - } - var textColor: Int - get() = vTitle.textColors.defaultColor - set(value) { - vTitle.setTextColor(value) - vSummary.setTextColor(value) - } - var paddingHeight: Boolean - get() = vPadding.visibility != View.GONE - set(value) { - if (value) - vPadding.visibility = View.INVISIBLE - else - vPadding.visibility = View.GONE - } - - init { - view.setOnClickListener { - click() - } - } - - fun onClick(block: () -> Unit) { - this.click = block - } - - override fun saveState(bundle: Bundle) {} - override fun restoreState(bundle: Bundle) {} -} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt b/design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt deleted file mode 100644 index f5fb84581d..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/common/TextInput.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.github.kr328.clash.design.common - -import android.graphics.drawable.Drawable -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.widget.EditText -import android.widget.TextView -import androidx.appcompat.app.AlertDialog -import com.github.kr328.clash.design.R - -class TextInput(screen: CommonUiScreen) : Base(screen) { - override val view: View = LayoutInflater.from(context).inflate( - R.layout.view_setting_text_input, - screen.layout, - false - ) - - private val vIcon: View = view.findViewById(android.R.id.icon) - private val vTitle: TextView = view.findViewById(android.R.id.title) - private val vContent: View = view.findViewById(android.R.id.content) - private val vText: TextView = view.findViewById(android.R.id.text1) - - var icon: Drawable? - get() = vIcon.background - set(value) { - vIcon.background = value - } - var title: String - get() = vTitle.text?.toString() ?: "" - set(value) { - vTitle.text = value - } - var content: CharSequence = "" - set(value) { - vText.text = displayContent(value) - field = value - - textChanged.apply { - textChanged = {} - this(value) - textChanged = this - } - } - var hint: CharSequence - get() = vText.hint ?: "" - set(value) { - vText.hint = value - } - - private var openInput: () -> Unit = this::openDialogInput - private var textChanged: (CharSequence) -> Unit = {} - private var displayContent: (CharSequence) -> CharSequence = { it } - - init { - vContent.setOnClickListener { - openInput() - } - } - - fun onOpenInput(block: () -> Unit) { - this.openInput = block - } - - fun onTextChanged(block: (CharSequence) -> Unit) { - this.textChanged = block - } - - fun onDisplayContent(block: (CharSequence) -> CharSequence) { - this.displayContent = block - - // Apply display content transform - content = content - } - - override fun saveState(bundle: Bundle) { - if (id == null) - return - - bundle.putCharSequence(id, content) - } - - override fun restoreState(bundle: Bundle) { - if (id == null) - return - - bundle.getCharSequence(id)?.apply { - content = this - } - } - - fun openDialogInput() { - val v = LayoutInflater.from(context) - .inflate(R.layout.dialog_input_text, screen.layout, false) - val c: EditText = v.findViewById(android.R.id.text1) - - c.setText(content) - c.hint = hint - - AlertDialog.Builder(context) - .setTitle(title) - .setView(v) - .setPositiveButton(R.string.ok) { _, _ -> content = c.text?.toString() ?: "" } - .setNegativeButton(R.string.cancel) { _, _ -> } - .show() - } -} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/common/Tips.kt b/design/src/main/java/com/github/kr328/clash/design/common/Tips.kt deleted file mode 100644 index 14722613b0..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/common/Tips.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.kr328.clash.design.common - -import android.graphics.drawable.Drawable -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.widget.TextView -import com.github.kr328.clash.design.R - -class Tips(screen: CommonUiScreen) : Base(screen) { - override val view: View = LayoutInflater.from(context) - .inflate(R.layout.view_setting_tip, screen.layout, false) - - private val vIcon: View = view.findViewById(android.R.id.icon) - private val vTitle: TextView = view.findViewById(android.R.id.title) - - var icon: Drawable? - get() = vIcon.background - set(value) { - vIcon.background = value - } - - var title: CharSequence - get() = vTitle.text - set(value) { - vTitle.text = value - } - - override fun saveState(bundle: Bundle) {} - override fun restoreState(bundle: Bundle) {} -} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt b/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt deleted file mode 100644 index 4a90874b3a..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/view/ColorfulTextCard.kt +++ /dev/null @@ -1,58 +0,0 @@ -package com.github.kr328.clash.design.view - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.TextView -import com.github.kr328.clash.design.R -import com.google.android.material.card.MaterialCardView - -class ColorfulTextCard @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyleAttr: Int = 0 -) : MaterialCardView(context, attributeSet, defStyleAttr) { - private val iconView: View - private val titleView: TextView - private val summaryView: TextView - - var title: CharSequence - get() = titleView.text - set(value) { - titleView.text = value - } - - var summary: CharSequence - get() = summaryView.text - set(value) { - summaryView.text = value - } - - var icon: Drawable? - get() = iconView.background - set(value) { - iconView.background = value - } - - init { - LayoutInflater.from(context).inflate(R.layout.view_colorful_text_card, this, true).apply { - iconView = findViewById(android.R.id.icon) - titleView = findViewById(android.R.id.title) - summaryView = findViewById(android.R.id.summary) - } - - // Custom attrs - context.theme.obtainStyledAttributes(attributeSet, R.styleable.ColorfulTextCard, 0, 0) - .apply { - try { - iconView.background = getDrawable(R.styleable.ColorfulTextCard_icon) - titleView.text = getString(R.styleable.ColorfulTextCard_title) - summaryView.text = getString(R.styleable.ColorfulTextCard_summary) - } finally { - recycle() - } - } - } -} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/view/CommonUiLayout.kt b/design/src/main/java/com/github/kr328/clash/design/view/CommonUiLayout.kt deleted file mode 100644 index a48f6e4fe1..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/view/CommonUiLayout.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.kr328.clash.design.view - -import android.content.Context -import android.util.AttributeSet -import android.widget.LinearLayout -import com.github.kr328.clash.design.common.CommonUiBuilder -import com.github.kr328.clash.design.common.CommonUiScreen - -class CommonUiLayout @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyleAttr: Int = 0 -) : LinearLayout(context, attributeSet, defStyleAttr) { - val screen: CommonUiScreen = CommonUiScreen(this) - - init { - orientation = VERTICAL - } - - fun build(builder: CommonUiBuilder.() -> Unit) { - screen.clear() - - CommonUiBuilder(screen).apply(builder) - } -} \ No newline at end of file diff --git a/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt b/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt deleted file mode 100644 index ce6b706845..0000000000 --- a/design/src/main/java/com/github/kr328/clash/design/view/TextCard.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.github.kr328.clash.design.view - -import android.content.Context -import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.LayoutInflater -import android.view.View -import android.widget.TextView -import com.github.kr328.clash.design.R -import com.google.android.material.card.MaterialCardView - -class TextCard @JvmOverloads constructor( - context: Context, - attributeSet: AttributeSet? = null, - defStyleAttr: Int = 0 -) : MaterialCardView(context, attributeSet, defStyleAttr) { - private val iconView: View - private val titleView: TextView - private val summaryView: TextView - - var title: CharSequence - get() = titleView.text - set(value) { - titleView.text = value - } - - var summary: CharSequence - get() = summaryView.text - set(value) { - summaryView.text = value - } - - var icon: Drawable? - get() = iconView.background - set(value) { - iconView.background = value - } - - init { - LayoutInflater.from(context).inflate(R.layout.view_text_card, this, true).apply { - iconView = findViewById(android.R.id.icon) - titleView = findViewById(android.R.id.title) - summaryView = findViewById(android.R.id.summary) - } - - // Custom attrs - context.theme.obtainStyledAttributes(attributeSet, R.styleable.TextCard, 0, 0).apply { - try { - iconView.background = getDrawable(R.styleable.TextCard_icon) - titleView.text = getString(R.styleable.TextCard_title) - summaryView.text = getString(R.styleable.TextCard_summary) - } finally { - recycle() - } - } - } -} \ No newline at end of file diff --git a/design/src/main/res/drawable/ic_edit.xml b/design/src/main/res/drawable/ic_edit.xml deleted file mode 100644 index 3f39f1cbbb..0000000000 --- a/design/src/main/res/drawable/ic_edit.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/design/src/main/res/layout/dialog_input_text.xml b/design/src/main/res/layout/dialog_input_text.xml deleted file mode 100644 index 2402e54ba6..0000000000 --- a/design/src/main/res/layout/dialog_input_text.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - \ No newline at end of file diff --git a/design/src/main/res/layout/view_category.xml b/design/src/main/res/layout/view_category.xml deleted file mode 100644 index 87c6a72cbe..0000000000 --- a/design/src/main/res/layout/view_category.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/design/src/main/res/layout/view_colorful_text_card.xml b/design/src/main/res/layout/view_colorful_text_card.xml deleted file mode 100644 index ce643cad09..0000000000 --- a/design/src/main/res/layout/view_colorful_text_card.xml +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/design/src/main/res/layout/view_setting_option.xml b/design/src/main/res/layout/view_setting_option.xml deleted file mode 100644 index 02543d3de5..0000000000 --- a/design/src/main/res/layout/view_setting_option.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/design/src/main/res/layout/view_setting_text_input.xml b/design/src/main/res/layout/view_setting_text_input.xml deleted file mode 100644 index 880790e239..0000000000 --- a/design/src/main/res/layout/view_setting_text_input.xml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - diff --git a/design/src/main/res/layout/view_setting_tip.xml b/design/src/main/res/layout/view_setting_tip.xml deleted file mode 100644 index 0767fee1bd..0000000000 --- a/design/src/main/res/layout/view_setting_tip.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/design/src/main/res/layout/view_text_card.xml b/design/src/main/res/layout/view_text_card.xml deleted file mode 100644 index 690d4d9af1..0000000000 --- a/design/src/main/res/layout/view_text_card.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/design/src/main/res/values/attrs.xml b/design/src/main/res/values/attrs.xml deleted file mode 100644 index eba5c53d1c..0000000000 --- a/design/src/main/res/values/attrs.xml +++ /dev/null @@ -1,19 +0,0 @@ - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml deleted file mode 100644 index 76ac825190..0000000000 --- a/design/src/main/res/values/strings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - OK - Cancel - diff --git a/design/src/main/res/values/style.xml b/design/src/main/res/values/style.xml deleted file mode 100644 index 0d2c4cc409..0000000000 --- a/design/src/main/res/values/style.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/gradle.properties b/gradle.properties deleted file mode 100644 index 485855a7fa..0000000000 --- a/gradle.properties +++ /dev/null @@ -1,44 +0,0 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit -# http://www.gradle.org/docs/current/userguide/build_environment.html -# Specifies the JVM arguments used for the daemon process. -# The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx1536m -# When configured, Gradle will run in incubating parallel mode. -# This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects -# org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app's APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn -android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX -android.enableJetifier=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official -kapt.incremental.apt=false -org.gradle.parallel=true - -# dependencies -gBuildToolsVersion=29.0.3 -gCompileSdkVersion=android-29 - -gMinSdkVersion=24 -gTargetSdkVersion=29 - -gVersionCode=10303 -gVersionName=1.3.3 -gKotlinVersion=1.3.72 -gKotlinCoroutineVersion=1.3.7 -gKotlinSerializationVersion=0.20.0 -gRoomVersion=2.2.5 -gAppCenterVersion=2.5.1 -gAndroidKtxVersion=1.3.0 -gRecyclerviewVersion=1.1.0 -gAppCompatVersion=1.1.0 -gMaterialDesignVersion=1.1.0 -gShizukuPreferenceVersion=4.2.0 -gMultiprocessPreferenceVersion=1.0.0 \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index f6b961fd5a86aa5fbfe90f707c3138408be7c718..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54329 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

    <5G2M1WZP5 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 F{|7XC?P&l2 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 758c86d94e..0000000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Tue Mar 03 00:37:05 CST 2020 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip diff --git a/gradlew b/gradlew deleted file mode 100755 index cccdd3d517..0000000000 --- a/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index e95643d6a2..0000000000 --- a/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/service/.gitignore b/service/.gitignore deleted file mode 100644 index 796b96d1c4..0000000000 --- a/service/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/service/build.gradle.kts b/service/build.gradle.kts deleted file mode 100644 index 6a0ad5e8d2..0000000000 --- a/service/build.gradle.kts +++ /dev/null @@ -1,72 +0,0 @@ -plugins { - id("com.android.library") - id("kotlin-android") - id("kotlin-android-extensions") - id("kotlin-kapt") - id("kotlinx-serialization") -} - -val gCompileSdkVersion: String by project -val gBuildToolsVersion: String by project - -val gMinSdkVersion: String by project -val gTargetSdkVersion: String by project - -val gVersionCode: String by project -val gVersionName: String by project - -val gKotlinVersion: String by project -val gKotlinCoroutineVersion: String by project -val gKotlinSerializationVersion: String by project -val gRoomVersion: String by project -val gAndroidKtxVersion: String by project -val gMultiprocessPreferenceVersion: String by project - -android { - compileSdkVersion(gCompileSdkVersion) - buildToolsVersion(gBuildToolsVersion) - - defaultConfig { - minSdkVersion(gMinSdkVersion) - targetSdkVersion(gTargetSdkVersion) - - versionCode = gVersionCode.toInt() - versionName = gVersionName - - consumerProguardFiles("consumer-rules.pro") - } - - buildTypes { - named("release") { - isMinifyEnabled = false - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" - ) - } - } - - compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = "1.8" - } -} - -dependencies { - kapt("androidx.room:room-compiler:$gRoomVersion") - - implementation(project(":core")) - implementation(project(":common")) - implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:$gKotlinSerializationVersion") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$gKotlinVersion") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$gKotlinCoroutineVersion") - implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:$gKotlinCoroutineVersion") - implementation("androidx.room:room-runtime:$gRoomVersion") - implementation("androidx.room:room-ktx:$gRoomVersion") - implementation("androidx.core:core-ktx:$gAndroidKtxVersion") - implementation("rikka.preference:multiprocesspreference:$gMultiprocessPreferenceVersion") -} \ No newline at end of file diff --git a/service/consumer-rules.pro b/service/consumer-rules.pro deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/service/proguard-rules.pro b/service/proguard-rules.pro deleted file mode 100644 index f1b424510d..0000000000 --- a/service/proguard-rules.pro +++ /dev/null @@ -1,21 +0,0 @@ -# Add project specific ProGuard rules here. -# You can control the set of applied configuration files using the -# proguardFiles setting in build.gradle. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - -# Uncomment this to preserve the line number information for -# debugging stack traces. -#-keepattributes SourceFile,LineNumberTable - -# If you keep the line number information, uncomment this to -# hide the original source file name. -#-renamesourcefileattribute SourceFile diff --git a/service/src/main/AndroidManifest.xml b/service/src/main/AndroidManifest.xml deleted file mode 100644 index e6b89a1bb2..0000000000 --- a/service/src/main/AndroidManifest.xml +++ /dev/null @@ -1,81 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl b/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl deleted file mode 100644 index 21c6b5f4ef..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/core/event/Event.aidl +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.kr328.clash.core.event; - -parcelable LogEvent; \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl b/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl deleted file mode 100644 index 7c693877ac..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/core/model/Packet.aidl +++ /dev/null @@ -1,4 +0,0 @@ -package com.github.kr328.clash.core.model; - -parcelable ProxyGroupWrapper; -parcelable General; \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl deleted file mode 100644 index bb3df8c5ac..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/IClashManager.aidl +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.kr328.clash.service; - -import com.github.kr328.clash.service.transact.IStreamCallback; -import com.github.kr328.clash.core.model.Packet; - -interface IClashManager { - // Control - void setSelector(String group, String selected); - void performHealthCheck(String group, IStreamCallback callback); - void setProxyMode(String mode); - - // Query - ProxyGroupWrapper queryProxyGroups(); - General queryGeneral(); - long queryBandwidth(); - - // Events - void registerLogListener(String key, IStreamCallback callback); - void unregisterLogListener(String key); -} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl b/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl deleted file mode 100644 index a3b936e846..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/IProfileService.aidl +++ /dev/null @@ -1,21 +0,0 @@ -package com.github.kr328.clash.service; - -import com.github.kr328.clash.service.transact.IStreamCallback; -import com.github.kr328.clash.service.model.Profile; - -interface IProfileService { - long acquireUnused(String type, String source); - long acquireCloned(long id); - String acquireTempUri(long id); - void release(long id); - void update(long id, in Profile metadata); - void commit(long id, in IStreamCallback callback); - void delete(long id); - void clear(long id); - - Profile queryById(long id); - Profile[] queryAll(); - Profile queryActive(); - - void setActive(long id); -} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/model/Profile.aidl b/service/src/main/aidl/com/github/kr328/clash/service/model/Profile.aidl deleted file mode 100644 index b3c51e8f6b..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/model/Profile.aidl +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.kr328.clash.service.model; - -parcelable Profile; \ No newline at end of file diff --git a/service/src/main/aidl/com/github/kr328/clash/service/transact/IStreamCallback.aidl b/service/src/main/aidl/com/github/kr328/clash/service/transact/IStreamCallback.aidl deleted file mode 100644 index 80d8e4491d..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/transact/IStreamCallback.aidl +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.kr328.clash.service.transact; - -import com.github.kr328.clash.service.transact.ParcelableContainer; - -interface IStreamCallback { - void send(in ParcelableContainer data); - void complete(); - void completeExceptionally(String reason); -} diff --git a/service/src/main/aidl/com/github/kr328/clash/service/transact/ParcelableContainer.aidl b/service/src/main/aidl/com/github/kr328/clash/service/transact/ParcelableContainer.aidl deleted file mode 100644 index 5323a66c5d..0000000000 --- a/service/src/main/aidl/com/github/kr328/clash/service/transact/ParcelableContainer.aidl +++ /dev/null @@ -1,3 +0,0 @@ -package com.github.kr328.clash.service.transact; - -parcelable ParcelableContainer; \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/BaseService.kt b/service/src/main/java/com/github/kr328/clash/service/BaseService.kt deleted file mode 100644 index ad69d2a5ee..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/BaseService.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.kr328.clash.service - -import android.app.Service -import android.content.Context -import com.github.kr328.clash.common.utils.createLanguageConfigurationContext -import com.github.kr328.clash.service.settings.ServiceSettings -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel - -abstract class BaseService : Service(), CoroutineScope by CoroutineScope(Dispatchers.Default) { - lateinit var settings: ServiceSettings - - override fun attachBaseContext(base: Context?) { - settings = ServiceSettings(base ?: return super.attachBaseContext(base)) - - val language = settings.get(ServiceSettings.LANGUAGE) - - super.attachBaseContext(base.createLanguageConfigurationContext(language)) - } - - override fun onDestroy() { - super.onDestroy() - - cancel() - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt deleted file mode 100644 index 0040c9c44f..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManager.kt +++ /dev/null @@ -1,77 +0,0 @@ -package com.github.kr328.clash.service - -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.core.model.General -import com.github.kr328.clash.core.model.ProxyGroupWrapper -import com.github.kr328.clash.service.data.ProfileDao -import com.github.kr328.clash.service.data.SelectedProxyDao -import com.github.kr328.clash.service.data.SelectedProxyEntity -import com.github.kr328.clash.service.transact.IStreamCallback -import com.github.kr328.clash.service.transact.ParcelableContainer -import kotlinx.coroutines.runBlocking - -class ClashManager: IClashManager.Stub() { - override fun setProxyMode(mode: String?) { - Clash.setProxyMode(requireNotNull(mode)) - } - - override fun queryProxyGroups(): ProxyGroupWrapper { - return ProxyGroupWrapper(Clash.queryProxyGroups()) - } - - override fun queryGeneral(): General { - return Clash.queryGeneral() - } - - override fun setSelector(proxy: String?, selected: String?) { - require(proxy != null && selected != null) - - runBlocking { - val current = ProfileDao.queryActive() ?: return@runBlocking - - SelectedProxyDao.setSelectedForProfile(SelectedProxyEntity(current.id, proxy, selected)) - } - - Clash.setSelector(proxy, selected) - } - - override fun queryBandwidth(): Long { - val data = Clash.queryBandwidth() - - return data.download + data.upload - } - - override fun performHealthCheck(group: String?, callback: IStreamCallback?) { - require(group != null && callback != null) - - Clash.performHealthCheck(group).whenComplete { _, u -> - if (u != null) - callback.completeExceptionally(u.message) - else - callback.complete() - } - } - - 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) - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt b/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt deleted file mode 100644 index a4c92eb31d..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashManagerService.kt +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.Intent -import android.os.IBinder - -class ClashManagerService : BaseService() { - override fun onBind(intent: Intent?): IBinder? { - return ClashManager() - } -} \ No newline at end of file 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 deleted file mode 100644 index 28ac0a76f9..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ClashService.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.Intent -import android.os.Binder -import android.os.IBinder -import com.github.kr328.clash.service.clash.ClashRuntime -import com.github.kr328.clash.service.clash.module.CloseModule -import com.github.kr328.clash.service.clash.module.DynamicNotificationModule -import com.github.kr328.clash.service.clash.module.ReloadModule -import com.github.kr328.clash.service.clash.module.StaticNotificationModule -import com.github.kr328.clash.service.settings.ServiceSettings -import com.github.kr328.clash.service.util.broadcastClashStarted -import com.github.kr328.clash.service.util.broadcastClashStopped -import com.github.kr328.clash.service.util.broadcastProfileLoaded -import kotlinx.coroutines.launch - -class ClashService : BaseService() { - private val service = this - private val runtime = ClashRuntime(this) - private var reason: String? = null - - override fun onCreate() { - super.onCreate() - - if (ServiceStatusProvider.serviceRunning) - return stopSelf() - - ServiceStatusProvider.serviceRunning = true - - StaticNotificationModule.createNotificationChannel(service) - StaticNotificationModule.notifyLoadingNotification(service) - - launch { - val settings = ServiceSettings(service) - - runtime.install(ReloadModule(service)) { - onLoaded { - if (it != null) { - service.stopSelfForReason(it.message) - } else { - service.broadcastProfileLoaded() - } - } - } - runtime.install(CloseModule()) { - onClosed { - service.stopSelfForReason(null) - } - } - - if (settings.get(ServiceSettings.NOTIFICATION_REFRESH)) - runtime.install(DynamicNotificationModule(service)) - else - runtime.install(StaticNotificationModule(service)) - - runtime.exec() - } - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - this.broadcastClashStarted() - - return START_STICKY - } - - override fun onBind(intent: Intent?): IBinder? { - return Binder() - } - - override fun onDestroy() { - ServiceStatusProvider.serviceRunning = false - - service.broadcastClashStopped(reason) - - super.onDestroy() - } - - private fun stopSelfForReason(reason: String?) { - this.reason = reason - - stopSelf() - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/Constants.kt b/service/src/main/java/com/github/kr328/clash/service/Constants.kt deleted file mode 100644 index c99d94b182..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/Constants.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.github.kr328.clash.service - -object Constants { - const val SERVICE_SETTING_FILE_NAME = "service" - const val CLASH_DIR = "clash" - const val PROFILES_DIR = "profiles" - - const val PROFILE_PROVIDER_SUFFIX = ".profiles" - const val STATUS_PROVIDER_SUFFIX = ".status" - const val SETTING_PROVIDER_SUFFIX = ".settings" -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt deleted file mode 100644 index 7d0720bda7..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileBackgroundService.kt +++ /dev/null @@ -1,220 +0,0 @@ -package com.github.kr328.clash.service - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.Binder -import android.os.Build -import android.os.IBinder -import android.os.RemoteException -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import com.github.kr328.clash.common.Global -import com.github.kr328.clash.common.ids.Intents -import com.github.kr328.clash.common.ids.NotificationChannels -import com.github.kr328.clash.common.ids.NotificationIds -import com.github.kr328.clash.common.ids.PendingIds -import com.github.kr328.clash.common.utils.intent -import com.github.kr328.clash.service.data.ProfileDao -import com.github.kr328.clash.service.transact.IStreamCallback -import com.github.kr328.clash.service.transact.ParcelableContainer -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlinx.coroutines.selects.select - -class ProfileBackgroundService : BaseService() { - private val self = this - private val requests = Channel(Channel.UNLIMITED) - private val connection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName?) { - stopSelf() - } - - override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { - val service = IProfileService.Stub.asInterface(binder) ?: return stopSelf() - - processProfiles(service) - } - } - - override fun onCreate() { - super.onCreate() - - createNotificationChannels() - - refreshStatusNotification(0) - - bindService(ProfileService::class.intent, connection, Context.BIND_AUTO_CREATE) - } - - override fun onDestroy() { - super.onDestroy() - - unbindService(connection) - - stopForeground(true) - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - super.onStartCommand(intent, flags, startId) - - when (intent?.action) { - Intents.INTENT_ACTION_PROFILE_REQUEST_UPDATE -> { - val id = intent.data?.schemeSpecificPart?.toLongOrNull() - ?: return START_NOT_STICKY - - requests.offer(id) - } - } - - return START_NOT_STICKY - } - - override fun onBind(intent: Intent?): IBinder? { - return Binder() - } - - private fun processProfiles(service: IProfileService) = launch { - val queue: MutableSet = mutableSetOf() - val responses = Channel>(Channel.UNLIMITED) - - while (true) { - val stop = select { - requests.onReceive { - ProfileReceiver.cancelNextUpdate(self, it) - - queue.add(it) - - service.commit(it, object : IStreamCallback.Stub() { - override fun completeExceptionally(reason: String?) { - responses.offer(it to RemoteException(reason)) - } - - override fun complete() { - responses.offer(it to null) - } - - override fun send(data: ParcelableContainer?) {} - }) - - false - } - responses.onReceive { - queue.remove(it.first) - - if (it.second == null) - sendUpdateCompleted(it.first) - else - sendUpdateFailed(it.first, it.second!!.message ?: "Unknown") - - false - } - if (queue.isEmpty()) { - launch { delay(1000 * 5) }.onJoin { - true - } - } - } - - refreshStatusNotification(queue.size) - - if (stop) break - } - - stopSelf() - } - - private fun createNotificationChannels() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - return - - NotificationManagerCompat.from(this).createNotificationChannels( - listOf( - NotificationChannel( - NotificationChannels.PROFILE_STATUS, - getText(R.string.profile_service_status_channel), - NotificationManager.IMPORTANCE_LOW - ), - NotificationChannel( - NotificationChannels.PROFILE_RESULT, - getText(R.string.profile_status_channel), - NotificationManager.IMPORTANCE_DEFAULT - ) - ) - ) - } - - private fun refreshStatusNotification(queueSize: Int) { - val content = if (queueSize != 0) - getString(R.string.format_in_queue, queueSize) - else - getString(R.string.waiting) - - val notification = NotificationCompat.Builder(this, NotificationChannels.PROFILE_STATUS) - .setContentTitle(getText(R.string.processing_profiles)) - .setContentText(content) - .setColor(getColor(R.color.colorAccentService)) - .setSmallIcon(R.drawable.ic_notification) - .setOngoing(true) - .setOnlyAlertOnce(true) - .setGroup(NotificationChannels.PROFILE_STATUS) - .build() - - startForeground(NotificationIds.PROFILE_STATUS, notification) - } - - private suspend fun sendUpdateCompleted(id: Long) { - val entity = ProfileDao.queryById(id) ?: return - - val intent = PendingIntent.getActivity( - this, - PendingIds.generateProfileResultId(id), - Global.openProfileIntent(id), - PendingIntent.FLAG_UPDATE_CURRENT - ) - - val notification = NotificationCompat.Builder(this, NotificationChannels.PROFILE_RESULT) - .setContentTitle(getText(R.string.process_result)) - .setContentText(getString(R.string.format_update_complete, entity.name)) - .setColor(getColor(R.color.colorAccentService)) - .setSmallIcon(R.drawable.ic_notification) - .setOnlyAlertOnce(true) - .setGroup(NotificationChannels.PROFILE_RESULT) - .setAutoCancel(true) - .setContentIntent(intent) - .build() - - NotificationManagerCompat.from(this) - .notify(NotificationIds.generateProfileResultId(id), notification) - } - - private suspend fun sendUpdateFailed(id: Long, reason: String) { - val entity = ProfileDao.queryById(id) ?: return - - val intent = PendingIntent.getActivity( - this, - PendingIds.generateProfileResultId(id), - Global.openProfileIntent(id), - PendingIntent.FLAG_UPDATE_CURRENT - ) - - val notification = NotificationCompat.Builder(this, NotificationChannels.PROFILE_RESULT) - .setContentTitle(getString(R.string.format_update_failure, entity.name)) - .setColor(getColor(R.color.colorAccentService)) - .setSmallIcon(R.drawable.ic_notification) - .setStyle(NotificationCompat.BigTextStyle().bigText(reason)) - .setOnlyAlertOnce(true) - .setGroup(NotificationChannels.PROFILE_RESULT) - .setAutoCancel(true) - .setContentIntent(intent) - .build() - - NotificationManagerCompat.from(this) - .notify(NotificationIds.generateProfileResultId(id), notification) - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileDocumentProvider.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileDocumentProvider.kt deleted file mode 100644 index f3292de843..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileDocumentProvider.kt +++ /dev/null @@ -1,143 +0,0 @@ -package com.github.kr328.clash.service - -import android.database.Cursor -import android.database.MatrixCursor -import android.os.CancellationSignal -import android.os.ParcelFileDescriptor -import android.provider.DocumentsContract.Document -import android.provider.DocumentsContract.Root -import android.provider.DocumentsProvider -import com.github.kr328.clash.service.files.ProfilesResolver -import kotlinx.coroutines.runBlocking - -class ProfileDocumentProvider : DocumentsProvider() { - companion object { - private const val DEFAULT_ROOT_ID = "0" - private val DEFAULT_DOCUMENT_COLUMNS = arrayOf( - Document.COLUMN_DOCUMENT_ID, - Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_MIME_TYPE, - Document.COLUMN_LAST_MODIFIED, - Document.COLUMN_SIZE, - Document.COLUMN_FLAGS - ) - private val DEFAULT_ROOT_COLUMNS = arrayOf( - Root.COLUMN_ROOT_ID, - Root.COLUMN_FLAGS, - Root.COLUMN_ICON, - Root.COLUMN_TITLE, - Root.COLUMN_SUMMARY, - Root.COLUMN_DOCUMENT_ID - ) - } - - private val resolver: ProfilesResolver by lazy { - ProfilesResolver(context!!) - } - - override fun openDocument( - documentId: String?, - mode: String?, - signal: CancellationSignal? - ): ParcelFileDescriptor { - val m = ParcelFileDescriptor.parseMode(mode) - - if (m and ParcelFileDescriptor.MODE_READ_ONLY == 0) - throw UnsupportedOperationException() - - return runBlocking { - val file = resolver.resolve(resolvePath(documentId ?: "")) - file.openFile(ParcelFileDescriptor.MODE_READ_ONLY) - } - } - - override fun queryChildDocuments( - parentDocumentId: String?, - projection: Array?, - sortOrder: String? - ): Cursor { - return runBlocking { - try { - val documentPath = parentDocumentId ?: "/" - val paths = resolvePath(documentPath) - val file = resolver.resolve(paths) - - MatrixCursor(resolveDocumentProjection(projection)).apply { - file.listFiles().forEach { - val childPaths = paths + it - val child = resolver.resolve(paths + it) - - newRow().apply { - add(Document.COLUMN_DOCUMENT_ID, childPaths.joinToString("/")) - add(Document.COLUMN_DISPLAY_NAME, child.name()) - add(Document.COLUMN_MIME_TYPE, child.mimeType()) - add(Document.COLUMN_LAST_MODIFIED, child.lastModified()) - add(Document.COLUMN_SIZE, child.size()) - add(Document.COLUMN_FLAGS, 0) - } - } - } - } catch (e: Exception) { - MatrixCursor(resolveDocumentProjection(projection)) - } - } - } - - override fun queryDocument(documentId: String?, projection: Array?): Cursor { - return runBlocking { - try { - val documentPath = documentId ?: "/" - val paths = resolvePath(documentPath) - val file = resolver.resolve(paths) - - MatrixCursor(resolveDocumentProjection(projection)).apply { - newRow().apply { - add(Document.COLUMN_DOCUMENT_ID, documentPath) - add(Document.COLUMN_DISPLAY_NAME, file.name()) - add(Document.COLUMN_MIME_TYPE, file.mimeType()) - add(Document.COLUMN_LAST_MODIFIED, file.lastModified()) - add(Document.COLUMN_SIZE, file.size()) - add(Document.COLUMN_FLAGS, 0) - } - } - } catch (e: Exception) { - MatrixCursor(resolveDocumentProjection(projection)) - } - } - } - - override fun onCreate(): Boolean { - return true - } - - override fun queryRoots(projection: Array?): Cursor { - val flags = Root.FLAG_LOCAL_ONLY or Root.FLAG_SUPPORTS_IS_CHILD - - return MatrixCursor(projection ?: DEFAULT_ROOT_COLUMNS).apply { - newRow().apply { - add(Root.COLUMN_ROOT_ID, DEFAULT_ROOT_ID) - add(Root.COLUMN_FLAGS, flags) - add(Root.COLUMN_ICON, R.drawable.ic_icon) - add(Root.COLUMN_TITLE, context!!.getString(R.string.clash_for_android)) - add(Root.COLUMN_SUMMARY, context!!.getString(R.string.profiles_and_providers)) - add(Root.COLUMN_DOCUMENT_ID, "/") - add(Root.COLUMN_MIME_TYPES, Document.MIME_TYPE_DIR) - } - } - } - - override fun isChildDocument(parentDocumentId: String?, documentId: String?): Boolean { - if (parentDocumentId == null || documentId == null) - return false - - return documentId.startsWith(parentDocumentId) - } - - private fun resolveDocumentProjection(projection: Array?): Array { - return projection ?: DEFAULT_DOCUMENT_COLUMNS - } - - private fun resolvePath(path: String): List { - return path.split("/").filter(String::isNotBlank) - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt deleted file mode 100644 index f7c833c6ac..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileProcessor.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.webkit.URLUtil -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.service.data.ProfileDao -import com.github.kr328.clash.service.model.Profile -import com.github.kr328.clash.service.model.Profile.Type -import com.github.kr328.clash.service.model.asEntity -import com.github.kr328.clash.service.util.resolveBaseDir -import com.github.kr328.clash.service.util.resolveProfileFile -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.future.await -import kotlinx.coroutines.withContext -import java.io.File -import java.io.FileNotFoundException -import java.util.* - -object ProfileProcessor { - suspend fun createOrUpdate(context: Context, metadata: Profile) = - withContext(Dispatchers.IO) { - metadata.enforceFieldValid() - - context.resolveBaseDir(metadata.id).mkdirs() - context.resolveProfileFile(metadata.id).parentFile?.mkdirs() - - downloadProfile( - context, metadata.uri, - context.resolveProfileFile(metadata.id), - context.resolveBaseDir(metadata.id) - ) - - val entity = if (metadata.type == Type.FILE) - metadata.copy( - uri = ProfileProvider.resolveUri( - context, - context.resolveProfileFile(metadata.id) - ) - ).asEntity() - else - metadata.asEntity() - - if (ProfileDao.queryById(metadata.id) == null) - ProfileDao.insert(entity) - else - ProfileDao.update(entity) - - ProfileReceiver.requestNextUpdate(context, metadata.id) - } - - private suspend fun downloadProfile( - context: Context, - source: Uri, - target: File, - baseDir: File - ) { - when (source.scheme?.toLowerCase(Locale.getDefault())) { - "http", "https" -> - Clash.downloadProfile(source.toString(), target, baseDir) - "content", "file", "resource" -> { - val fd = withContext(Dispatchers.IO) { - @Suppress("BlockingMethodInNonBlockingContext") - context.contentResolver.openFileDescriptor(source, "r") - } ?: throw FileNotFoundException("$source not found") - - Clash.downloadProfile(fd, target, baseDir) - } - else -> throw IllegalArgumentException("Invalid uri type") - }.await() - } - - private fun Profile.enforceFieldValid() { - when { - id < 0 -> - throw IllegalArgumentException("Invalid id") - name.isBlank() -> - throw IllegalArgumentException("Empty name") - type != Type.FILE && type != Type.URL && type != Type.EXTERNAL -> - throw IllegalArgumentException("Invalid type") - !URLUtil.isValidUrl(uri.toString()) -> - throw IllegalArgumentException("Invalid uri") - source?.isValidIntent() == false -> - throw IllegalArgumentException("Invalid source") - interval < 0 -> - throw IllegalArgumentException("Invalid interval") - } - } - - private fun String.isValidIntent(): Boolean { - return try { - Intent.parseUri(this, 0) - true - } catch (e: Exception) { - false - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileProvider.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileProvider.kt deleted file mode 100644 index 170bc68fa0..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileProvider.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.Context -import android.net.Uri -import androidx.core.content.FileProvider -import java.io.File - -class ProfileProvider : FileProvider() { - companion object { - fun resolveUri(context: Context, file: File): Uri { - return getUriForFile( - context, - context.packageName + Constants.PROFILE_PROVIDER_SUFFIX, - file - ) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileReceiver.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileReceiver.kt deleted file mode 100644 index e5e5a38b8f..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileReceiver.kt +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.kr328.clash.service - -import android.app.AlarmManager -import android.app.PendingIntent -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.net.Uri -import androidx.core.content.getSystemService -import com.github.kr328.clash.common.ids.Intents -import com.github.kr328.clash.common.ids.PendingIds -import com.github.kr328.clash.common.utils.componentName -import com.github.kr328.clash.common.utils.startForegroundServiceCompat -import com.github.kr328.clash.service.data.ProfileDao -import com.github.kr328.clash.service.model.asProfile -import kotlinx.coroutines.sync.Mutex - -class ProfileReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (context == null) return - - when (intent?.action) { - Intents.INTENT_ACTION_PROFILE_REQUEST_UPDATE -> { - // Redirect to service - intent.component = ProfileBackgroundService::class.componentName - context.startForegroundServiceCompat(intent) - } - } - } - - companion object { - private val initialized = Mutex() - - @Synchronized - suspend fun initialize(context: Context) { - if ( !initialized.tryLock() ) - return - - ProfileDao.queryAllIds().forEach { - requestNextUpdate(context, it) - } - } - - suspend fun requestNextUpdate(context: Context, id: Long) { - val metadata = ProfileDao.queryById(id)?.asProfile(context) ?: return - val service = context.getSystemService() ?: return - - val pendingIntent = cancelNextUpdate(context, id) - - if (metadata.interval <= 0) - return - - service.set( - AlarmManager.RTC, - metadata.lastModified + metadata.interval, - pendingIntent - ) - } - - fun cancelNextUpdate(context: Context, id: Long): PendingIntent { - val intent = buildUpdatePendingIntent(context, id) - val service = context.getSystemService() ?: return intent - - service.cancel(intent) - - return intent - } - - fun buildUpdateIntentForId(id: Long): Intent { - return Intent(Intents.INTENT_ACTION_PROFILE_REQUEST_UPDATE) - .setComponent(ProfileReceiver::class.componentName) - .setData(Uri.fromParts("id", id.toString(), null)) - } - - private fun buildUpdatePendingIntent(context: Context, id: Long): PendingIntent { - return PendingIntent.getBroadcast( - context, - PendingIds.generateProfileResultId(id), - buildUpdateIntentForId(id), - PendingIntent.FLAG_UPDATE_CURRENT - ) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt b/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt deleted file mode 100644 index 1ba73c0cbc..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ProfileService.kt +++ /dev/null @@ -1,220 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.Intent -import android.net.Uri -import android.os.IBinder -import android.os.RemoteException -import com.github.kr328.clash.service.data.ProfileDao -import com.github.kr328.clash.service.model.Profile -import com.github.kr328.clash.service.model.asProfile -import com.github.kr328.clash.service.transact.IStreamCallback -import com.github.kr328.clash.service.util.broadcastProfileChanged -import com.github.kr328.clash.service.util.resolveBaseDir -import com.github.kr328.clash.service.util.resolveProfileFile -import com.github.kr328.clash.service.util.resolveTempProfileFile -import kotlinx.coroutines.* -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -class ProfileService : BaseService() { - private val service = this - private val lock = Mutex() - private val pending = mutableMapOf() - private val tasks = mutableMapOf() - private val request = Channel(Channel.CONFLATED) - - override fun onBind(intent: Intent?): IBinder? { - return object : IProfileService.Stub() { - override fun setActive(id: Long) { - launch { - ProfileDao.setActive(id) - - service.broadcastProfileChanged() - } - } - - override fun commit(id: Long, callback: IStreamCallback?) { - launch { - lock.withLock { - tasks[id] = callback - - request.offer(Unit) - } - } - } - - override fun release(id: Long) { - launch { - lock.withLock { - pending.remove(id) - } - } - } - - override fun acquireUnused(type: String, source: String?): Long { - return runBlocking { - lock.withLock { - val id = generateNextId() - - pending[id] = Profile( - id = id, - name = "", - type = Profile.Type.valueOf(type), - uri = Uri.EMPTY, - source = source, - active = false, - interval = 0, - lastModified = 0 - ) - - service.resolveBaseDir(id).apply { - deleteRecursively() - mkdirs() - } - - id - } - } - } - - override fun acquireCloned(id: Long): Long { - return runBlocking { - val clonedId = generateNextId() - - pending[clonedId] = - queryMetadataById(id)?.copy( - id = clonedId, - active = false, - lastModified = 0, - type = Profile.Type.FILE - ) ?: return@runBlocking -1L - - clonedId - } - } - - override fun queryActive(): Profile? { - return runBlocking { - ProfileDao.queryActive()?.asProfile(service) - } - } - - override fun delete(id: Long) { - launch { - lock.withLock { - if (pending.remove(id) != null) - service.resolveBaseDir(id).deleteRecursively() - ProfileDao.remove(id) - } - - ProfileReceiver.cancelNextUpdate(service, id) - - service.resolveProfileFile(id).delete() - service.resolveTempProfileFile(id).delete() - service.resolveBaseDir(id).deleteRecursively() - - service.broadcastProfileChanged() - } - } - - override fun clear(id: Long) { - launch { - withContext(Dispatchers.IO) { - resolveBaseDir(id).listFiles()?.forEach { - it.deleteRecursively() - } - } - - service.broadcastProfileChanged() - } - } - - override fun queryAll(): Array { - return runBlocking { - ProfileDao.queryAll().map { it.asProfile(service) }.toTypedArray() - } - } - - override fun queryById(id: Long): Profile? { - return runBlocking { - lock.withLock { - queryMetadataById(id) - } - } - } - - override fun acquireTempUri(id: Long): String? { - val file = service.resolveProfileFile(id) - if ( !file.exists() ) - return null - - val tempFile = service.resolveTempProfileFile(id) - - tempFile.parentFile?.mkdirs() - - file.copyTo(tempFile, overwrite = true) - - return ProfileProvider.resolveUri(service, tempFile).toString() - } - - override fun update(id: Long, metadata: Profile?) { - launch { - lock.withLock { - pending[id] = metadata ?: return@launch - } - } - } - } - } - - override fun onCreate() { - super.onCreate() - - launch { - ProfileReceiver.initialize(service) - - process() - } - } - - private suspend fun process() { - while (isActive) { - request.receive() - - val ctx = lock.withLock { - tasks.entries.firstOrNull()?.also { - tasks[it.key] - } - } ?: continue - - try { - val metadata = queryMetadataById(ctx.key) - ?: throw RemoteException("No such profile") - - ProfileProcessor.createOrUpdate(service, metadata) - - ctx.value?.complete() - - service.broadcastProfileChanged() - } catch (e: Exception) { - ctx.value?.completeExceptionally(e.message) - } - finally { - lock.withLock { - tasks.remove(ctx.key) - } - } - - request.offer(Unit) - } - } - - private suspend fun queryMetadataById(id: Long): Profile? { - return pending[id] ?: ProfileDao.queryById(id)?.asProfile(service) - } - - private suspend fun generateNextId(): Long { - return (ProfileDao.queryAllIds() + pending.keys).max()?.plus(1) ?: 0 - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/RestartReceiver.kt b/service/src/main/java/com/github/kr328/clash/service/RestartReceiver.kt deleted file mode 100644 index b7ea5cc3cd..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/RestartReceiver.kt +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import com.github.kr328.clash.common.utils.intent -import com.github.kr328.clash.common.utils.startForegroundServiceCompat -import com.github.kr328.clash.service.util.startClashService - -class RestartReceiver : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (context == null) - return - - when (intent?.action) { - Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> { - } - else -> return - } - - context.startForegroundServiceCompat(ProfileBackgroundService::class.intent) - - if (ServiceStatusProvider.shouldStartClashOnBoot) - context.startClashService() - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ServiceSettingsProvider.kt b/service/src/main/java/com/github/kr328/clash/service/ServiceSettingsProvider.kt deleted file mode 100644 index 698f4dbe23..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ServiceSettingsProvider.kt +++ /dev/null @@ -1,32 +0,0 @@ -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() { - override fun onCreatePreference(context: Context?): SharedPreferences { - 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 - ) - } - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/ServiceStatusProvider.kt b/service/src/main/java/com/github/kr328/clash/service/ServiceStatusProvider.kt deleted file mode 100644 index 978d6ebaf7..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/ServiceStatusProvider.kt +++ /dev/null @@ -1,81 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.ContentProvider -import android.content.ContentValues -import android.database.Cursor -import android.net.Uri -import android.os.Bundle -import com.github.kr328.clash.common.Global - -class ServiceStatusProvider : ContentProvider() { - companion object { - const val METHOD_PING_CLASH_SERVICE = "pingClashService" - private const val CLASH_SERVICE_RUNNING_FILE = "service_running" - - var serviceRunning: Boolean = false - set(value) { - field = value - shouldStartClashOnBoot = value - } - var shouldStartClashOnBoot: Boolean - get() = Global.application.cacheDir.resolve(CLASH_SERVICE_RUNNING_FILE).exists() - set(value) { - Global.application.cacheDir.resolve(CLASH_SERVICE_RUNNING_FILE).apply { - if (value) - createNewFile() - else - delete() - } - } - var currentProfile: String? = null - } - - override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { - return when (method) { - METHOD_PING_CLASH_SERVICE -> { - return if (serviceRunning) - Bundle().apply { - putString("name", currentProfile) - } - else - null - } - else -> super.call(method, arg, extras) - } - } - - override fun insert(uri: Uri, values: ContentValues?): Uri? { - throw IllegalArgumentException("Stub!") - } - - override fun query( - uri: Uri, - projection: Array?, - selection: String?, - selectionArgs: Array?, - sortOrder: String? - ): Cursor? { - throw IllegalArgumentException("Stub!") - } - - override fun update( - uri: Uri, - values: ContentValues?, - selection: String?, - selectionArgs: Array? - ): Int { - throw IllegalArgumentException("Stub!") - } - - override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - throw IllegalArgumentException("Stub!") - } - - override fun getType(uri: Uri): String? { - throw IllegalArgumentException("Stub!") - } - - override fun onCreate(): Boolean { - return true - } -} \ 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 deleted file mode 100644 index 0258fa9fcd..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/TunService.kt +++ /dev/null @@ -1,154 +0,0 @@ -package com.github.kr328.clash.service - -import android.content.Intent -import android.net.VpnService -import com.github.kr328.clash.service.clash.ClashRuntime -import com.github.kr328.clash.service.clash.module.* -import com.github.kr328.clash.service.settings.ServiceSettings -import com.github.kr328.clash.service.util.* -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.cancel -import kotlinx.coroutines.launch - -class TunService : VpnService(), CoroutineScope by CoroutineScope(Dispatchers.Default) { - companion object { - // from https://github.com/shadowsocks/shadowsocks-android/blob/master/core/src/main/java/com/github/shadowsocks/bg/VpnService.kt - private const val VPN_MTU = 9000 - private const val PRIVATE_VLAN4_SUBNET = 30 - private const val PRIVATE_VLAN4_CLIENT = "172.31.255.253" - private const val PRIVATE_VLAN4_MIRROR = "172.31.255.254" - private const val PRIVATE_VLAN_DNS = "198.18.0.1" - } - - private val service = this - private val runtime = ClashRuntime(this) - private var reason: String? = null - - override fun onCreate() { - super.onCreate() - - if (ServiceStatusProvider.serviceRunning) - return stopSelf() - - ServiceStatusProvider.serviceRunning = true - - StaticNotificationModule.createNotificationChannel(this) - StaticNotificationModule.notifyLoadingNotification(this) - - launch { - val settings = ServiceSettings(service) - val dnsInject = DnsInjectModule() - - runtime.install(TunModule(service)) { - configure = TunConfigure(settings) - } - - runtime.install(ReloadModule(service)) { - onLoaded { - if (it != null) { - service.stopSelfForReason(it.message) - } else { - service.broadcastProfileLoaded() - } - } - } - runtime.install(CloseModule()) { - onClosed { - service.stopSelfForReason(null) - } - } - - if (settings.get(ServiceSettings.NOTIFICATION_REFRESH)) - runtime.install(DynamicNotificationModule(service)) - else - runtime.install(StaticNotificationModule(service)) - - runtime.install(dnsInject) { - dnsOverride = settings.get(ServiceSettings.OVERRIDE_DNS) - } - - runtime.install(NetworkObserveModule(service)) { - onNetworkChanged { network, dnsServers -> - setUnderlyingNetworks(network?.let { arrayOf(it) }) - - if (settings.get(ServiceSettings.AUTO_ADD_SYSTEM_DNS)) { - val dnsStrings = dnsServers.map { - it.asSocketAddressText(53) - } - - dnsInject.appendDns = dnsStrings - } - - broadcastNetworkChanged() - } - } - - runtime.exec() - } - } - - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - service.broadcastClashStarted() - - return super.onStartCommand(intent, flags, startId) - } - - override fun onDestroy() { - TunModule.requestStop() - - ServiceStatusProvider.serviceRunning = false - - service.broadcastClashStopped(reason) - - cancel() - - super.onDestroy() - } - - private inner class TunConfigure(private val settings: ServiceSettings) : TunModule.Configure { - override val builder: Builder - get() = Builder() - override val mtu: Int - get() = VPN_MTU - override val gateway: String - get() = "$PRIVATE_VLAN4_CLIENT/$PRIVATE_VLAN4_SUBNET" - override val mirror: String - get() = PRIVATE_VLAN4_MIRROR - override val route: List - get() { - return if (settings.get(ServiceSettings.BYPASS_PRIVATE_NETWORK)) - resources.getStringArray(R.array.bypass_private_route).toList() - else - resources.getStringArray(R.array.bypass_local_route).toList() - } - override val dnsAddress: String - get() = PRIVATE_VLAN_DNS - override val dnsHijacking: Boolean - get() = settings.get(ServiceSettings.DNS_HIJACKING) - override val allowApplications: Collection - get() { - return if (settings.get(ServiceSettings.ACCESS_CONTROL_MODE) == ServiceSettings.ACCESS_CONTROL_MODE_WHITELIST) { - (settings.get(ServiceSettings.ACCESS_CONTROL_PACKAGES) + packageName) - } else emptySet() - } - override val disallowApplication: Collection - get() { - return if (settings.get(ServiceSettings.ACCESS_CONTROL_MODE) == ServiceSettings.ACCESS_CONTROL_MODE_BLACKLIST) { - (settings.get(ServiceSettings.ACCESS_CONTROL_PACKAGES) - packageName) - } else emptySet() - } - - override fun onCreateTunFailure() { - stopSelfForReason("Establish VPN rejected by system") - } - } - - private fun stopSelfForReason(reason: String?) { - this.reason = reason - - stopSelf() - - TunModule.requestStop() - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/ClashRuntime.kt b/service/src/main/java/com/github/kr328/clash/service/clash/ClashRuntime.kt deleted file mode 100644 index 10d5c7d659..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/clash/ClashRuntime.kt +++ /dev/null @@ -1,104 +0,0 @@ -package com.github.kr328.clash.service.clash - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import com.github.kr328.clash.common.Permissions -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.service.clash.module.Module -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.delay -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import kotlinx.coroutines.selects.select -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.withLock - -class ClashRuntime(private val context: Context) { - companion object { - private val GIL = Mutex() // :) - } - - private val modules: MutableList = mutableListOf() - private val mutex = Mutex() - - suspend fun install(module: T, configure: T.() -> Unit = {}) = mutex.withLock { - modules.add(module) - - module.onCreate() - module.configure() - } - - suspend fun exec() { - GIL.withLock { - execLocked() - } - } - - private suspend fun execLocked() { - val broadcastChannel = Channel(Channel.UNLIMITED) - val tickerChannel = Channel() - val receiver = object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - broadcastChannel.offer(intent ?: return) - } - } - var tickerEnabled: Boolean - - coroutineScope { - launch { - while (isActive) { - tickerChannel.offer(Unit) - delay(1000) - } - } - - try { - Clash.start() - - context.registerReceiver(receiver, IntentFilter().apply { - modules.flatMap { it.receiveBroadcasts }.distinct().forEach { - addAction(it) - } - }, Permissions.PERMISSION_RECEIVE_BROADCASTS, null) - - modules.forEach { - it.onStart() - } - - while (isActive) { - tickerEnabled = modules.any { it.enableTicker } - - select { - broadcastChannel.onReceive { intent -> - modules.forEach { - it.onBroadcastReceived(intent) - } - } - if (tickerEnabled) { - tickerChannel.onReceive { - modules.forEach { - it.onTick() - } - } - } - } - } - } finally { - runCatching { - modules.reversed().forEach { - it.onStop() - } - } - - runCatching { - context.unregisterReceiver(receiver) - } - - Clash.stop() - } - } - } -} diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/CloseModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/CloseModule.kt deleted file mode 100644 index ed33227f43..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/CloseModule.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.kr328.clash.service.clash.module - -import android.content.Intent -import com.github.kr328.clash.common.ids.Intents - -class CloseModule : Module() { - override val receiveBroadcasts: Set - get() = setOf(Intents.INTENT_ACTION_CLASH_REQUEST_STOP) - - private var callback: () -> Unit = {} - - fun onClosed(cb: () -> Unit) { - callback = cb - } - - override suspend fun onBroadcastReceived(intent: Intent) { - when (intent.action) { - Intents.INTENT_ACTION_CLASH_REQUEST_STOP -> - callback() - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/DnsInjectModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/DnsInjectModule.kt deleted file mode 100644 index 1c8500d121..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/DnsInjectModule.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.kr328.clash.service.clash.module - -import com.github.kr328.clash.core.Clash - -class DnsInjectModule : Module() { - var dnsOverride: Boolean = false - set(value) { - field = value - - Clash.setDnsOverride(value, appendDns) - } - var appendDns: List = emptyList() - set(value) { - field = value - - Clash.setDnsOverride(dnsOverride, value) - } - - override suspend fun onStart() { - Clash.setDnsOverride(dnsOverride, appendDns) - } - - override suspend fun onStop() { - Clash.setDnsOverride(false, emptyList()) - } - -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/DynamicNotificationModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/DynamicNotificationModule.kt deleted file mode 100644 index ec30ecc237..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/DynamicNotificationModule.kt +++ /dev/null @@ -1,96 +0,0 @@ -package com.github.kr328.clash.service.clash.module - -import android.app.PendingIntent -import android.app.Service -import android.content.Intent -import android.os.PowerManager -import androidx.core.app.NotificationCompat -import com.github.kr328.clash.common.Global -import com.github.kr328.clash.common.ids.Intents -import com.github.kr328.clash.common.ids.NotificationChannels -import com.github.kr328.clash.common.ids.NotificationIds -import com.github.kr328.clash.common.utils.asBytesString -import com.github.kr328.clash.common.utils.asSpeedString -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.service.R -import com.github.kr328.clash.service.ServiceStatusProvider -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class DynamicNotificationModule(private val service: Service) : Module() { - override val receiveBroadcasts: Set - get() = setOf( - Intent.ACTION_SCREEN_ON, - Intent.ACTION_SCREEN_OFF, - Intents.INTENT_ACTION_PROFILE_LOADED - ) - private val builder = NotificationCompat.Builder(service, NotificationChannels.CLASH_STATUS) - .setSmallIcon(R.drawable.ic_notification) - .setOngoing(true) - .setColor(service.getColor(R.color.colorAccentService)) - .setOnlyAlertOnce(true) - .setShowWhen(false) - .setGroup(NotificationChannels.CLASH_STATUS) - .setContentIntent( - PendingIntent.getActivity( - service, - NotificationIds.CLASH_STATUS, - Global.openMainIntent(), - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - private var currentProfile = "Not selected" - - override suspend fun onBroadcastReceived(intent: Intent) { - when (intent.action) { - Intent.ACTION_SCREEN_ON -> - enableTicker = true - Intent.ACTION_SCREEN_OFF -> - enableTicker = false - Intents.INTENT_ACTION_PROFILE_LOADED -> - reload() - } - } - - override suspend fun onStart() { - enableTicker = service.getSystemService(PowerManager::class.java).isInteractive - } - - override suspend fun onStop() { - service.stopForeground(true) - } - - override suspend fun onTick() { - val traffic = Clash.querySpeed() - val bandwidth = Clash.queryBandwidth() - - val uploading = traffic.upload.asSpeedString() - val downloading = traffic.download.asSpeedString() - val uploaded = bandwidth.upload.asBytesString() - val downloaded = bandwidth.download.asBytesString() - - withContext(Dispatchers.Default) { - val notification = builder - .setContentTitle(currentProfile) - .setContentText( - service.getString( - R.string.clash_notification_content, - uploading, downloading - ) - ) - .setSubText( - service.getString( - R.string.clash_notification_content, - uploaded, downloaded - ) - ) - .build() - - service.startForeground(NotificationIds.CLASH_STATUS, notification) - } - } - - private fun reload() { - currentProfile = ServiceStatusProvider.currentProfile ?: "Not selected" - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/Module.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/Module.kt deleted file mode 100644 index eace7acaf5..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/Module.kt +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.kr328.clash.service.clash.module - -import android.content.Intent - -abstract class Module { - open suspend fun onCreate() {} - open suspend fun onStart() {} - open suspend fun onStop() {} - open suspend fun onTick() {} - open suspend fun onBroadcastReceived(intent: Intent) {} - - open val receiveBroadcasts: Set = emptySet() - var enableTicker: Boolean = false -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/NetworkObserveModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/NetworkObserveModule.kt deleted file mode 100644 index 0ba3c3cefc..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/NetworkObserveModule.kt +++ /dev/null @@ -1,112 +0,0 @@ -package com.github.kr328.clash.service.clash.module - -import android.content.Context -import android.net.* -import com.github.kr328.clash.common.utils.Log -import kotlinx.coroutines.sync.Mutex -import java.net.InetAddress - -class NetworkObserveModule(context: Context) : Module() { - private var networkChanged: (Network?, List) -> Unit = { _, _ -> } - private var network: Network? = null - private val lock = Mutex() - private val connectivity = context.getSystemService(ConnectivityManager::class.java) - private val callback = object : ConnectivityManager.NetworkCallback() { - private val internet = mutableMapOf() - private val dns = mutableMapOf>() - - override fun onAvailable(network: Network) { - detectDefaultNetwork() - } - - override fun onLost(network: Network) { - internet.remove(network) - dns.remove(network) - - detectDefaultNetwork() - } - - override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) { - val old = dns[network] - val new = linkProperties.dnsServers - - if (old != new) { - dns[network] = new - detectDefaultNetwork() - } - } - - override fun onCapabilitiesChanged( - network: Network, - networkCapabilities: NetworkCapabilities - ) { - val old = internet[network] - val new = networkCapabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - - if (old != new) { - internet[network] = new - detectDefaultNetwork() - } - } - } - - override suspend fun onStart() { - try { - connectivity.registerNetworkCallback(NetworkRequest.Builder().build(), callback) - } catch (e: Exception) { - Log.w("Register NetworkCallback failure", e) - } - } - - override suspend fun onStop() { - try { - connectivity.unregisterNetworkCallback(callback) - } catch (e: Exception) { - Log.w("Unregister NetworkCallback failure", e) - } - } - - fun onNetworkChanged(callback: (Network?, List) -> Unit) { - networkChanged = callback - } - - private fun detectDefaultNetwork() { - if (!lock.tryLock()) - return - - val def = connectivity.allNetworks - .asSequence() - .mapNotNull { network -> - connectivity.getNetworkCapabilities(network)?.let { it to network } - } - .filterNot { - it.first.hasTransport(NetworkCapabilities.TRANSPORT_VPN) || - !it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) - } - .sortedBy { - when { - it.first.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> 0 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> 1 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_BLUETOOTH) -> 2 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_LOWPAN) -> 3 - it.first.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> 4 - else -> 5 - } + if (it.first.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)) - -1000 - else - 0 - } - .map { - it.second - } - .firstOrNull() - - if (def != network) { - network = def - - networkChanged(def, connectivity.getLinkProperties(def)?.dnsServers ?: emptyList()) - } - - lock.unlock() - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/ReloadModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/ReloadModule.kt deleted file mode 100644 index 8084a2fe6a..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/ReloadModule.kt +++ /dev/null @@ -1,66 +0,0 @@ -package com.github.kr328.clash.service.clash.module - -import android.content.Context -import android.content.Intent -import com.github.kr328.clash.common.ids.Intents -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.service.ServiceStatusProvider -import com.github.kr328.clash.service.data.ProfileDao -import com.github.kr328.clash.service.data.SelectedProxyDao -import com.github.kr328.clash.service.util.resolveBaseDir -import com.github.kr328.clash.service.util.resolveProfileFile -import kotlinx.coroutines.future.await -import kotlinx.coroutines.sync.Mutex - -class ReloadModule(private val context: Context) : Module() { - override val receiveBroadcasts: Set - get() = setOf(Intents.INTENT_ACTION_NETWORK_CHANGED, Intents.INTENT_ACTION_PROFILE_CHANGED) - private val reloadMutex = Mutex() - private var loadedCallback: (Exception?) -> Unit = {} - - override suspend fun onStart() { - reload() - } - - override suspend fun onBroadcastReceived(intent: Intent) { - if (!reloadMutex.tryLock()) - return - - when (intent.action) { - Intents.INTENT_ACTION_NETWORK_CHANGED, Intents.INTENT_ACTION_PROFILE_CHANGED -> { - reload() - } - } - - reloadMutex.unlock() - } - - - fun onLoaded(callback: (Exception?) -> Unit) { - loadedCallback = callback - } - - private suspend fun reload() { - try { - val active = ProfileDao.queryActive() - ?: throw NullPointerException("No profile selected") - - Clash.loadProfile( - context.resolveProfileFile(active.id), - context.resolveBaseDir(active.id).apply { mkdirs() } - ).await() - - val remove = SelectedProxyDao.querySelectedForProfile(active.id) - .filterNot { Clash.setSelector(it.proxy, it.selected) } - .map { it.selected } - - SelectedProxyDao.removeSelectedForProfile(active.id, remove) - - ServiceStatusProvider.currentProfile = active.name - - loadedCallback(null) - } catch (e: Exception) { - loadedCallback(e) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/StaticNotificationModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/StaticNotificationModule.kt deleted file mode 100644 index 66086e129c..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/StaticNotificationModule.kt +++ /dev/null @@ -1,88 +0,0 @@ -package com.github.kr328.clash.service.clash.module - -import android.app.NotificationChannel -import android.app.NotificationManager -import android.app.PendingIntent -import android.app.Service -import android.content.Intent -import android.os.Build -import androidx.core.app.NotificationCompat -import androidx.core.app.NotificationManagerCompat -import com.github.kr328.clash.common.Global -import com.github.kr328.clash.common.ids.Intents -import com.github.kr328.clash.common.ids.NotificationChannels -import com.github.kr328.clash.common.ids.NotificationIds -import com.github.kr328.clash.service.R -import com.github.kr328.clash.service.ServiceStatusProvider - -class StaticNotificationModule(private val service: Service) : Module() { - override val receiveBroadcasts: Set - get() = setOf(Intents.INTENT_ACTION_PROFILE_LOADED) - private val builder = NotificationCompat.Builder(service, NotificationChannels.CLASH_STATUS) - .setSmallIcon(R.drawable.ic_notification) - .setOngoing(true) - .setColor(service.getColor(R.color.colorAccentService)) - .setOnlyAlertOnce(true) - .setShowWhen(false) - .setGroup(NotificationChannels.CLASH_STATUS) - .setContentIntent( - PendingIntent.getActivity( - service, - NotificationIds.CLASH_STATUS, - Global.openMainIntent(), - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - - override suspend fun onBroadcastReceived(intent: Intent) { - when (intent.action) { - Intents.INTENT_ACTION_PROFILE_LOADED -> { - update() - } - } - } - - override suspend fun onStop() { - service.stopForeground(true) - } - - private fun update() { - val profileName = ServiceStatusProvider.currentProfile ?: "Not selected" - - val notification = builder - .setContentTitle(profileName) - .setContentText(service.getText(R.string.running)) - .build() - - service.startForeground(NotificationIds.CLASH_STATUS, notification) - } - - companion object { - fun createNotificationChannel(service: Service) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) - return - NotificationManagerCompat.from(service).createNotificationChannel( - NotificationChannel( - NotificationChannels.CLASH_STATUS, - service.getText(R.string.clash_service_status_channel), - NotificationManager.IMPORTANCE_LOW - ) - ) - } - - fun notifyLoadingNotification(service: Service) { - val notification = - NotificationCompat.Builder(service, NotificationChannels.CLASH_STATUS) - .setSmallIcon(R.drawable.ic_notification) - .setOngoing(true) - .setColor(service.getColor(R.color.colorAccentService)) - .setOnlyAlertOnce(true) - .setShowWhen(false) - .setGroup(NotificationChannels.CLASH_STATUS) - .setContentTitle(service.getText(R.string.loading)) - .build() - - service.startForeground(NotificationIds.CLASH_STATUS, notification) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt b/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt deleted file mode 100644 index 7826fa80ba..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/clash/module/TunModule.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.github.kr328.clash.service.clash.module - -import android.app.PendingIntent -import android.net.VpnService -import android.os.Build -import com.github.kr328.clash.common.Global -import com.github.kr328.clash.common.ids.PendingIds -import com.github.kr328.clash.core.Clash -import com.github.kr328.clash.service.util.parseCIDR -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext - -class TunModule(private val service: VpnService) : Module() { - interface Configure { - val builder: VpnService.Builder - val mtu: Int - val gateway: String - val mirror: String - val route: List - val dnsAddress: String - val dnsHijacking: Boolean - val allowApplications: Collection - val disallowApplication: Collection - - fun onCreateTunFailure() - } - - var configure: Configure? = null - - override suspend fun onStart() { - withContext(Dispatchers.IO) { - val c = configure ?: throw IllegalArgumentException("Configure required") - - val builder = c.builder - - parseCIDR(c.gateway).let { - builder.addAddress(it.ip, it.prefix) - } - c.route.map { parseCIDR(it) }.forEach { - builder.addRoute(it.ip, it.prefix) - } - c.allowApplications.forEach { - builder.addAllowedApplication(it) - } - c.disallowApplication.forEach { - builder.addDisallowedApplication(it) - } - - builder.setBlocking(false) - builder.setMtu(c.mtu) - builder.setSession("Clash") - builder.addDnsServer(c.dnsAddress) - builder.setConfigureIntent( - PendingIntent.getActivity( - service, - PendingIds.CLASH_VPN, - Global.openMainIntent(), - PendingIntent.FLAG_UPDATE_CURRENT - ) - ) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - builder.setMetered(false) - } - - val fd = try { - builder.establish() ?: throw NullPointerException() - } - catch (e: Exception) { - return@withContext c.onCreateTunFailure() - } - - if (c.dnsHijacking) { - Clash.startTunDevice( - fd.detachFd(), - c.mtu, - c.gateway, - c.mirror, - IPV4_ANY, - service::protect, - service::stopSelf - ) - } else { - Clash.startTunDevice( - fd.detachFd(), - c.mtu, - c.gateway, - c.mirror, - c.dnsAddress, - service::protect, - service::stopSelf - ) - } - } - } - - override suspend fun onStop() { - Clash.stopTunDevice() - } - - companion object { - private const val IPV4_ANY = "0.0.0.0" - - fun requestStop() { - Clash.stopTunDevice() - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/Database.kt b/service/src/main/java/com/github/kr328/clash/service/data/Database.kt deleted file mode 100644 index ef25c8e9c0..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/data/Database.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.github.kr328.clash.service.data - -import android.content.Context -import androidx.room.Room -import androidx.room.RoomDatabase -import com.github.kr328.clash.common.Global -import com.github.kr328.clash.service.data.migrations.MIGRATIONS -import androidx.room.Database as DatabaseMetadata - -@DatabaseMetadata( - version = 4, - exportSchema = false, - entities = [ProfileEntity::class, SelectedProxyEntity::class] -) -abstract class Database : RoomDatabase() { - abstract fun openProfileDao(): ProfileDao - abstract fun openSelectedProxyDao(): SelectedProxyDao - - companion object { - val database = open(Global.application) - - private fun open(context: Context): Database { - return Room.databaseBuilder( - context.applicationContext, - Database::class.java, - "clash-config" - ).addMigrations(*MIGRATIONS).build() - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ProfileDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/ProfileDao.kt deleted file mode 100644 index 0315eecbb5..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/data/ProfileDao.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.github.kr328.clash.service.data - -import androidx.room.* - -@Dao -interface ProfileDao { - @Query("UPDATE profiles SET active = CASE WHEN id = :id THEN 1 ELSE 0 END") - suspend fun setActive(id: Long) - - @Query("SELECT * FROM profiles WHERE active = 1 LIMIT 1") - suspend fun queryActive(): ProfileEntity? - - @Query("SELECT * FROM profiles") - suspend fun queryAll(): List - - @Query("SELECT * FROM profiles WHERE id = :id") - suspend fun queryById(id: Long): ProfileEntity? - - @Query("SELECT id FROM profiles") - suspend fun queryAllIds(): List - - @Insert(onConflict = OnConflictStrategy.ABORT) - suspend fun insert(profile: ProfileEntity): Long - - @Update(onConflict = OnConflictStrategy.ABORT) - suspend fun update(profile: ProfileEntity) - - @Query("DELETE FROM profiles WHERE id = :id") - suspend fun remove(id: Long) - - companion object : ProfileDao by Database.database.openProfileDao() -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/ProfileEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/ProfileEntity.kt deleted file mode 100644 index d543aeab41..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/data/ProfileEntity.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.kr328.clash.service.data - -import androidx.annotation.Keep -import androidx.room.ColumnInfo -import androidx.room.Entity - -@Entity(tableName = "profiles", primaryKeys = ["id"]) -@Keep -data class ProfileEntity( - @ColumnInfo(name = "name") val name: String, - @ColumnInfo(name = "type") val type: Int, - @ColumnInfo(name = "uri") val uri: String, - @ColumnInfo(name = "source") val source: String?, - @ColumnInfo(name = "active") val active: Boolean, - @ColumnInfo(name = "interval") val interval: Long, - @ColumnInfo(name = "id") val id: Long -) { - companion object { - const val TYPE_FILE = 1 - const val TYPE_URL = 2 - const val TYPE_EXTERNAL = 3 - const val TYPE_UNKNOWN = -1 - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/SelectedProxyDao.kt b/service/src/main/java/com/github/kr328/clash/service/data/SelectedProxyDao.kt deleted file mode 100644 index 819e0e86b8..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/data/SelectedProxyDao.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.github.kr328.clash.service.data - -import androidx.room.Dao -import androidx.room.Insert -import androidx.room.OnConflictStrategy -import androidx.room.Query - -@Dao -interface SelectedProxyDao { - @Insert(onConflict = OnConflictStrategy.REPLACE) - suspend fun setSelectedForProfile(item: SelectedProxyEntity) - - @Query("SELECT * FROM selected_proxies WHERE profile_id = :id") - suspend fun querySelectedForProfile(id: Long): List - - @Query("DELETE FROM selected_proxies WHERE profile_id = :id AND proxy in (:selected)") - suspend fun removeSelectedForProfile(id: Long, selected: List) - - companion object : SelectedProxyDao by Database.database.openSelectedProxyDao() -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/SelectedProxyEntity.kt b/service/src/main/java/com/github/kr328/clash/service/data/SelectedProxyEntity.kt deleted file mode 100644 index 54a22730eb..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/data/SelectedProxyEntity.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.kr328.clash.service.data - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey - -@Entity( - tableName = "selected_proxies", - foreignKeys = [ForeignKey( - entity = ProfileEntity::class, - childColumns = ["profile_id"], - parentColumns = ["id"], - onDelete = ForeignKey.CASCADE, - onUpdate = ForeignKey.CASCADE - )], - primaryKeys = ["profile_id", "proxy"] -) -data class SelectedProxyEntity( - @ColumnInfo(name = "profile_id") val profileId: Long, - @ColumnInfo(name = "proxy") val proxy: String, - @ColumnInfo(name = "selected") val selected: String -) \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migration12.kt b/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migration12.kt deleted file mode 100644 index c77a7d1367..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migration12.kt +++ /dev/null @@ -1,144 +0,0 @@ -package com.github.kr328.clash.service.data.migrations - -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import androidx.core.content.edit -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import com.github.kr328.clash.common.Global -import com.github.kr328.clash.common.utils.Log -import com.github.kr328.clash.service.Constants -import com.github.kr328.clash.service.data.ProfileEntity -import com.github.kr328.clash.service.settings.ServiceSettings -import com.github.kr328.clash.service.util.resolveBaseDir -import com.github.kr328.clash.service.util.resolveProfileFile -import java.io.File - -object Migration12: Migration(1, 2) { - private fun process(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE profiles RENAME TO _profiles") - database.execSQL("ALTER TABLE profile_select_proxies RENAME TO _profile_select_proxies") - - database.execSQL("CREATE TABLE IF NOT EXISTS `profiles` (`name` TEXT NOT NULL, `type` INTEGER NOT NULL, `uri` TEXT NOT NULL, `source` TEXT, `active` INTEGER NOT NULL, `last_update` INTEGER NOT NULL, `update_interval` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))") - database.execSQL("CREATE TABLE IF NOT EXISTS `profile_select_proxies` (`profile_id` INTEGER NOT NULL, `proxy` TEXT NOT NULL, `selected` TEXT NOT NULL, PRIMARY KEY(`profile_id`, `proxy`), FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") - - database.query("SELECT name, token, file, active, last_update, id FROM _profiles") - .use { cursor -> - - Global.application.filesDir.resolve(Constants.CLASH_DIR).listFiles()?.forEach { - it.deleteRecursively() - } - - cursor.moveToFirst() - while (!cursor.isAfterLast) { - // old - // name, token, file, active, last_update, id - val name = cursor.getString(0) - val token = cursor.getString(1) - val file = cursor.getString(2) - val active = cursor.getInt(3) - val lastUpdate = cursor.getLong(4) - val id = cursor.getLong(5) - - // new - // name, type, uri, source, active, last_update, update_interval, id - val type = when { - token.startsWith("url") -> ProfileEntity.TYPE_URL - token.startsWith("file") -> ProfileEntity.TYPE_FILE - else -> ProfileEntity.TYPE_UNKNOWN - } - - File(file).renameTo(Global.application.resolveProfileFile(id)) - Global.application.resolveBaseDir(id).mkdirs() - - database.insert("profiles", - SQLiteDatabase.CONFLICT_ABORT, - ContentValues().apply { - put("name", name) - put("type", type) - put("uri", token.removePrefix("url|").removePrefix("file|")) - putNull("source") - put("active", active) - put("last_update", lastUpdate) - put("update_interval", 0) - put("id", id) - }) - - cursor.moveToNext() - } - } - - database.query("SELECT profile_id, proxy, selected FROM _profile_select_proxies ORDER BY id") - .use { cursor -> - cursor.moveToFirst() - while (!cursor.isAfterLast) { - // old - // profile_id, proxy, selected, id - val profileId = cursor.getLong(0) - val proxy: String = cursor.getString(1) - val selected = cursor.getString(2) - - // new - // profile_id, proxy, selected - - database.insert("profile_select_proxies", - SQLiteDatabase.CONFLICT_REPLACE, - ContentValues().apply { - put("profile_id", profileId) - put("proxy", proxy) - put("selected", selected) - }) - - cursor.moveToNext() - } - } - - database.execSQL("DROP TABLE IF EXISTS _profiles") - database.execSQL("DROP TABLE IF EXISTS _profile_select_proxies") - - // Migration settings - val oldSettings = Global.application - .getSharedPreferences("clash_service", Context.MODE_PRIVATE) - val newSettings = ServiceSettings( - Global.application - .getSharedPreferences(Constants.SERVICE_SETTING_FILE_NAME, Context.MODE_PRIVATE) - ) - - val accessMode = oldSettings - .getInt("key_access_control_mode", 0) - val accessPackages = oldSettings - .getStringSet("ley_access_control_apps", emptySet())!! // just typo :) - val dnsHijack = oldSettings - .getBoolean("key_dns_hijacking_enabled", true) - val bypassPrivate = oldSettings - .getBoolean("key_bypass_private_network", true) - - oldSettings.edit { - clear() - } - - newSettings.commit { - val newAccessMode = when (accessMode) { - 0 -> ServiceSettings.ACCESS_CONTROL_MODE_ALL - 1 -> ServiceSettings.ACCESS_CONTROL_MODE_WHITELIST - 2 -> ServiceSettings.ACCESS_CONTROL_MODE_BLACKLIST - else -> ServiceSettings.ACCESS_CONTROL_MODE_ALL - } - - put(ServiceSettings.ACCESS_CONTROL_MODE, newAccessMode) - put(ServiceSettings.ACCESS_CONTROL_PACKAGES, accessPackages) - put(ServiceSettings.DNS_HIJACKING, dnsHijack) - put(ServiceSettings.BYPASS_PRIVATE_NETWORK, bypassPrivate) - } - } - - override fun migrate(database: SupportSQLiteDatabase) { - try { - process(database) - Log.i("Database Migrated 1 -> 2") - } catch (e: Exception) { - Log.e("Migration failure", e) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migration23.kt b/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migration23.kt deleted file mode 100644 index 600c4eec72..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migration23.kt +++ /dev/null @@ -1,99 +0,0 @@ -package com.github.kr328.clash.service.data.migrations - -import android.content.ContentValues -import android.content.Context -import android.database.sqlite.SQLiteDatabase -import androidx.core.app.NotificationManagerCompat -import androidx.core.content.edit -import androidx.core.database.getStringOrNull -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import com.github.kr328.clash.common.Global -import com.github.kr328.clash.common.utils.Log - -object Migration23: Migration(2, 3) { - override fun migrate(database: SupportSQLiteDatabase) { - try { - database.execSQL("ALTER TABLE profile_select_proxies RENAME TO _selected_proxies") - database.execSQL("ALTER TABLE profiles RENAME TO _profiles") - - database.execSQL("CREATE TABLE IF NOT EXISTS `profiles` (`name` TEXT NOT NULL, `type` INTEGER NOT NULL, `uri` TEXT NOT NULL, `source` TEXT, `active` INTEGER NOT NULL, `interval` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))") - database.execSQL("CREATE TABLE IF NOT EXISTS `selected_proxies` (`profile_id` INTEGER NOT NULL, `proxy` TEXT NOT NULL, `selected` TEXT NOT NULL, PRIMARY KEY(`profile_id`, `proxy`), FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") - - database.query("SELECT name, type, uri, source, active, update_interval, id FROM _profiles") - .use { cursor -> - cursor.moveToFirst() - while (!cursor.isAfterLast) { - // old - // name, type, uri, source, active, last_update, update_interval(seconds), id - // new - // name, type, uri, source, active, interval(millis seconds), id - val name = cursor.getString(0) - val type = cursor.getInt(1) - val uri = cursor.getString(2) - val source = cursor.getStringOrNull(3) - val active = cursor.getInt(4) - val interval = cursor.getLong(5) - val id = cursor.getLong(6) - - database.insert("profiles", - SQLiteDatabase.CONFLICT_ABORT, - ContentValues().apply { - put("name", name) - put("type", type) - put("uri", uri) - put("source", source) - put("active", active) - put("interval", interval * 1000) - put("id", id) - }) - - cursor.moveToNext() - } - } - - database.query("SELECT profile_id, proxy, selected FROM _selected_proxies") - .use { cursor -> - cursor.moveToFirst() - while (!cursor.isAfterLast) { - // just copy - // profile_id, proxy, selected - val profileId = cursor.getLong(0) - val proxy = cursor.getString(1) - val selected = cursor.getString(2) - - database.insert("selected_proxies", - SQLiteDatabase.CONFLICT_REPLACE, - ContentValues().apply { - put("profile_id", profileId) - put("proxy", proxy) - put("selected", selected) - }) - - cursor.moveToNext() - } - } - - database.execSQL("DROP TABLE IF EXISTS _profiles") - database.execSQL("DROP TABLE IF EXISTS _selected_proxies") - - val uiSp = Global.application - .getSharedPreferences("ui", Context.MODE_PRIVATE) - val srvSp = Global.application - .getSharedPreferences("service", Context.MODE_PRIVATE) - - srvSp.edit { - putBoolean("enable_vpn", uiSp.getBoolean("enable_vpn", true)) - } - - NotificationManagerCompat.from(Global.application).apply { - deleteNotificationChannel("profile_service_status") - deleteNotificationChannel("profile_service_result") - } - - Log.i("Database Migrated 2 -> 3") - } catch (e: Exception) { - Log.e("Migration failure", e) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migration34.kt b/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migration34.kt deleted file mode 100644 index 1fb66ace21..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migration34.kt +++ /dev/null @@ -1,113 +0,0 @@ -package com.github.kr328.clash.service.data.migrations - -import android.content.ContentValues -import android.database.sqlite.SQLiteDatabase -import androidx.core.database.getStringOrNull -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import com.github.kr328.clash.common.utils.Log -import com.github.kr328.clash.service.data.ProfileEntity -import com.github.kr328.clash.service.data.SelectedProxyEntity - -object Migration34: Migration(3, 4) { - override fun migrate(database: SupportSQLiteDatabase) { - try { - val profiles = mutableListOf() - try { - database.query("SELECT name, type, uri, source, active, interval, id FROM profiles") - .use { cursor -> - cursor.moveToFirst() - while (!cursor.isAfterLast) { - // old - // name, type, uri, source, active, last_update, update_interval(seconds), id - // new - // name, type, uri, source, active, interval(millis seconds), id - val name = cursor.getString(0) - val type = cursor.getInt(1) - val uri = cursor.getString(2) - val source = cursor.getStringOrNull(3) - val active = cursor.getInt(4) - val interval = cursor.getLong(5) - val id = cursor.getLong(6) - - profiles.add(ProfileEntity(name, type, uri, source, active != 0, interval, id)) - - cursor.moveToNext() - } - } - } - catch (e: Exception) { - Log.w("Query old data failure", e) - } - - val selectedProxies = mutableListOf() - - try { - database.query("SELECT profile_id, proxy, selected FROM selected_proxies") - .use { cursor -> - cursor.moveToFirst() - while (!cursor.isAfterLast) { - // just copy - // profile_id, proxy, selected - val profileId = cursor.getLong(0) - val proxy = cursor.getString(1) - val selected = cursor.getString(2) - - selectedProxies.add(SelectedProxyEntity(profileId, proxy, selected)) - - cursor.moveToNext() - } - } - } - catch (e: Exception) { - Log.w("Query old data failure", e) - } - - // Clean up database - runCatching { - database.execSQL("DROP TABLE IF EXISTS profile_select_proxies") - database.execSQL("DROP TABLE IF EXISTS selected_proxies") - database.execSQL("DROP TABLE IF EXISTS profiles") - database.execSQL("DROP TABLE IF EXISTS _profile_select_proxies") - database.execSQL("DROP TABLE IF EXISTS _selected_proxies") - database.execSQL("DROP TABLE IF EXISTS _profiles") - } - runCatching { - database.execSQL("DROP TABLE IF EXISTS profile_select_proxies") - database.execSQL("DROP TABLE IF EXISTS selected_proxies") - database.execSQL("DROP TABLE IF EXISTS profiles") - database.execSQL("DROP TABLE IF EXISTS _profile_select_proxies") - database.execSQL("DROP TABLE IF EXISTS _selected_proxies") - database.execSQL("DROP TABLE IF EXISTS _profiles") - } - - database.execSQL("CREATE TABLE IF NOT EXISTS `profiles` (`name` TEXT NOT NULL, `type` INTEGER NOT NULL, `uri` TEXT NOT NULL, `source` TEXT, `active` INTEGER NOT NULL, `interval` INTEGER NOT NULL, `id` INTEGER NOT NULL, PRIMARY KEY(`id`))") - database.execSQL("CREATE TABLE IF NOT EXISTS `selected_proxies` (`profile_id` INTEGER NOT NULL, `proxy` TEXT NOT NULL, `selected` TEXT NOT NULL, PRIMARY KEY(`profile_id`, `proxy`), FOREIGN KEY(`profile_id`) REFERENCES `profiles`(`id`) ON UPDATE CASCADE ON DELETE CASCADE )") - - profiles.forEach { - database.insert("profiles", SQLiteDatabase.CONFLICT_ABORT, ContentValues().apply { - put("name", it.name) - put("type", it.type) - put("uri", it.uri) - put("source", it.source) - put("active", it.active) - put("interval", it.interval) - put("id", it.id) - }) - } - - selectedProxies.forEach { - database.insert("selected_proxies", - SQLiteDatabase.CONFLICT_REPLACE, ContentValues().apply { - put("profile_id", it.profileId) - put("proxy", it.proxy) - put("selected", it.selected) - }) - } - - Log.i("Database Migrated 3 -> 4") - } catch (e: Exception) { - Log.e("Migration failure", e) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migrations.kt b/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migrations.kt deleted file mode 100644 index ce53f5d5b0..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/data/migrations/Migrations.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.github.kr328.clash.service.data.migrations - -import androidx.room.migration.Migration - -val MIGRATIONS: Array = arrayOf(Migration12, Migration23, Migration34) \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/files/ProfileDirectoryResolver.kt b/service/src/main/java/com/github/kr328/clash/service/files/ProfileDirectoryResolver.kt deleted file mode 100644 index ecc079d4af..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/files/ProfileDirectoryResolver.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.github.kr328.clash.service.files - -import android.content.Context -import android.os.ParcelFileDescriptor -import android.provider.DocumentsContract -import com.github.kr328.clash.service.R -import com.github.kr328.clash.service.util.resolveBaseDir -import com.github.kr328.clash.service.util.resolveProfileFile -import java.io.FileNotFoundException - -class ProfileDirectoryResolver(private val context: Context) { - companion object { - const val FILE_NAME_CONFIG = "config.yaml" - const val FILE_NAME_PROVIDER = "providers" - } - - private val nextResolver = ProviderResolver(context) - - fun resolve(id: Long, paths: List): VirtualFile { - if (paths.size == 1) { - return object : VirtualFile { - override fun name(): String { - return when (paths[0]) { - FILE_NAME_CONFIG -> - context.getString(R.string.profile_yaml) - FILE_NAME_PROVIDER -> - context.getString(R.string.provider_files) - else -> - throw FileNotFoundException() - } - } - - override fun lastModified(): Long { - return 0 - } - - override fun size(): Long { - return context.resolveProfileFile(id).length() - } - - override fun mimeType(): String { - return when (paths[0]) { - FILE_NAME_CONFIG -> - "text/plain" - FILE_NAME_PROVIDER -> - DocumentsContract.Document.MIME_TYPE_DIR - else -> - throw FileNotFoundException() - } - } - - override fun listFiles(): List { - if (paths[0] == FILE_NAME_PROVIDER) { - return context.resolveBaseDir(id).list()?.toList() ?: emptyList() - } - - return emptyList() - } - - override fun openFile(mode: Int): ParcelFileDescriptor { - return if (paths[0] == FILE_NAME_CONFIG) - ParcelFileDescriptor.open(context.resolveProfileFile(id), mode) - else - throw UnsupportedOperationException() - } - } - } else if (paths.size == 2) { - return nextResolver.resolve(id, paths[1]) - } - - throw FileNotFoundException() - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/files/ProfilesResolver.kt b/service/src/main/java/com/github/kr328/clash/service/files/ProfilesResolver.kt deleted file mode 100644 index 9fb06f2245..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/files/ProfilesResolver.kt +++ /dev/null @@ -1,86 +0,0 @@ -package com.github.kr328.clash.service.files - -import android.content.Context -import android.os.ParcelFileDescriptor -import android.provider.DocumentsContract -import com.github.kr328.clash.service.R -import com.github.kr328.clash.service.data.ProfileDao -import com.github.kr328.clash.service.util.resolveProfileFile -import java.io.FileNotFoundException - -class ProfilesResolver(private val context: Context) { - private val nextResolver = ProfileDirectoryResolver(context) - - suspend fun resolve(paths: List): VirtualFile { - return when (paths.size) { - 0 -> { - val files = ProfileDao.queryAllIds().map(Long::toString) - - object : VirtualFile { - override fun name(): String { - return context.getString(R.string.clash_for_android) - } - - override fun lastModified(): Long { - return 0 - } - - override fun size(): Long { - return 0 - } - - override fun mimeType(): String { - return DocumentsContract.Document.MIME_TYPE_DIR - } - - override fun listFiles(): List { - return files - } - - override fun openFile(mode: Int): ParcelFileDescriptor { - throw UnsupportedOperationException() - } - } - } - 1 -> { - val profile = paths[0].toLongOrNull()?.let { - ProfileDao.queryById(it) - } ?: throw FileNotFoundException() - - object : VirtualFile { - override fun name(): String { - return profile.name - } - - override fun lastModified(): Long { - return context.resolveProfileFile(profile.id).lastModified() - } - - override fun size(): Long { - return 0 - } - - override fun mimeType(): String { - return DocumentsContract.Document.MIME_TYPE_DIR - } - - override fun listFiles(): List { - return listOf( - ProfileDirectoryResolver.FILE_NAME_CONFIG, - ProfileDirectoryResolver.FILE_NAME_PROVIDER - ) - } - - override fun openFile(mode: Int): ParcelFileDescriptor { - throw UnsupportedOperationException() - } - } - } - else -> { - val id = paths[0].toLongOrNull() ?: throw FileNotFoundException() - - nextResolver.resolve(id, paths.subList(1, paths.size)) - } - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/files/ProviderResolver.kt b/service/src/main/java/com/github/kr328/clash/service/files/ProviderResolver.kt deleted file mode 100644 index 55a4f005ea..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/files/ProviderResolver.kt +++ /dev/null @@ -1,41 +0,0 @@ -package com.github.kr328.clash.service.files - -import android.content.Context -import android.os.ParcelFileDescriptor -import com.github.kr328.clash.service.util.resolveBaseDir -import java.io.FileNotFoundException -import java.net.URLDecoder - -class ProviderResolver(private val context: Context) { - fun resolve(id: Long, fileName: String): VirtualFile { - val file = context.resolveBaseDir(id).resolve(fileName) - if (!file.exists()) - throw FileNotFoundException() - - return object : VirtualFile { - override fun name(): String { - return URLDecoder.decode(file.name, "utf-8") - } - - override fun lastModified(): Long { - return file.lastModified() - } - - override fun size(): Long { - return file.length() - } - - override fun mimeType(): String { - return "text/plain" - } - - override fun listFiles(): List { - return emptyList() - } - - override fun openFile(mode: Int): ParcelFileDescriptor { - return ParcelFileDescriptor.open(file, mode) - } - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/files/VirtualFile.kt b/service/src/main/java/com/github/kr328/clash/service/files/VirtualFile.kt deleted file mode 100644 index 688174f50b..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/files/VirtualFile.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.kr328.clash.service.files - -import android.os.ParcelFileDescriptor - -interface VirtualFile { - fun name(): String - fun lastModified(): Long - fun size(): Long - fun mimeType(): String - fun listFiles(): List - fun openFile(mode: Int): ParcelFileDescriptor -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/model/Converters.kt b/service/src/main/java/com/github/kr328/clash/service/model/Converters.kt deleted file mode 100644 index 5e9c918bfc..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/model/Converters.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.github.kr328.clash.service.model - -import android.content.Context -import android.net.Uri -import com.github.kr328.clash.service.data.ProfileEntity -import com.github.kr328.clash.service.util.resolveProfileFile - -fun ProfileEntity.asProfile(context: Context): Profile { - val type = when (this.type) { - ProfileEntity.TYPE_FILE -> Profile.Type.FILE - ProfileEntity.TYPE_URL -> Profile.Type.URL - ProfileEntity.TYPE_EXTERNAL -> Profile.Type.EXTERNAL - else -> Profile.Type.UNKNOWN - } - val lastModified = context.resolveProfileFile(id).lastModified() - - return Profile( - id = id, - name = name, - type = type, - uri = Uri.parse(uri), - source = source, - active = active, - interval = interval, - lastModified = lastModified - ) -} - -fun Profile.asEntity(): ProfileEntity { - val type = when (this.type) { - Profile.Type.FILE -> ProfileEntity.TYPE_FILE - Profile.Type.URL -> ProfileEntity.TYPE_URL - Profile.Type.EXTERNAL -> ProfileEntity.TYPE_EXTERNAL - Profile.Type.UNKNOWN -> ProfileEntity.TYPE_UNKNOWN - } - - return ProfileEntity( - name = name, - type = type, - uri = uri.toString(), - source = source, - active = active, - interval = interval, - id = id - ) -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/model/Profile.kt b/service/src/main/java/com/github/kr328/clash/service/model/Profile.kt deleted file mode 100644 index 0f8ab8054b..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/model/Profile.kt +++ /dev/null @@ -1,44 +0,0 @@ -@file:UseSerializers(UriSerializer::class) - -package com.github.kr328.clash.service.model - -import android.net.Uri -import android.os.Parcel -import android.os.Parcelable -import com.github.kr328.clash.common.serialization.Parcels -import kotlinx.serialization.Serializable -import kotlinx.serialization.UseSerializers - -@Serializable -data class Profile( - val id: Long, - val name: String, - val type: Type, - val uri: Uri, - val source: String?, - val active: Boolean, - val interval: Long, - val lastModified: Long -) : Parcelable { - enum class Type { - FILE, URL, EXTERNAL, UNKNOWN - } - - override fun writeToParcel(parcel: Parcel, flags: Int) { - Parcels.dump(serializer(), this, parcel) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): Profile { - return Parcels.load(serializer(), parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/model/Serializers.kt b/service/src/main/java/com/github/kr328/clash/service/model/Serializers.kt deleted file mode 100644 index cafebb315e..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/model/Serializers.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.kr328.clash.service.model - -import android.net.Uri -import kotlinx.serialization.* - -class UriSerializer : KSerializer { - override val descriptor: SerialDescriptor - get() = PrimitiveDescriptor("Uri", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): Uri { - return Uri.parse(decoder.decodeString()) - } - - override fun serialize(encoder: Encoder, value: Uri) { - encoder.encodeString(value.toString()) - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/settings/ServiceSettings.kt b/service/src/main/java/com/github/kr328/clash/service/settings/ServiceSettings.kt deleted file mode 100644 index b194879cd8..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/settings/ServiceSettings.kt +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.kr328.clash.service.settings - -import android.content.Context -import android.content.SharedPreferences -import com.github.kr328.clash.common.settings.BaseSettings -import com.github.kr328.clash.service.ServiceSettingsProvider - -class ServiceSettings(preference: SharedPreferences) : - BaseSettings(preference) { - 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" - const val ACCESS_CONTROL_MODE_WHITELIST = "access_control_mode_whitelist" - - val ENABLE_VPN = - BooleanEntry("enable_vpn", true) - val LANGUAGE = - StringEntry("language", "") - val BYPASS_PRIVATE_NETWORK = - BooleanEntry("bypass_private_network", true) - val ACCESS_CONTROL_MODE = - StringEntry("access_control_mode", ACCESS_CONTROL_MODE_ALL) - val ACCESS_CONTROL_PACKAGES = - StringSetEntry("access_control_packages", emptySet()) - val DNS_HIJACKING = - BooleanEntry("dns_hijacking", true) - val NOTIFICATION_REFRESH = - BooleanEntry("notification_refresh", true) - val AUTO_ADD_SYSTEM_DNS = - BooleanEntry("auto_add_system_dns", true) - val OVERRIDE_DNS = - BooleanEntry("override_dns", true) - } -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/transact/ParcelableContainer.kt b/service/src/main/java/com/github/kr328/clash/service/transact/ParcelableContainer.kt deleted file mode 100644 index 712575a9f1..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/transact/ParcelableContainer.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.kr328.clash.service.transact - -import android.os.Parcel -import android.os.Parcelable - -data class ParcelableContainer(val data: Parcelable?) : Parcelable { - constructor(parcel: Parcel) : - this(parcel.readParcelable(ParcelableContainer::class.java.classLoader)) - - override fun writeToParcel(parcel: Parcel, flags: Int) { - parcel.writeParcelable(data, 0) - } - - override fun describeContents(): Int { - return 0 - } - - companion object CREATOR : Parcelable.Creator { - override fun createFromParcel(parcel: Parcel): ParcelableContainer { - return ParcelableContainer(parcel) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) - } - } - -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt deleted file mode 100644 index 34e000f18a..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/util/BroadcastUtils.kt +++ /dev/null @@ -1,42 +0,0 @@ -package com.github.kr328.clash.service.util - -import android.content.Context -import android.content.Intent -import com.github.kr328.clash.common.Permissions -import com.github.kr328.clash.common.ids.Intents - -fun Context.sendBroadcastSelf(intent: Intent) { - this.sendBroadcast( - intent.setPackage(this.packageName), - Permissions.PERMISSION_RECEIVE_BROADCASTS - ) -} - -fun Context.broadcastProfileChanged() { - val intent = Intent(Intents.INTENT_ACTION_PROFILE_CHANGED) - - this.sendBroadcastSelf(intent) -} - -fun Context.broadcastProfileLoaded() { - val intent = Intent(Intents.INTENT_ACTION_PROFILE_LOADED) - - this.sendBroadcastSelf(intent) -} - -fun Context.broadcastNetworkChanged() { - this.sendBroadcastSelf(Intent(Intents.INTENT_ACTION_NETWORK_CHANGED)) -} - -fun Context.broadcastClashStarted() { - this.sendBroadcastSelf(Intent(Intents.INTENT_ACTION_CLASH_STARTED)) -} - -fun Context.broadcastClashStopped(reason: String?) { - this.sendBroadcastSelf( - Intent(Intents.INTENT_ACTION_CLASH_STOPPED).putExtra( - Intents.INTENT_EXTRA_CLASH_STOP_REASON, - reason - ) - ) -} diff --git a/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt deleted file mode 100644 index 3d5129c42d..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/util/FileUtils.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.kr328.clash.service.util - -import android.content.Context -import com.github.kr328.clash.service.Constants -import java.io.File - -fun Context.resolveProfileFile(id: Long): File { - return filesDir.resolve(Constants.PROFILES_DIR).resolve("$id.yaml") -} - -fun Context.resolveBaseDir(id: Long): File { - return filesDir.resolve(Constants.CLASH_DIR).resolve(id.toString()) -} - -fun Context.resolveTempProfileFile(id: Long): File { - return cacheDir.resolve(Constants.PROFILES_DIR).resolve("$id.yaml") -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/InetAddressUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/InetAddressUtils.kt deleted file mode 100644 index d1c54c561e..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/util/InetAddressUtils.kt +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.kr328.clash.service.util - -import java.net.Inet4Address -import java.net.Inet6Address -import java.net.InetAddress - -fun InetAddress.asSocketAddressText(port: Int): String { - return when (this) { - is Inet6Address -> - "[${numericToTextFormat(this.address)}]:$port" - is Inet4Address -> - "${this.hostAddress}:$port" - else -> throw IllegalArgumentException("Unsupported Inet type ${this.javaClass}") - } -} - -private const val INT16SZ = 2 -private const val INADDRSZ = 16 -private fun numericToTextFormat(src: ByteArray): String { - val sb = StringBuilder(39) - for (i in 0 until INADDRSZ / INT16SZ) { - sb.append( - Integer.toHexString( - src[i shl 1].toInt() shl 8 and 0xff00 - or (src[(i shl 1) + 1].toInt() and 0xff) - ) - ) - if (i < INADDRSZ / INT16SZ - 1) { - sb.append(":") - } - } - return sb.toString() -} - diff --git a/service/src/main/java/com/github/kr328/clash/service/util/Net.kt b/service/src/main/java/com/github/kr328/clash/service/util/Net.kt deleted file mode 100644 index 292bc64012..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/util/Net.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.github.kr328.clash.service.util - -import java.net.InetAddress - -data class IPNet(val ip: InetAddress, val prefix: Int) - -fun parseCIDR(cidr: String): IPNet { - val s = cidr.split("/", limit = 2) - - if (s.size != 2) - throw IllegalArgumentException("Invalid address") - - val address = InetAddress.getByName(s[0]) - val prefix = s[1].toInt() - - return IPNet(address, prefix) -} \ No newline at end of file diff --git a/service/src/main/java/com/github/kr328/clash/service/util/ServiceUtils.kt b/service/src/main/java/com/github/kr328/clash/service/util/ServiceUtils.kt deleted file mode 100644 index 0bcc05bcd5..0000000000 --- a/service/src/main/java/com/github/kr328/clash/service/util/ServiceUtils.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.github.kr328.clash.service.util - -import android.content.Context -import android.content.Intent -import android.net.VpnService -import com.github.kr328.clash.common.ids.Intents -import com.github.kr328.clash.common.utils.intent -import com.github.kr328.clash.common.utils.startForegroundServiceCompat -import com.github.kr328.clash.service.ClashService -import com.github.kr328.clash.service.TunService -import com.github.kr328.clash.service.settings.ServiceSettings - -fun Context.startClashService(): Intent? { - val startTun = ServiceSettings(this).get(ServiceSettings.ENABLE_VPN) - - if (startTun) { - val vpnRequest = VpnService.prepare(this) - if (vpnRequest != null) - return vpnRequest - - startForegroundServiceCompat(TunService::class.intent) - } else { - startForegroundServiceCompat(ClashService::class.intent) - } - - return null -} - -fun Context.stopClashService() { - sendBroadcastSelf(Intent(Intents.INTENT_ACTION_CLASH_REQUEST_STOP)) -} \ No newline at end of file diff --git a/service/src/main/res/drawable/ic_icon.xml b/service/src/main/res/drawable/ic_icon.xml deleted file mode 100644 index e20fae3d40..0000000000 --- a/service/src/main/res/drawable/ic_icon.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/service/src/main/res/drawable/ic_notification.xml b/service/src/main/res/drawable/ic_notification.xml deleted file mode 100644 index 44b4fb0e47..0000000000 --- a/service/src/main/res/drawable/ic_notification.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - diff --git a/service/src/main/res/values-zh/strings.xml b/service/src/main/res/values-zh/strings.xml deleted file mode 100644 index a1c455536f..0000000000 --- a/service/src/main/res/values-zh/strings.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - Clash 状态 - 配置服务状态 - 配置处理状态 - 正在运行 - %d 个项目在队列中 - 更新 %s 完成 - 更新 %s 失败 - 处理结果 - 正在处理配置文件 - 正在回收资源 - 销毁中 - Clash for Android - 配置文件和外部引用 - 配置文件.yaml - 外部引用文件 - 等待中 - 载入中 - \ No newline at end of file diff --git a/service/src/main/res/values/arrays.xml b/service/src/main/res/values/arrays.xml deleted file mode 100644 index 41432e62f7..0000000000 --- a/service/src/main/res/values/arrays.xml +++ /dev/null @@ -1,84 +0,0 @@ - - - - - 1.0.0.0/8 - 2.0.0.0/7 - 4.0.0.0/6 - 8.0.0.0/7 - 11.0.0.0/8 - 12.0.0.0/6 - 16.0.0.0/4 - 32.0.0.0/3 - 64.0.0.0/3 - 96.0.0.0/4 - 112.0.0.0/5 - 120.0.0.0/6 - 124.0.0.0/7 - 126.0.0.0/8 - 128.0.0.0/3 - 160.0.0.0/5 - 168.0.0.0/8 - 169.0.0.0/9 - 169.128.0.0/10 - 169.192.0.0/11 - 169.224.0.0/12 - 169.240.0.0/13 - 169.248.0.0/14 - 169.252.0.0/15 - 169.255.0.0/16 - 170.0.0.0/7 - 172.0.0.0/12 - 172.32.0.0/11 - 172.64.0.0/10 - 172.128.0.0/9 - 173.0.0.0/8 - 174.0.0.0/7 - 176.0.0.0/4 - 192.0.0.0/9 - 192.128.0.0/11 - 192.160.0.0/13 - 192.169.0.0/16 - 192.170.0.0/15 - 192.172.0.0/14 - 192.176.0.0/12 - 192.192.0.0/10 - 193.0.0.0/8 - 194.0.0.0/7 - 196.0.0.0/6 - 200.0.0.0/5 - 208.0.0.0/4 - 224.0.0.0/3 - 172.31.255.252/30 - - - - 1.0.0.0/8 - 2.0.0.0/7 - 4.0.0.0/6 - 8.0.0.0/5 - 16.0.0.0/4 - 32.0.0.0/3 - 64.0.0.0/3 - 96.0.0.0/4 - 112.0.0.0/5 - 120.0.0.0/6 - 124.0.0.0/7 - 126.0.0.0/8 - 128.0.0.0/3 - 160.0.0.0/5 - 168.0.0.0/8 - 169.0.0.0/9 - 169.128.0.0/10 - 169.192.0.0/11 - 169.224.0.0/12 - 169.240.0.0/13 - 169.248.0.0/14 - 169.252.0.0/15 - 169.255.0.0/16 - 170.0.0.0/7 - 172.0.0.0/6 - 176.0.0.0/4 - 192.0.0.0/2 - - diff --git a/service/src/main/res/values/colors.xml b/service/src/main/res/values/colors.xml deleted file mode 100644 index 6aaac4a329..0000000000 --- a/service/src/main/res/values/colors.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #1E4376 - \ No newline at end of file diff --git a/service/src/main/res/values/strings.xml b/service/src/main/res/values/strings.xml deleted file mode 100644 index 498f793137..0000000000 --- a/service/src/main/res/values/strings.xml +++ /dev/null @@ -1,22 +0,0 @@ - - - "%1$s↑\t%2$s↓" - - Clash Status - Profile Service Status - Processing Profiles - %d items in queue - Waiting - Process Result - Update %s Completed - Update %s Failure - Profile Processing Status - Running - Loading - Destroying - Recycling resources - Clash for Android - Profiles and Providers - Profile.yaml - Provider Files - diff --git a/service/src/main/res/xml/profile_provider.xml b/service/src/main/res/xml/profile_provider.xml deleted file mode 100644 index 588cf65a6d..0000000000 --- a/service/src/main/res/xml/profile_provider.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts deleted file mode 100644 index b47463e000..0000000000 --- a/settings.gradle.kts +++ /dev/null @@ -1,7 +0,0 @@ -include(":app") -include(":core") -include(":service") -include(":design") -include(":common") - -rootProject.name = "ClashForAndroid" From f0ec5d353c77f538fd0dd726a93a5b75af6fb2ce Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 21 Jun 2020 19:51:58 +0800 Subject: [PATCH 357/358] clean up repo --- .gitignore | 47 ----------------------------------------------- .gitmodules | 3 --- 2 files changed, 50 deletions(-) delete mode 100644 .gitignore delete mode 100644 .gitmodules diff --git a/.gitignore b/.gitignore deleted file mode 100644 index d1eb60c753..0000000000 --- a/.gitignore +++ /dev/null @@ -1,47 +0,0 @@ -.gradle -build/ -/app/release/ -/captures - -# Ignore Gradle GUI config -gradle-app.setting - -# Avoid ignoring Gradle wrapper jar targetFile (.jar files are usually ignored) -!gradle-wrapper.jar - -# Cache of project -.gradletasknamecache - -# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 -# gradle/wrapper/gradle-wrapper.properties - -# Ignore IDEA config -.idea -*.iml - -# KeyStore -*.keystore -*.jks - -# clion cmake build -cmake-build-* - -# local.properties -local.properties - -# keystore -keystore.properties - -# vscode -.vscode - -# cxx -.cxx - -*.hprof - -# firebase -google-services.json - -# Dolphin -.directory diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index acb07f96f1..0000000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "core/src/main/golang/clash"] - path = core/src/main/golang/clash - url = https://github.com/Kr328/Clash From de8d6c09453684756214ec3125e78fbcded2b62d Mon Sep 17 00:00:00 2001 From: Kr328 <39107975+Kr328@users.noreply.github.com> Date: Sun, 21 Jun 2020 19:55:16 +0800 Subject: [PATCH 358/358] update clash license --- NOTICE | 675 +-------------------------------------------------------- 1 file changed, 1 insertion(+), 674 deletions(-) diff --git a/NOTICE b/NOTICE index 6853d8820c..fffadab100 100644 --- a/NOTICE +++ b/NOTICE @@ -2,680 +2,7 @@ * Clash ========================================================================== - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. + closed-source assurance * Android Open Source Project * Android X Support Library