Add CI/CD configuration and API documentation

This commit is contained in:
2026-07-01 21:40:53 +08:00
commit c590135d68
4168 changed files with 740252 additions and 0 deletions
@@ -0,0 +1,302 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.github.kr328.clash">
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<!-- 网络权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<application
android:name=".MainApplication"
android:allowBackup="true"
android:usesCleartextTraffic="true"
android:banner="@mipmap/ic_launcher"
android:fullBackupContent="@xml/full_backup_content"
android:icon="@mipmap/ic_launcher"
android:label="@string/application_name"
android:networkSecurityConfig="@xml/network_security_config"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/BootstrapTheme"
tools:ignore="GoogleAppIndexingWarning"
tools:targetApi="N">
<meta-data
android:name="releaseName"
android:value="@string/release_name" />
<meta-data
android:name="releaseCode"
android:value="@integer/release_code" />
<activity
android:name=".MainActivity"
android:configChanges="uiMode"
android:exported="true"
android:label="@string/launch_name"
android:launchMode="singleTop" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE_PREFERENCES" />
</intent-filter>
</activity>
<activity
android:name=".ExternalControlActivity"
android:exported="true"
android:label="@string/external_control_activity"
android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="clash-----------"/>
<data android:scheme="clashmeta-----------"/>
<data android:host="install-config-----------"/>
</intent-filter>
<intent-filter>
<action android:name="com.github.metacubex.clash.meta.action.START_CLASH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.github.metacubex.clash.meta.action.STOP_CLASH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="com.github.metacubex.clash.meta.action.TOGGLE_CLASH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity
android:name=".ApkBrokenActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/application_broken" />
<activity
android:name=".AppCrashedActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/application_crashed"
android:launchMode="singleTask" />
<activity
android:name=".ProfilesActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/profiles" />
<activity
android:name=".NewProfileActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/create_profile" />
<activity
android:name=".PropertiesActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/profile" />
<activity
android:name=".ProxyActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/proxy" />
<activity
android:name=".ProvidersActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/providers" />
<activity
android:name=".LogsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/logs" />
<activity
android:name=".LogcatActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/logcat" />
<activity
android:name=".SettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/settings" />
<activity
android:name=".NetworkSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/network" />
<activity
android:name=".AppSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/app" />
<activity
android:name=".OverrideSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/override" />
<activity
android:name=".MetaFeatureSettingsActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/meta_features" />
<activity
android:name=".AccessControlActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/access_control_packages" />
<activity
android:name=".HelpActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/help" />
<activity
android:name=".FilesActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="@string/files" />
<activity
android:name=".ProfileActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="设置中心"
/>
<activity
android:name=".LoginActivity"
android:configChanges="uiMode"
android:exported="false"
/>
<activity
android:name=".SplashActivity"
android:configChanges="uiMode"
android:exported="true" />
<activity
android:name=".PlansActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="购买订阅"
/>
<activity
android:name=".MyOrdersActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="我的订单"
/>
<activity
android:name=".ConfigOrderActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="配置订阅"
/>
<activity
android:name=".SubmitOrderActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="订单详情"
/>
<activity
android:name=".GongdanActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="我的工单"
/>
<activity
android:name=".NewGongdanActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="新建工单"
/>
<activity
android:name=".H5WebActivity"
android:configChanges="uiMode"
android:exported="false"
/>
<activity
android:name=".TicketDetailActivity"
android:configChanges="uiMode"
android:exported="false"
android:label=""
/>
<activity
android:name=".RegisterActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="注册"
/>
<activity
android:name=".InvitationActivity"
android:configChanges="uiMode"
android:exported="false"
android:label="我的邀请"
/>
<service
android:name=".LogcatService"
android:exported="false"
android:label="@string/clash_logcat" />
<service
android:name=".TileService"
android:exported="true"
android:icon="@drawable/ic_logo_service"
android:foregroundServiceType="connectedDevice"
android:label="@string/launch_name"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
<intent-filter>
<action android:name="android.service.quicksettings.action.QS_TILE" />
</intent-filter>
</service>
<receiver
android:name=".RestartReceiver"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
</intent-filter>
</receiver>
</application>
</manifest>
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":120,"w":150,"h":150,"nm":"Disconnected State","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-10.788],[3.663,-3.663],[5.179,0],[3.663,3.663],[0,5.179],[0,0]],"o":[[0,0],[0,5.179],[-3.663,3.663],[-5.18,0],[-3.663,-3.663],[0,-10.788],[0,0]],"v":[[13.281,-17.188],[19.531,-2.344],[13.811,11.467],[0.001,17.188],[-13.811,11.467],[-19.531,-2.344],[-13.281,-17.188]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.001,-24.874],[0.001,-4.563]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.6,0.6,0.6,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.5,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[25,27.344],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Solid Background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":0,"k":[130,130,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.866666666667,0.866666666667,0.866666666667,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Ripple","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[130,130,100]},{"t":60,"s":[197.368,197.368,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.866666666667,0.866666666667,0.866666666667,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":120,"w":150,"h":150,"nm":"Connected State","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-10.788],[3.663,-3.663],[5.179,0],[3.663,3.663],[0,5.179],[0,0]],"o":[[0,0],[0,5.179],[-3.663,3.663],[-5.18,0],[-3.663,-3.663],[0,-10.788],[0,0]],"v":[[13.281,-17.188],[19.531,-2.344],[13.811,11.467],[0.001,17.188],[-13.811,11.467],[-19.531,-2.344],[-13.281,-17.188]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.001,-24.874],[0.001,-4.563]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.5,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[25,27.344],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Solid Background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":0,"k":[130,130,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.360784313725,0.725490196078,0.2,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Ripple","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[130,130,100]},{"t":60,"s":[197.368,197.368,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[0.360151014141,0.725490196078,0.199154169419,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"4.8.0","meta":{"g":"LottieFiles AE 3.1.1","a":"","k":"","d":"","tc":""},"fr":60,"ip":0,"op":120,"w":150,"h":150,"nm":"Connecting State","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Icon","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[25,25,0],"ix":1},"s":{"a":0,"k":[80,80,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,-10.788],[3.663,-3.663],[5.179,0],[3.663,3.663],[0,5.179],[0,0]],"o":[[0,0],[0,5.179],[-3.663,3.663],[-5.18,0],[-3.663,-3.663],[0,-10.788],[0,0]],"v":[[13.281,-17.188],[19.531,-2.344],[13.811,11.467],[0.001,17.188],[-13.811,11.467],[-19.531,-2.344],[-13.281,-17.188]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[0.001,-24.874],[0.001,-4.563]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4.5,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[25,27.344],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Progress","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":0,"k":[110,110,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[99.99]},{"t":80,"s":[0]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":0,"s":[100]},{"t":119,"s":[0.01]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":3.5,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Solid Background","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":0,"k":[130,130,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.494117676978,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Ripple","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":0,"s":[80]},{"t":60,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[75,75,0],"ix":2},"a":{"a":0,"k":[22,20,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.833,0.833,0.833],"y":[0.833,0.833,0.833]},"o":{"x":[0.167,0.167,0.167],"y":[0.167,0.167,0.167]},"t":0,"s":[130,130,100]},{"t":60,"s":[197.368,197.368,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[76,76],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.494117647059,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[22,20],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":120,"st":0,"bm":0}],"markers":[]}
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
{"v":"5.9.3","fr":29.9700012207031,"ip":0,"op":60.0000024438501,"w":1920,"h":1080,"nm":"1_Loading_Line","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Blue Line","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[960,539,0],"ix":2,"l":2},"a":{"a":0,"k":[0,-1,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[800,800],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"st","c":{"a":0,"k":[0.212477396049,0.540471813725,0.898039215686,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":80,"ix":5},"lc":2,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,-1],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.9],"y":[0]},"t":15,"s":[0]},{"t":60.0000024438501,"s":[99.9]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.1],"y":[1]},"o":{"x":[0.9],"y":[0]},"t":0,"s":[0.01]},{"t":45.0000018328876,"s":[100]}],"ix":2},"o":{"a":0,"k":-1,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"ct":1,"bm":0}],"markers":[{"tm":60.0000024438501,"cm":"END (No Loop)","dr":0}]}
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,137 @@
package com.github.kr328.clash
import android.Manifest.permission.INTERNET
import android.content.ClipData
import android.content.ClipboardManager
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.core.content.getSystemService
import com.github.kr328.clash.design.AccessControlDesign
import com.github.kr328.clash.design.model.AppInfo
import com.github.kr328.clash.design.util.toAppInfo
import com.github.kr328.clash.service.store.ServiceStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
class AccessControlActivity : BaseActivity<AccessControlDesign>() {
override suspend fun main() {
val service = ServiceStore(this)
val selected = withContext(Dispatchers.IO) {
service.accessControlPackages.toMutableSet()
}
defer {
withContext(Dispatchers.IO) {
service.accessControlPackages = selected
}
}
val design = AccessControlDesign(this, uiStore, selected)
setContentDesign(design)
design.requests.send(AccessControlDesign.Request.ReloadApps)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
AccessControlDesign.Request.ReloadApps -> {
design.patchApps(loadApps(selected))
}
AccessControlDesign.Request.SelectAll -> {
val all = withContext(Dispatchers.Default) {
design.apps.map(AppInfo::packageName)
}
selected.clear()
selected.addAll(all)
design.rebindAll()
}
AccessControlDesign.Request.SelectNone -> {
selected.clear()
design.rebindAll()
}
AccessControlDesign.Request.SelectInvert -> {
val all = withContext(Dispatchers.Default) {
design.apps.map(AppInfo::packageName).toSet() - selected
}
selected.clear()
selected.addAll(all)
design.rebindAll()
}
AccessControlDesign.Request.Import -> {
val clipboard = getSystemService<ClipboardManager>()
val data = clipboard?.primaryClip
if (data != null && data.itemCount > 0) {
val packages = data.getItemAt(0).text.split("\n").toSet()
val all = design.apps.map(AppInfo::packageName).intersect(packages)
selected.clear()
selected.addAll(all)
}
design.rebindAll()
}
AccessControlDesign.Request.Export -> {
val clipboard = getSystemService<ClipboardManager>()
val data = ClipData.newPlainText(
"packages",
selected.joinToString("\n")
)
clipboard?.setPrimaryClip(data)
}
}
}
}
}
}
private suspend fun loadApps(selected: Set<String>): List<AppInfo> =
withContext(Dispatchers.IO) {
val reverse = uiStore.accessControlReverse
val sort = uiStore.accessControlSort
val systemApp = uiStore.accessControlSystemApp
val base = compareByDescending<AppInfo> { it.packageName in selected }
val comparator = if (reverse) base.thenDescending(sort) else base.then(sort)
val pm = packageManager
val packages = pm.getInstalledPackages(PackageManager.GET_PERMISSIONS)
packages.asSequence()
.filter {
it.packageName != packageName
}
.filter {
it.packageName == "android" || it.requestedPermissions?.contains(INTERNET) == true
}
.filter {
systemApp || !it.isSystemApp
}
.map {
it.toAppInfo(pm)
}
.sortedWith(comparator)
.toList()
}
private val PackageInfo.isSystemApp: Boolean
get() {
return applicationInfo.flags and ApplicationInfo.FLAG_SYSTEM != 0
}
}
@@ -0,0 +1,20 @@
package com.github.kr328.clash
import android.content.Intent
import android.net.Uri
import com.github.kr328.clash.design.ApkBrokenDesign
import kotlinx.coroutines.isActive
class ApkBrokenActivity : BaseActivity<ApkBrokenDesign>() {
override suspend fun main() {
val design = ApkBrokenDesign(this)
setContentDesign(design)
while (isActive) {
val req = design.requests.receive()
startActivity(Intent(Intent.ACTION_VIEW).setData(Uri.parse(req.url)))
}
}
}
@@ -0,0 +1,33 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.compat.versionCodeCompat
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.design.AppCrashedDesign
import com.github.kr328.clash.log.SystemLogcat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
class AppCrashedActivity : BaseActivity<AppCrashedDesign>() {
override suspend fun main() {
val design = AppCrashedDesign(this)
setContentDesign(design)
val packageInfo = withContext(Dispatchers.IO) {
packageManager.getPackageInfo(packageName, 0)
}
Log.i("App version: versionName = ${packageInfo.versionName} versionCode = ${packageInfo.versionCodeCompat}")
val logs = withContext(Dispatchers.IO) {
SystemLogcat.dumpCrash()
}
design.setAppLogs(logs)
while (isActive) {
events.receive()
}
}
}
@@ -0,0 +1,62 @@
package com.github.kr328.clash
import android.content.pm.PackageManager
import com.github.kr328.clash.common.util.componentName
import com.github.kr328.clash.design.AppSettingsDesign
import com.github.kr328.clash.design.model.Behavior
import com.github.kr328.clash.service.store.ServiceStore
import com.github.kr328.clash.util.ApplicationObserver
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class AppSettingsActivity : BaseActivity<AppSettingsDesign>(), Behavior {
override suspend fun main() {
val design = AppSettingsDesign(
this,
uiStore,
ServiceStore(this),
this,
clashRunning,
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ClashStart, Event.ClashStop, Event.ServiceRecreated ->
recreate()
else -> Unit
}
}
design.requests.onReceive {
ApplicationObserver.createdActivities.forEach {
it.recreate()
}
}
}
}
}
override var autoRestart: Boolean
get() {
val status = packageManager.getComponentEnabledSetting(
RestartReceiver::class.componentName
)
return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
}
set(value) {
val status = if (value)
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
packageManager.setComponentEnabledSetting(
RestartReceiver::class.componentName,
status,
PackageManager.DONT_KILL_APP,
)
}
}
@@ -0,0 +1,229 @@
package com.github.kr328.clash
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContract
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
import com.github.kr328.clash.core.bridge.ClashException
import com.github.kr328.clash.design.Design
import com.github.kr328.clash.design.model.DarkMode
import com.github.kr328.clash.design.store.UiStore
import com.github.kr328.clash.design.ui.DayNight
import com.github.kr328.clash.design.util.resolveThemedBoolean
import com.github.kr328.clash.design.util.resolveThemedColor
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.remote.Broadcasts
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.util.ActivityResultLifecycle
import com.github.kr328.clash.util.ApplicationObserver
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
abstract class BaseActivity<D : Design<*>> : AppCompatActivity(),
CoroutineScope by MainScope(),
Broadcasts.Observer {
protected val uiStore by lazy { UiStore(this) }
protected val events = Channel<Event>(Channel.UNLIMITED)
protected var activityStarted: Boolean = false
protected val clashRunning: Boolean
get() = Remote.broadcasts.clashRunning
protected var design: D? = null
set(value) {
field = value
if (value != null) {
setContentView(value.root)
} else {
setContentView(View(this))
}
}
private var defer: suspend () -> Unit = {}
private var deferRunning = false
private val nextRequestKey = AtomicInteger(0)
private var dayNight: DayNight = DayNight.Day
protected abstract suspend fun main()
fun defer(operation: suspend () -> Unit) {
this.defer = operation
}
suspend fun <I, O> startActivityForResult(
contracts: ActivityResultContract<I, O>,
input: I,
): O = withContext(Dispatchers.Main) {
val requestKey = nextRequestKey.getAndIncrement().toString()
ActivityResultLifecycle().use { lifecycle, start ->
suspendCoroutine { c ->
activityResultRegistry.register(requestKey, lifecycle, contracts) {
c.resume(it)
}.apply { start() }.launch(input)
}
}
}
suspend fun setContentDesign(design: D) {
suspendCoroutine<Unit> {
window.decorView.post {
this.design = design
it.resume(Unit)
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
applyDayNight()
launch {
main()
}
}
override fun onStart() {
super.onStart()
activityStarted = true
Remote.broadcasts.addObserver(this)
events.trySend(Event.ActivityStart)
}
override fun onStop() {
super.onStop()
activityStarted = false
Remote.broadcasts.removeObserver(this)
events.trySend(Event.ActivityStop)
}
override fun onDestroy() {
design?.cancel()
cancel()
super.onDestroy()
}
override fun finish() {
if (deferRunning) return
deferRunning = true
launch {
try {
defer()
} finally {
withContext(NonCancellable) {
super.finish()
}
}
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
if (queryDayNight(newConfig) != dayNight) {
ApplicationObserver.createdActivities.forEach {
it.recreate()
}
}
}
open fun shouldDisplayHomeAsUpEnabled(): Boolean {
return true
}
override fun onSupportNavigateUp(): Boolean {
this.onBackPressed()
return true
}
override fun onProfileChanged() {
events.trySend(Event.ProfileChanged)
}
override fun onProfileUpdateCompleted(uuid: UUID?) {
events.trySend(Event.ProfileUpdateCompleted)
}
override fun onProfileUpdateFailed(uuid: UUID?, reason: String?) {
events.trySend(Event.ProfileUpdateFailed)
}
override fun onProfileLoaded() {
events.trySend(Event.ProfileLoaded)
}
override fun onServiceRecreated() {
events.trySend(Event.ServiceRecreated)
}
override fun onStarted() {
events.trySend(Event.ClashStart)
}
override fun onStopped(cause: String?) {
events.trySend(Event.ClashStop)
if (cause != null && activityStarted) {
launch {
design?.showExceptionToast(ClashException(cause))
}
}
}
private fun queryDayNight(config: Configuration = resources.configuration): DayNight {
return when (uiStore.darkMode) {
DarkMode.Auto -> if (config.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) DayNight.Night else DayNight.Day
DarkMode.ForceLight -> DayNight.Day
DarkMode.ForceDark -> DayNight.Night
}
}
private fun applyDayNight(config: Configuration = resources.configuration) {
// val dayNight = theme.applyStyle(R.style.AppThemeLight, true) //默认白天模式
/*
val dayNight = queryDayNight(config)
when (dayNight) {
DayNight.Night -> theme.applyStyle(R.style.AppThemeDark, true)
DayNight.Day -> theme.applyStyle(R.style.AppThemeLight, true)
}*/
window.isAllowForceDarkCompat = false
window.isSystemBarsTranslucentCompat = true
window.statusBarColor = resolveThemedColor(android.R.attr.statusBarColor)
window.navigationBarColor = resolveThemedColor(android.R.attr.navigationBarColor)
if (Build.VERSION.SDK_INT >= 23) {
window.isLightStatusBarsCompat = resolveThemedBoolean(android.R.attr.windowLightStatusBar)
}
if (Build.VERSION.SDK_INT >= 27) {
window.isLightNavigationBarCompat = resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
}
this.dayNight = DayNight.Night //dayNight
}
enum class Event {
ServiceRecreated,
ActivityStart,
ActivityStop,
ClashStop,
ClashStart,
ProfileLoaded,
ProfileChanged,
ProfileUpdateCompleted,
ProfileUpdateFailed,
}
}
@@ -0,0 +1,47 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.ConfigOrderDesign
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.ProfileDesign
import com.github.kr328.clash.network.PlanData
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import java.util.concurrent.TimeUnit
//ConfigOrderDesign
class ConfigOrderActivity : BaseActivity<ConfigOrderDesign>() {
override suspend fun main() {
// 获取 Intent 并接收传递的 PlanData 对象
// 使用 Parcelable 获取对象
val planData = intent.getSerializableExtra("planData") as? PlanData
val design = ConfigOrderDesign(this)
setContentDesign(design)
design.fillData(planData)
val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
println("events : " + it.name)
}
design.requests.onReceive {
println("requests : " + it.name)
when (it) {
ConfigOrderDesign.Request.SubmitOrder -> {
startActivity(SubmitOrderActivity::class.intent.putExtra("trade_no",design.trade_no))
}
}
}
}
}
}
}
@@ -0,0 +1,92 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import android.os.Bundle
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.design.MainDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.remote.StatusClient
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.startClashService
import com.github.kr328.clash.util.stopClashService
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
import java.util.*
class ExternalControlActivity : Activity(), CoroutineScope by MainScope() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when(intent.action) {
Intent.ACTION_VIEW -> {
val uri = intent.data ?: return finish()
val url = uri.getQueryParameter("url") ?: return finish()
launch {
val uuid = withProfile {
val type = when (uri.getQueryParameter("type")?.lowercase(Locale.getDefault())) {
"url" -> Profile.Type.Url
"file" -> Profile.Type.File
else -> Profile.Type.Url
}
val name = uri.getQueryParameter("name") ?: getString(R.string.new_profile)
create(type, name).also {
patch(it, name, url, 0)
}
}
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
finish()
}
}
Intents.ACTION_TOGGLE_CLASH -> if(Remote.broadcasts.clashRunning) {
stopClash()
}
else {
startClash()
}
Intents.ACTION_START_CLASH -> if(!Remote.broadcasts.clashRunning) {
startClash()
}
else {
Toast.makeText(this, R.string.external_control_started, Toast.LENGTH_LONG).show()
}
Intents.ACTION_STOP_CLASH -> if(Remote.broadcasts.clashRunning) {
stopClash()
}
else {
Toast.makeText(this, R.string.external_control_stopped, Toast.LENGTH_LONG).show()
}
}
return finish()
}
private fun startClash() {
// if (currentProfile == null) {
// Toast.makeText(this, R.string.no_profile_selected, Toast.LENGTH_LONG).show()
// return
// }
val vpnRequest = startClashService()
if (vpnRequest != null) {
Toast.makeText(this, R.string.unable_to_start_vpn, Toast.LENGTH_LONG).show()
return
}
Toast.makeText(this, R.string.external_control_started, Toast.LENGTH_LONG).show()
}
private fun stopClash() {
stopClashService()
Toast.makeText(this, R.string.external_control_stopped, Toast.LENGTH_LONG).show()
}
}
@@ -0,0 +1,160 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.github.kr328.clash
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import com.github.kr328.clash.common.util.grantPermissions
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.common.util.uuid
import com.github.kr328.clash.design.FilesDesign
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.remote.FilesClient
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.fileName
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import java.util.*
import java.util.concurrent.TimeUnit
class FilesActivity : BaseActivity<FilesDesign>() {
override suspend fun main() {
val uuid = intent.uuid ?: return finish()
val profile = withProfile { queryByUUID(uuid) } ?: return finish()
val root = uuid.toString()
val design = FilesDesign(this)
val client = FilesClient(this)
val stack = Stack<String>()
design.configurationEditable = profile.type != Profile.Type.Url
design.fetch(client, stack, root)
setContentDesign(design)
val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart, Event.ActivityStop -> {
design.fetch(client, stack, root)
}
else -> Unit
}
}
design.requests.onReceive {
try {
when (it) {
FilesDesign.Request.PopStack -> {
if (stack.empty()) {
finish()
} else {
stack.pop()
}
}
is FilesDesign.Request.OpenDirectory -> {
stack.push(it.file.id)
}
is FilesDesign.Request.OpenFile -> {
startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
Intent(Intent.ACTION_VIEW).setDataAndType(
client.buildDocumentUri(it.file.id),
"text/plain"
).grantPermissions()
)
}
is FilesDesign.Request.DeleteFile -> {
client.deleteDocument(it.file.id)
}
is FilesDesign.Request.RenameFile -> {
val newName = design.requestFileName(it.file.name)
client.renameDocument(it.file.id, newName)
}
is FilesDesign.Request.ImportFile -> {
if (Build.VERSION.SDK_INT >= 23) {
val hasPermission = ContextCompat.checkSelfPermission(
this@FilesActivity,
Manifest.permission.READ_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
if (!hasPermission) {
val granted = startActivityForResult(
ActivityResultContracts.RequestPermission(),
Manifest.permission.READ_EXTERNAL_STORAGE,
)
if (!granted) {
return@onReceive
}
}
}
val uri: Uri? = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*"
)
if (uri != null) {
if (it.file == null) {
val name = design.requestFileName(uri.fileName ?: "File")
client.importDocument(stack.last(), uri, name)
} else {
client.copyDocument(it.file!!.id, uri)
}
}
}
is FilesDesign.Request.ExportFile -> {
val uri: Uri? = startActivityForResult(
ActivityResultContracts.CreateDocument("text/plain"),
it.file.name
)
if (uri != null) {
client.copyDocument(uri, it.file.id)
}
}
}
} catch (e: Exception) {
design.showExceptionToast(e)
}
design.fetch(client, stack, root)
}
if (activityStarted) {
ticker.onReceive {
design.updateElapsed()
}
}
}
}
}
override fun onBackPressed() {
design?.requests?.trySend(FilesDesign.Request.PopStack)
}
private suspend fun FilesDesign.fetch(client: FilesClient, stack: Stack<String>, root: String) {
val documentId = stack.lastOrNull() ?: root
val files = if (stack.empty()) {
val list = client.list(documentId)
val config = list.firstOrNull { it.id.endsWith("config.yaml") }
if (config == null || config.size > 0) list else listOf(config)
} else {
client.list(documentId)
}
swapFiles(files, stack.empty())
}
}
@@ -0,0 +1,157 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
import android.view.Gravity
import android.widget.Button
import android.widget.FrameLayout
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.design.BaseListActivity
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.adapter.TicketsDataAdapter
import com.github.kr328.clash.design.util.showCustomDialog
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.TicketsData
import com.github.kr328.clash.network.safeApiRequestCall
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class GongdanActivity : BaseListActivity<TicketsData>() {
override fun createAdapter(): RecyclerView.Adapter<*> {
return TicketsDataAdapter(onItemClick = { planData ->
// 在这里处理 item 点击事件
val intent = Intent(this, TicketDetailActivity::class.java)
intent.putExtra("ticketid", planData.id ?: 0)
startActivity(intent)
}, onCloseItemClick = { planData ->
showCustomDialog(
title = "提示",
message = "确定关闭当前工单吗?",
positiveButtonText = "确定",
negativeButtonText = "取消",
onPositiveClick = {
// 执行确认操作
// startActivity(PlansActivity::class.intent)
CoroutineScope(Dispatchers.IO).launch {
val apiService = ApiClient.retrofit.create(ApiService::class.java)
val fieldMap = mutableMapOf<String, Int>()
fieldMap.put("id",planData.id ?: 0)
safeApiRequestCall {
apiService.closeTicket(
PreferenceManager.loginauthData,
request = fieldMap
)
}.let {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
if (it != null && it.isSuccessful) {
withContext(Dispatchers.Main) {
Toast.makeText(this@GongdanActivity, "关闭成功", Toast.LENGTH_SHORT).show()
}
refereshData()
}else{
withContext(Dispatchers.Main) {
Toast.makeText(
this@GongdanActivity,
"关闭失败",
Toast.LENGTH_SHORT
).show()
}
}
}
}
},
onNegativeClick = {
// 执行取消操作
}
)
})
}
override fun onCreate() {
// Create the top-right icon
val iconView = Button(this)
iconView.setText("新建工单")
iconView.layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.TOP or Gravity.END
).apply {
marginEnd = 16 // Add margin to the right
topMargin = 16 // Add margin to the top
}
// Set the background tint programmatically
/* ViewCompat.setBackgroundTintList(
iconView,
ContextCompat.getColorStateList(this, R.color.blue)
)
*/
val colorStateList: ColorStateList = ColorStateList(
arrayOf<IntArray>(intArrayOf(android.R.attr.state_pressed)), intArrayOf(
R.color.blue
)
)
iconView.setBackgroundTintList(colorStateList)
iconView.setOnClickListener {
val intent = Intent(this@GongdanActivity, NewGongdanActivity::class.java)
//startActivity(intent)
resultLauncher.launch(intent)
}
mainBinding?.activityBarLayout?.addView(iconView)
}
private val resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val data = result.data
val resultData = data?.getStringExtra("resultKey")
// Process the result
println("registerForActivityResult : resultData : ${resultData} ")
if (resultData == "shuaxin"){
refereshData()
}
}
}
override suspend fun loadData(page: Int, callback: (List<TicketsData>?, Throwable?) -> Unit) {
// android:backgroundTint="@color/blue"
// android:background="@drawable/rounded_button_background"
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.getTickets(PreferenceManager.loginauthData)}.let {
if (it != null && it.isSuccessful) {
callback(it.body()?.data,null)
}else{
callback(null, Throwable("请求失败"))
}
}
}
}
@@ -0,0 +1,37 @@
package com.github.kr328.clash
import android.content.Intent
import com.github.kr328.clash.design.H5WebViewDesign
import com.github.kr328.clash.design.PreferenceManager
class H5WebActivity : BaseActivity<H5WebViewDesign>() {
override suspend fun main() {
val design = H5WebViewDesign(this) {
startActivity(Intent(Intent.ACTION_VIEW).setData(it))
//Uri.parse(PreferenceManager.getConfigFromPreferences(this@ProfileActivity)?.telegramurl)
}
setContentDesign(design)
val url = intent.getStringExtra("url")
if (url != null && url.length > 1){
title = url
design.refereshApplyFrom()
design.breaseURL(url)
}else{
PreferenceManager.getConfigFromPreferences(this@H5WebActivity)?.kefuurl?.let {
design.breaseURL(
it
)
}
}
// while (isActive) {
// events.receive()
// }
}
}
@@ -0,0 +1,19 @@
package com.github.kr328.clash
import android.content.Intent
import com.github.kr328.clash.design.HelpDesign
import kotlinx.coroutines.isActive
class HelpActivity : BaseActivity<HelpDesign>() {
override suspend fun main() {
// val design = HelpDesign(this) {
// startActivity(Intent(Intent.ACTION_VIEW).setData(it))
// }
//
// setContentDesign(design)
//
// while (isActive) {
// events.receive()
// }
}
}
@@ -0,0 +1,24 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.InvitationDesign
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.ProfileDesign
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import java.util.concurrent.TimeUnit
class InvitationActivity : BaseActivity<InvitationDesign>() {
override suspend fun main() {
PreferenceManager.init(this)
val design = InvitationDesign(this)
setContentDesign(design)
design.requestdataDisible()
}
}
@@ -0,0 +1,187 @@
package com.github.kr328.clash
import android.content.ComponentName
import android.content.Context
import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.common.compat.startForegroundServiceCompat
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.fileName
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.LogcatDesign
import com.github.kr328.clash.design.dialog.withModelProgressBar
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.log.LogcatFilter
import com.github.kr328.clash.log.LogcatReader
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.io.OutputStreamWriter
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class LogcatActivity : BaseActivity<LogcatDesign>() {
private var conn: ServiceConnection? = null
override suspend fun main() {
val fileName = intent?.fileName
if (fileName != null) {
val file = LogFile.parseFromFileName(fileName) ?: return showInvalid()
return mainLocalFile(file)
}
return mainStreaming()
}
private suspend fun mainLocalFile(file: LogFile) {
val messages = try {
LogcatReader(this, file).readAll()
} catch (e: Exception) {
Log.e("Fail to read log file ${file.fileName}: ${e.message}")
return showInvalid()
}
val design = LogcatDesign(this, false)
setContentDesign(design)
design.patchMessages(messages, 0, messages.size)
while (isActive) {
when (design.requests.receive()) {
LogcatDesign.Request.Delete -> {
withContext(Dispatchers.IO) {
logsDir.resolve(file.fileName).delete()
}
finish()
}
LogcatDesign.Request.Export -> {
val output = startActivityForResult(
ActivityResultContracts.CreateDocument("text/plain"),
file.fileName
)
if (output != null) {
try {
withContext(Dispatchers.IO) {
writeLogTo(messages, file, output)
}
design.showToast(R.string.file_exported, ToastDuration.Long)
} catch (e: Exception) {
design.showExceptionToast(e)
}
}
}
else -> Unit
}
}
}
private suspend fun mainStreaming() {
val design = LogcatDesign(this, true)
setContentDesign(design)
startForegroundServiceCompat(LogcatService::class.intent)
val logcat = bindLogcatService()
val ticker = ticker(500)
var initial = true
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
LogcatDesign.Request.Close -> {
stopService(LogcatService::class.intent)
finish()
}
else -> Unit
}
}
if (activityStarted) {
ticker.onReceive {
val snapshot = logcat.snapshot(initial) ?: return@onReceive
design.patchMessages(snapshot.messages, snapshot.removed, snapshot.appended)
initial = false
}
}
}
}
}
override fun onDestroy() {
conn?.apply(this::unbindService)
super.onDestroy()
}
private suspend fun bindLogcatService(): LogcatService {
return suspendCoroutine { ctx ->
bindService(LogcatService::class.intent, object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
val srv = service!!.queryLocalInterface("") as LogcatService
ctx.resume(srv)
conn = this
}
override fun onServiceDisconnected(name: ComponentName?) {
conn = null
}
}, Context.BIND_AUTO_CREATE)
}
}
@Suppress("BlockingMethodInNonBlockingContext")
private suspend fun writeLogTo(messages: List<LogMessage>, file: LogFile, uri: Uri) {
LogcatFilter(OutputStreamWriter(contentResolver.openOutputStream(uri)), this).use {
withContext(Dispatchers.Main) {
withModelProgressBar {
configure {
isIndeterminate = true
max = messages.size
}
withContext(Dispatchers.IO) {
it.writeHeader(file.date)
messages.forEachIndexed { idx, msg ->
configure {
isIndeterminate = false
progress = idx
}
it.writeMessage(msg)
}
}
}
}
}
}
private fun showInvalid() {
Toast.makeText(this, R.string.invalid_log_file, Toast.LENGTH_LONG).show()
}
}
@@ -0,0 +1,164 @@
package com.github.kr328.clash
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.IBinder
import android.os.IInterface
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.github.kr328.clash.common.compat.getColorCompat
import com.github.kr328.clash.common.compat.pendingIntentFlags
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.log.LogcatCache
import com.github.kr328.clash.log.LogcatWriter
import com.github.kr328.clash.service.RemoteService
import com.github.kr328.clash.service.remote.ILogObserver
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.remote.unwrap
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.Channel
import java.io.IOException
import java.util.*
class LogcatService : Service(), CoroutineScope by CoroutineScope(Dispatchers.Default), IInterface {
private val cache = LogcatCache()
private val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) {
stopSelf()
}
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
startObserver(service ?: return stopSelf())
}
}
override fun onCreate() {
super.onCreate()
running = true
createNotificationChannel()
showNotification()
bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
}
override fun onDestroy() {
cancel()
unbindService(connection)
stopForeground(true)
running = false
super.onDestroy()
}
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
}
}
}
suspend fun snapshot(full: Boolean): LogcatCache.Snapshot? {
return cache.snapshot(full)
}
private fun startObserver(binder: IBinder) {
if (!binder.isBinderAlive)
return stopSelf()
launch(Dispatchers.IO) {
val service = binder.unwrap(IRemoteService::class).clash()
val channel = Channel<LogMessage>(CACHE_CAPACITY)
try {
logsDir.mkdirs()
LogcatWriter(this@LogcatService).use {
val observer = object : ILogObserver {
override fun newItem(log: LogMessage) {
channel.trySend(log)
}
}
service.setLogObserver(observer)
while (isActive) {
val msg = channel.receive()
it.appendMessage(msg)
cache.append(msg)
}
}
} catch (e: IOException) {
Log.e("Write log file: $e", e)
} finally {
withContext(NonCancellable) {
if (binder.isBinderAlive) {
service.setLogObserver(null)
}
stopSelf()
}
}
}
}
private fun createNotificationChannel() {
NotificationManagerCompat.from(this)
.createNotificationChannel(
NotificationChannelCompat.Builder(
CHANNEL_ID,
NotificationManagerCompat.IMPORTANCE_DEFAULT
).setName(getString(R.string.clash_logcat)).build()
)
}
private fun showNotification() {
val notification = NotificationCompat
.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_logo_service)
.setColor(getColorCompat(R.color.color_clash_light))
.setContentTitle(getString(R.string.clash_logcat))
.setContentText(getString(R.string.running))
.setContentIntent(
PendingIntent.getActivity(
this,
R.id.nf_logcat_status,
LogcatActivity::class.intent
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP),
pendingIntentFlags(PendingIntent.FLAG_UPDATE_CURRENT)
)
)
.build()
startForeground(R.id.nf_logcat_status, notification)
}
companion object {
private const val CHANNEL_ID = "clash_logcat_channel"
private const val CACHE_CAPACITY = 128
var running: Boolean = false
}
}
@@ -0,0 +1,267 @@
package com.github.kr328.clash
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.databinding.ActivityLoginBinding
import com.github.kr328.clash.design.databinding.ActivityProfileBinding
import com.github.kr328.clash.design.ui.DayNight
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.layoutInflater
import com.github.kr328.clash.design.util.resolveThemedBoolean
import com.github.kr328.clash.design.util.resolveThemedColor
import com.github.kr328.clash.design.util.root
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiClientConfig
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.ConfigResponse
import com.github.kr328.clash.network.LoginRequest
import com.github.kr328.clash.network.LoginResponse
import com.github.kr328.clash.network.safeApiCall
import com.github.kr328.clash.network.safeApiRequestCall
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
class LoginActivity : AppCompatActivity() {
private lateinit var apiService: ApiService
private var dayNight: DayNight = DayNight.Day
@Deprecated("Deprecated in Java")
override fun onBackPressed() {
// 禁用返回键
// 不调用 super.onBackPressed()
}
private var isPasswordVisible = false
private var configRetryCount = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityLoginBinding
.inflate(this.layoutInflater, this.root, false)
setContentView(binding.root)
applyDayNight()
PreferenceManager.init(this)
binding.togglePasswordVisibility.setOnClickListener {
isPasswordVisible = !isPasswordVisible
if (isPasswordVisible) {
// 显示明文密码
binding.loginPasswordEditText.inputType = InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
binding.togglePasswordVisibility.setImageResource(R.drawable.visibility_24px) // 更改图标为显示状态
} else {
// 显示星号密码
binding.loginPasswordEditText.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_PASSWORD
binding.togglePasswordVisibility.setImageResource(R.drawable.visibility_off_24px) // 更改图标为隐藏状态
}
// 将光标移动到文本的末尾
binding.loginPasswordEditText.setSelection(binding.loginPasswordEditText.text.length)
}
binding.loginLoginButton.setOnClickListener {
val email = binding.loginEmailEditText.text.toString()
val password = binding.loginPasswordEditText.text.toString()
val isAgreementChecked = binding.loginAgreementCheckBox.isChecked
if (email.isEmpty() || password.isEmpty()) {
Toast.makeText(this, "请输入邮箱和密码", Toast.LENGTH_SHORT).show()
} else if (!isAgreementChecked) {
Toast.makeText(this, "请同意隐私政策和用户协议", Toast.LENGTH_SHORT).show()
} else {
// Hide the keyboard
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val currentFocusView = currentFocus
if (currentFocusView != null) {
inputMethodManager.hideSoftInputFromWindow(currentFocusView.windowToken, 0)
}
performLogin(email, password)
}
}
binding.loginForgotPassword.setOnClickListener {
// Navigate to Forgot Password screen or show dialog
}
binding.loginRegister.setOnClickListener {
// Navigate to Register screen
//startActivity(RegisterActivity::class::intent)
// startActivity(RegisterActivity::class::intent)
val intent = Intent(this@LoginActivity, RegisterActivity::class.java)
startActivity(intent)
// finish() // It
}
apiService = ApiClientConfig.retrofit.create(ApiService::class.java)
//获取 Config 数据
getconfig()
}
private fun getconfig(){
runOnUiThread {
LoadingDialog.show(this, "正在初始化...")
}
CoroutineScope(Dispatchers.IO).launch {
safeApiCall {apiService.getConfig()}.let {
if (it != null) {
if (it.code == 1 ) {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
PreferenceManager.saveConfigToPreferences( it)
//重新初始化 ApiClint
}
}else{
configRetryCount++
if (configRetryCount < 3) {
getconfig()
} else {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
Toast.makeText(this@LoginActivity, "初始化失败,请检查配置是否正确", Toast.LENGTH_LONG).show()
}
}
}
}
}
}
/**
* withContext(Dispatchers.Main) {
* if (response.isSuccessful) {
* val userData = response.body()
* // 更新 UI
* } else {
* Toast.makeText(this@MainActivity, "Failed to fetch user data", Toast.LENGTH_SHORT).show()
* }
* }
* */
private fun applyDayNight(config: Configuration = resources.configuration) {
// val dayNight = theme.applyStyle(R.style.AppThemeLight, true) //默认白天模式
/*
val dayNight = queryDayNight(config)
when (dayNight) {
DayNight.Night -> theme.applyStyle(R.style.AppThemeDark, true)
DayNight.Day -> theme.applyStyle(R.style.AppThemeLight, true)
}*/
window.isAllowForceDarkCompat = false
window.isSystemBarsTranslucentCompat = true
window.statusBarColor = resolveThemedColor(android.R.attr.statusBarColor)
window.navigationBarColor = resolveThemedColor(android.R.attr.navigationBarColor)
if (Build.VERSION.SDK_INT >= 23) {
window.isLightStatusBarsCompat = resolveThemedBoolean(android.R.attr.windowLightStatusBar)
}
if (Build.VERSION.SDK_INT >= 27) {
window.isLightNavigationBarCompat = resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
}
this.dayNight = DayNight.Night //dayNight
}
private fun performLogin(email: String, password: String) {
// Perform login logic here, possibly calling an API
// Show the loading indicator with a custom message
LoadingDialog.show(this, "正在登录...")
val apiServiceApp = ApiClient.retrofit.create(ApiService::class.java)
//获取 Config 数据
CoroutineScope(Dispatchers.IO).launch {
safeApiRequestCall {
apiServiceApp.loginUser(LoginRequest(email,password))}.let {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
if (it != null &&
it.isSuccessful) {
val response: LoginResponse? = it.body()
println(response )
println(it.errorBody()?.string() ?: "")
if (response?.data != null){
PreferenceManager.loginemail = email
PreferenceManager.loginToken = response.data?.token ?: ""
PreferenceManager.loginauthData = response.data?.auth_data ?: ""
PreferenceManager.isLoginin = true
// saveLoginMailToken(this@LoginActivity,email)
// saveLoginToken(this@LoginActivity, response.data.token ?: "")
// saveLoginAuthData(this@LoginActivity, response.data.auth_data ?: "")
// saveLoginStatus(this@LoginActivity,true)
//获取用户信息
withContext(Dispatchers.Main) {
val intent = Intent(this@LoginActivity, MainActivity::class.java)
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish() // It
}
}
}else{
// Handle error response, even if it's 422
try {
val errorResponse = it?.errorBody()?.string() ?: ""
print(errorResponse)
val errorJson = JSONObject(errorResponse)
val message = errorJson.optString("message")
// Process the error message or show it to the user
if (!message.isNullOrEmpty()) {
withContext(Dispatchers.Main) {
Toast.makeText(this@LoginActivity, "登录失败:${message}", Toast.LENGTH_LONG).show()
}
}
println( "Message: $message, ")
} catch (e: JSONException) {
// Handle JSON parsing error
withContext(Dispatchers.Main) {
Toast.makeText(this@LoginActivity, "登录失败:${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
}
}
}
@@ -0,0 +1,71 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setFileName
import com.github.kr328.clash.design.LogsDesign
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.util.logsDir
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
class LogsActivity : BaseActivity<LogsDesign>() {
override suspend fun main() {
if (LogcatService.running) {
return startActivity(LogcatActivity::class.intent)
}
val design = LogsDesign(this)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart -> {
val files = withContext(Dispatchers.IO) {
loadFiles()
}
design.patchLogs(files)
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
LogsDesign.Request.StartLogcat -> {
startActivity(LogcatActivity::class.intent)
finish()
}
LogsDesign.Request.DeleteAll -> {
if (design.requestDeleteAll()) {
withContext(Dispatchers.IO) {
deleteAllLogs()
}
events.trySend(Event.ActivityStart)
}
}
is LogsDesign.Request.OpenFile -> {
startActivity(LogcatActivity::class.intent.setFileName(it.file.fileName))
}
}
}
}
}
}
private fun loadFiles(): List<LogFile> {
val list = cacheDir.resolve("logs").listFiles()?.toList() ?: emptyList()
return list.mapNotNull { LogFile.parseFromFileName(it.name) }
}
private fun deleteAllLogs() {
logsDir.deleteRecursively()
}
}
@@ -0,0 +1,784 @@
package com.github.kr328.clash
import android.os.Handler
import android.os.Looper
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.PopupMenu
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.design.MainDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.util.startClashService
import com.github.kr328.clash.util.stopClashService
import com.github.kr328.clash.util.withClash
import com.github.kr328.clash.util.withProfile
import com.github.kr328.clash.core.bridge.*
import com.github.kr328.clash.core.model.FetchStatus
import com.github.kr328.clash.core.model.ProxySort
import com.github.kr328.clash.core.model.TunnelState
import com.github.kr328.clash.design.NewProfileDesign
import com.github.kr328.clash.design.PreferenceLiveData
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.R
import com.github.kr328.clash.design.component.ProxyMenu
import com.github.kr328.clash.design.databinding.ActivitySplashBinding
import com.github.kr328.clash.design.dialog.requestModelTextInput
import com.github.kr328.clash.design.dialog.withModelProgressBar
import com.github.kr328.clash.design.network.APIGlobalObject
import com.github.kr328.clash.design.util.ValidatorHttpUrl
import com.github.kr328.clash.design.util.root
import com.github.kr328.clash.design.util.showCustomDialog
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiClientConfig
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.ConfigResponse
import com.github.kr328.clash.network.LoginRequest
import com.github.kr328.clash.network.SubscribeData
import com.github.kr328.clash.network.SubscribeResponse
import com.github.kr328.clash.network.safeApiCall
import com.github.kr328.clash.network.safeApiRequestCall
import com.github.kr328.clash.service.ProfileManager
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.util.PreferenceManagerServer
import com.github.kr328.clash.utity.LoadingDialog
import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.concurrent.timer
class MainActivity : BaseActivity<MainDesign>() {
private lateinit var apiService: ApiService
private var subData: SubscribeData? = null
private val selectnodeName: LiveData<String> by lazy {
PreferenceLiveData(this)
}
override suspend fun main() {
val design = MainDesign(this)
if (PreferenceManager.isLoginin){
setContentDesign(design)
design.fetch()
design.startBannsers()
//1 : 查询 config 信息
apiService = ApiClientConfig.retrofit.create(ApiService::class.java)
//获取 Config 数据
// CoroutineScope(Dispatchers.IO).launch { }
val currentTime = System.currentTimeMillis()
val cacheExpiryTime = 10 * 60 * 1000 // 10 minutes in milliseconds
// 如果缓存数据存在且未超过 10 分钟,使用缓存数据;否则,重新请求数据
if ( PreferenceManager.cached_data != null && (currentTime - PreferenceManager.cache_timestamp) <= cacheExpiryTime) {
// 使用缓存数据
val gson = Gson()
val configResponse = gson.fromJson(PreferenceManager.cached_data, ConfigResponse::class.java)
println(configResponse)
println("Using cached data: ${PreferenceManager.cached_data}")
println("Using cached_userSubscritedata : ${PreferenceManager.cached_userSubscritedata}")
if (PreferenceManager.cached_userSubscritedata.length > 0){
val subcache = gson.fromJson(PreferenceManager.cached_userSubscritedata, SubscribeResponse::class.java)
subData = subcache.data
//存储到本地
val plainid = subcache.data.plan_id ?: 0
// 或者检查使用量
val guoqi = (subcache.data.u ) + (subcache.data.d) > (subcache.data.transfer_enable ?: 0) && (subcache.data.transfer_enable ?: 0) > 0
if ( plainid == 0 || guoqi ) {
//提示到期
//删除所有节点
withProfile {
var allProfiles = queryAll()
println(allProfiles)
//删除所有节点
if (allProfiles.isNotEmpty() && allProfiles.count() > 0){
allProfiles.forEach {
delete(it.uuid)
println("Deleted ${it.uuid} ${it.name}: ${it.source}")
}
}
}
//删除所有节点文件
withContext(Dispatchers.Main) {
showCustomDialog(
title = "提示",
message = "您的订阅已到期,请续费订阅开启服务",
positiveButtonText = "确定",
negativeButtonText = "取消",
onPositiveClick = {
// 执行确认操作
startActivity(PlansActivity::class.intent)
},
onNegativeClick = {
// 执行取消操作
}
)
}
}
}
println("subData ${subData}")
}else{
// 缓存已过期,重新请求数据
println("Cache expired, requesting new data")
// 重新请求数据
//requestNewData()
LoadingDialog.show(this, "正在更新节点数据...")
configRequesting()
}
selectnodeName.observe(this, androidx.lifecycle.Observer { newValue ->
})
val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart,
Event.ServiceRecreated,
Event.ClashStop, Event.ClashStart,
Event.ProfileLoaded, Event.ProfileChanged -> design.fetch()
else -> Unit
}
}
design.requests.onReceive {
when (it) {
MainDesign.Request.ToggleStatus -> {
if (clashRunning)
stopClashService()
else
design.startClash()
}
MainDesign.Request.OpenProxy ->
startActivity(ProxyActivity::class.intent)
MainDesign.Request.OpenProfiles ->
startActivity(ProfilesActivity::class.intent)
MainDesign.Request.OpenProviders ->
startActivity(ProvidersActivity::class.intent)
MainDesign.Request.OpenLogs ->
startActivity(LogsActivity::class.intent)
MainDesign.Request.OpenSettings ->
startActivity(SettingsActivity::class.intent)
MainDesign.Request.OpenHelp ->
startActivity(HelpActivity::class.intent)
MainDesign.Request.OpenAbout ->
design.showAbout(queryAppVersionName())
MainDesign.Request.OpenSettingsDIY ->
startActivity(ProfileActivity::class.intent)
MainDesign.Request.OpenSettingsKEFU ->
startActivity(PlansActivity::class.intent)
MainDesign.Request.OpenModeDirect ->
withClash {
PreferenceManager.modeName = TunnelState.Mode.Direct.name
val override = queryOverride(Clash.OverrideSlot.Session)
override.mode = TunnelState.Mode.Direct
patchOverride(Clash.OverrideSlot.Session, override)
}
MainDesign.Request.OpenModeGlobal -> withClash {
CoroutineScope(Dispatchers.Main).launch {
PreferenceManager.modeName = TunnelState.Mode.Global.name
LoadingDialog.show(this@MainActivity, "正在切换中...")
val names = withClash { queryProxyGroupNames(uiStore.proxyExcludeNotSelectable) }
withClash {
val o = queryOverride(Clash.OverrideSlot.Session)
o.mode = TunnelState.Mode.Global
patchOverride(Clash.OverrideSlot.Session, o)
}
startServer()
}
}
MainDesign.Request.OpenModeRule -> withClash {
CoroutineScope(Dispatchers.Main).launch {
PreferenceManager.modeName = TunnelState.Mode.Rule.name
LoadingDialog.show(this@MainActivity, "正在切换中...")
withClash {
val o = queryOverride(Clash.OverrideSlot.Session)
o.mode = TunnelState.Mode.Rule
patchOverride(Clash.OverrideSlot.Session, o)
}
startServer()
}
}
MainDesign.Request.OpenModeMenu -> {
design.OpenModeMenu()
}
MainDesign.Request.OpenChangeMode -> {
design.OpenModeMenu()
}
MainDesign.Request.OpenModeSheet ->{
design.OpenModeSheet()
}
}
}
if (clashRunning) {
ticker.onReceive {
design.fetchTraffic()
}
}
}
}
}else{
val binding = ActivitySplashBinding
.inflate(this.layoutInflater, this.root, false)
setContentView(binding.root)
Handler(Looper.getMainLooper()).postDelayed({
startActivity(LoginActivity::class.intent)
}, 1000) // Simulating a network delay
}
}
private suspend fun configRequesting(retryCount: Int = 0){
val gson = Gson()
val currentTime = System.currentTimeMillis()
/*apiService.getConfig().let {
if (it.isSuccessful) {
val response: ConfigResponse = it.body() ?: throw Exception("Response body is null")
if (response.code == 1 ) {
PreferenceManager.saveConfigToPreferences(response)
//更新缓存数据
PreferenceManager.cache_timestamp = currentTime
PreferenceManager.cached_data = gson.toJson( it.body())
println("API Response: ${gson.toJson( it.body())}")
//重新初始化 ApiClint
//2 : 获取订阅节点信息
performSubscrite()
}
} else {
println("API Error: ${it.errorBody()}")
configRequesting()
}
}
*/
safeApiCall {apiService.getConfig()}.let {
if (it !=null){
if (it.code == 1 ) {
PreferenceManager.saveConfigToPreferences(it)
//更新缓存数据
PreferenceManager.cache_timestamp = currentTime
PreferenceManager.cached_data = gson.toJson( it)
println("API Response: ${it}")
//重新初始化 ApiClint
//2 : 获取订阅节点信息
performSubscrite()
}
}else{
if (retryCount < 3) {
configRequesting(retryCount + 1)
} else {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
showCustomDialog(
title = "提示",
message = "初始化失败,请检查配置是否正确",
positiveButtonText = "确定",
negativeButtonText = "",
onPositiveClick = {
},
onNegativeClick = {
}
)
}
}
}
}
}
private fun performSubscrite(){
val apiServiceApp = ApiClient.retrofit.create(ApiService::class.java)
//获取 Config 数据
CoroutineScope(Dispatchers.IO).launch {
safeApiRequestCall {
apiServiceApp.getSubscribe(PreferenceManager.loginauthData)}.let {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
if (it != null &&
it.isSuccessful) {
println(" performSubscrite : ${it.body()?.data}")
subData = it.body()?.data
val gson = Gson()
PreferenceManager.cached_userSubscritedata = gson.toJson( it.body())
APIGlobalObject.subData = it.body()?.data
//存储到本地
val plainid = it.body()?.data?.plan_id ?: 0
// 或者检查使用量
val guoqi = (it.body()?.data?.u ?: 0) + (it.body()?.data?.d ?: 0) > (it.body()?.data?.transfer_enable ?: 0) && (it.body()?.data?.transfer_enable ?: 0) > 0
if ( plainid == 0 || guoqi ) {
//提示到期
//删除所有节点
withProfile {
var allProfiles = queryAll()
println(allProfiles)
if (allProfiles.isNotEmpty() && allProfiles.count() > 1){
allProfiles.forEach { delete(it.uuid) }
//删除所有节点
}
}
//删除所有节点文件
withContext(Dispatchers.Main) {
showCustomDialog(
title = "提示",
message = "您的订阅已到期,请续费订阅开启服务",
positiveButtonText = "确定",
negativeButtonText = "取消",
onPositiveClick = {
// 执行确认操作
startActivity(PlansActivity::class.intent)
},
onNegativeClick = {
// 执行取消操作
}
)
}
}else{
//更新本地节点
withProfile {
var profileActive = queryActive()
if (profileActive != null){
try {
println(">>>>>>>>> 更新节点订阅地址:${profileActive.uuid} ${profileActive.source}")
update(profileActive.uuid)
}catch (e: Exception){
println(">>>>>>>>> 更新节点订阅地址 catch${e.message}")
}
finally {
println(">>>>>>>>> 更新节点订阅地址 完成 finally")
}
}
}
}
}
}
}
}
private suspend fun MainDesign.fetch() {
setClashRunning(clashRunning)
val state = withClash {
queryTunnelState()
}
println("运行时查询系统mode模式: ${state}")
val providers = withClash {
queryProviders()
}
if (providers.isNotEmpty()) {
println("模式providers ${providers}")
providers.forEach { println("The element is ${it.name}") }
}
setMode(state.mode)
setHasProviders(providers.isNotEmpty())
withProfile {
setProfileName(queryActive()?.name)
setSelectNodeName(PreferenceManager.selectnodeName )
}
}
//查询上传速度和下载速度
private suspend fun MainDesign.fetchTraffic() {
withClash {
// setForwarded(queryTrafficTotal())
setUploadedSeep(queryTrafficTotal())
setDownloadedSeep(queryTrafficTotal())
}
}
private suspend fun MainDesign.startClash() {
//删除所有节点
withProfile {
var allProfiles = queryAll()
if (allProfiles.isNotEmpty() && allProfiles.count() > 1){ //删除超过多余的 1 个节点之外的节点
allProfiles.forEach { delete(it.uuid) }
//删除所有节点
}
}
// val activeProfile = withProfile { queryActive() }
// if (activeProfile != null) {
// android.util.Log.i("activeProfile",">> ${activeProfile.type.name} ${activeProfile.uuid} ${activeProfile.source} ")
// showToast(R.string.no_profile_selected, ToastDuration.Long) {
// setAction(R.string.profiles) {
// startActivity(ProfilesActivity::class.intent)
// }
// }
// }
val activeProfile = withProfile { queryActive() }
println("当前订阅文件:${activeProfile}")
if (activeProfile == null ) { //|| !activeProfile.imported
LoadingDialog.show(this@MainActivity, "正在请求节点数据...")
// showToast("正在请求节点数据....", ToastDuration.Long)
//create(Profile.Type.Url, "VPN")
withProfile{
println(">>>>>>>>>${subData}")
//https://hongxingdl.com/hxvip?token=bc2f2839ccc608f34d6a6fc90b6e7f15
//https://hongxingdl.com/api/v1/client/subscribe?token=bc2f2839ccc608f34d6a6fc90b6e7f15
//"https://api.0009.uk/api/v1/client/subscribe?token=5652e3eec20c553047dbe3fcf8754090"
if (subData?.subscribe_url != null && subData?.plan?.name != null && subData?.plan?.id != null) {
var suburl = subData?.subscribe_url ?: ""
var encodeURL = java.net.URLEncoder.encode(suburl, "utf-8")
encodeURL = encodeURL.replace("?","%3F")
var newULR =suburl
println(">>>>>>>>> "+suburl)
if( ApiClientConfig.ConfigNodeURL.length > 3){
newULR = ApiClientConfig.ConfigNodeURL + encodeURL
}
println(">>>>>>>>> "+newULR)
//val uuid: UUID = create(Profile.Type.Url,subData?.plan?.name ?: "VPN",newULR)
launch {
val uuid2 = withProfile {
val type = Profile.Type.Url
val name = subData?.plan?.name ?: "VPN"
create(type, name).also {
patch(it, name, newULR, 0)
}
}
withProcessing { updateStatus ->
withProfile {
try {
commit(uuid2) {
launch {
activeProfile()
updateStatus(it)
}
}
} catch (e: Exception) {
LoadingDialog.hide()
showExceptionToast("初始化节点数据失败,请检查网络重试")
}
coroutineScope {
}
}
}
/* coroutineScope {
commit(uuid2) {
println(">>>>>>>>>> ${it.progress} ${it.action} ${it.args.toString()} ${it} ")
if (it.action == FetchStatus.Action.Verifying){
// 在协程中启动 activeProfile 调用
CoroutineScope(Dispatchers.IO).launch {
activeProfile()
//自动开启订阅
startServer()
}
}
}
}*/
}
/* commit(uuid){
println(">> ${it.progress} ${it.action} ${it.args.toString()} ")
if (it.action == FetchStatus.Action.Verifying){
// 在协程中启动 activeProfile 调用
CoroutineScope(Dispatchers.IO).launch {
activeProfile()
//自动开启订阅
startServer()
}
}
}*/
}else{
//重新请求网络
performSubscrite()
}
/*
if (subData == null){
withContext(Dispatchers.Main){
LoadingDialog.hide()
}
performSubscrite()
}else{
val plainid =subData?.plan_id ?: 0
// 或者检查使用量
val guoqi = (subData?.u ?: 0) + (subData?.d ?: 0) > (subData?.transfer_enable ?: 0) && (subData?.transfer_enable ?: 0) > 0
if ( plainid == 0 || guoqi ) {
//提示到期
performSubscrite()
}
}
*/
}
}else{
// showToast(R.string.no_profile_selected, ToastDuration.Long) {
// setAction(R.string.profiles) {
// startActivity(ProfilesActivity::class.intent)
// }
// }
println("当前订阅文件>> ${activeProfile.type.name} ${activeProfile.uuid} ${activeProfile.imported} ")
withProfile {
activeProfile.uuid
}
startServer()
}
}
suspend fun withProcessing(executeTask: suspend (suspend (FetchStatus) -> Unit) -> Unit) {
try {
executeTask {
// applyFrom(it)
println("applyFrom ${it.progress} ${it}")
if (it.action == FetchStatus.Action.Verifying) {
//验证通过
CoroutineScope(Dispatchers.IO).launch {
//选中节点
activeProfile()
//自动开启订阅
startServer()
}
}else{
//请求中
}
}
} finally {
}
}
private suspend fun startServer(){
//读取配置文件
val vpnRequest = startClashService()
try {
if (vpnRequest != null) {
val result = startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
vpnRequest
)
if (result.resultCode == RESULT_OK)
{
startClashService()
withContext(Dispatchers.Main){
LoadingDialog.hide()
}
}
}
println("缓存配置模式:${PreferenceManager.modeName}")
CoroutineScope(Dispatchers.Main).launch {
// Delay for 3 seconds
//第一次全部使用全局模式连接
delay(500)
withClash {
// Query the current mode
val override = queryOverride(Clash.OverrideSlot.Session)
// Toggle between Rule and Global modes
if (PreferenceManager.modeName == TunnelState.Mode.Global.name){
override.mode = TunnelState.Mode.Global
}else{
override.mode = TunnelState.Mode.Rule //第一次默认使用智能 模式连接
}
// Apply the new mode`
patchOverride(Clash.OverrideSlot.Session, override)
//模式切换完毕
// Optional: Log or display the current mode for verification
println("模式切换 Clash mode is now set to: ${override.mode}")
delay(500)
//如果切换全局模式,默认 直接主动选择节点 根据缓存来
//if (override.mode == TunnelState.Mode.Global){
val names = queryProxyGroupNames(uiStore.proxyExcludeNotSelectable)
println("模式切换>>>>>>>>>>>>>"+names)
if ( names.size > 0){
var progxys = queryProxyGroup(names[0], ProxySort.Default)
if (progxys.proxies.first().title.length > 0){
println("模式切换>>>>>>>>>>>>>: "+progxys.proxies.last())
if ( progxys.proxies.size > 2)
{
val zutuolist = progxys.proxies.filter { it.title == "自动选择" }
var autoNode = ""
if (PreferenceManager.selectnodeName.length > 0){
autoNode = PreferenceManager.selectnodeName
} else{
if (zutuolist.size > 0){
//说明存在自动选择
autoNode = "自动选择"
}else{
autoNode = progxys.proxies.first().title
}
PreferenceManager.selectnodeName = autoNode // progxys.proxies.last().title
}
patchSelector(names.first(),autoNode) // progxys.proxies.last().title
withContext(Dispatchers.Main){
design?.setSelectNodeName(PreferenceManager.selectnodeName)
}
}
}
}
// }
withContext(Dispatchers.Main){
LoadingDialog.hide()
}
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main){
LoadingDialog.hide()
}
//design?.showToast(R.string.unable_to_start_vpn, ToastDuration.Long)
}
}
private suspend fun activeProfile(){
withProfile {
setActive(queryAll().first())
}
}
private suspend fun queryAppVersionName(): String {
return withContext(Dispatchers.IO) {
packageManager.getPackageInfo(packageName, 0).versionName// + "\n" + Bridge.nativeCoreVersion().replace("_", "-")
}
}
}
@@ -0,0 +1,75 @@
package com.github.kr328.clash
import android.app.Application
import android.content.Context
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.compat.currentProcessName
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.service.util.PreferenceManagerServer
import com.github.kr328.clash.service.util.sendServiceRecreated
import com.github.kr328.clash.util.clashDir
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
@Suppress("unused")
class MainApplication : Application() {
override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
PreferenceManagerServer.init(this)
PreferenceManager.init(this)
Global.init(this)
}
override fun onCreate() {
super.onCreate()
val processName = currentProcessName
extractGeoFiles()
Log.d("Process $processName started")
if (processName == packageName) {
Remote.launch()
} else {
sendServiceRecreated()
}
}
private fun extractGeoFiles() {
clashDir.mkdirs();
val geoipFile = File(clashDir, "geoip.metadb")
if(!geoipFile.exists()) {
FileOutputStream(geoipFile).use {
assets.open("geoip.metadb").copyTo(it);
}
}
val geositeFile = File(clashDir, "geosite.dat")
if(!geositeFile.exists()) {
FileOutputStream(geositeFile).use {
assets.open("geosite.dat").copyTo(it);
}
}
val ASNFile = File(clashDir, "ASN.mmdb")
if(!ASNFile.exists()) {
FileOutputStream(ASNFile).use {
assets.open("ASN.mmdb").copyTo(it);
}
}
}
fun finalize() {
Global.destroy()
}
}
@@ -0,0 +1,136 @@
package com.github.kr328.clash
import android.database.Cursor
import android.net.Uri
import android.provider.OpenableColumns
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.design.MetaFeatureSettingsDesign
import com.github.kr328.clash.util.clashDir
import com.github.kr328.clash.util.withClash
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
class MetaFeatureSettingsActivity : BaseActivity<MetaFeatureSettingsDesign>() {
override suspend fun main() {
val configuration = withClash { queryOverride(Clash.OverrideSlot.Persist) }
defer {
withClash {
patchOverride(Clash.OverrideSlot.Persist, configuration)
}
}
val design = MetaFeatureSettingsDesign(
this,
configuration
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
MetaFeatureSettingsDesign.Request.ResetOverride -> {
if (design.requestResetConfirm()) {
defer {
withClash {
clearOverride(Clash.OverrideSlot.Persist)
}
}
finish()
}
}
MetaFeatureSettingsDesign.Request.ImportGeoIp -> {
val uri = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*")
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportGeoIp)
}
MetaFeatureSettingsDesign.Request.ImportGeoSite -> {
val uri = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*")
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportGeoSite)
}
MetaFeatureSettingsDesign.Request.ImportCountry -> {
val uri = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*")
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportCountry)
}
MetaFeatureSettingsDesign.Request.ImportASN -> {
val uri = startActivityForResult(
ActivityResultContracts.GetContent(),
"*/*")
importGeoFile(uri, MetaFeatureSettingsDesign.Request.ImportASN)
}
}
}
}
}
}
private val validDatabaseExtensions = listOf(
".metadb", ".db", ".dat", ".mmdb"
)
private suspend fun importGeoFile(uri: Uri?, importType: MetaFeatureSettingsDesign.Request) {
val cursor: Cursor? = uri?.let {
contentResolver.query(it, null, null, null, null, null)
}
cursor?.use {
if (it.moveToFirst()) {
val columnIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME)
val displayName: String =
if (columnIndex != -1) it.getString(columnIndex) else "";
val ext = "." + displayName.substringAfterLast(".")
if (!validDatabaseExtensions.contains(ext)) {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.geofile_unknown_db_format)
.setMessage(getString(R.string.geofile_unknown_db_format_message,
validDatabaseExtensions.joinToString("/")))
.setPositiveButton("OK") { _, _ -> }
.show()
return
}
val outputFileName = when (importType) {
MetaFeatureSettingsDesign.Request.ImportGeoIp ->
"geoip$ext"
MetaFeatureSettingsDesign.Request.ImportGeoSite ->
"geosite$ext"
MetaFeatureSettingsDesign.Request.ImportCountry ->
"country$ext"
MetaFeatureSettingsDesign.Request.ImportASN ->
"ASN$ext"
else -> ""
}
withContext(Dispatchers.IO) {
val outputFile = File(clashDir, outputFileName);
contentResolver.openInputStream(uri).use { ins ->
FileOutputStream(outputFile).use { outs ->
ins?.copyTo(outs)
}
}
}
Toast.makeText(this, getString(R.string.geofile_imported, displayName),
Toast.LENGTH_LONG).show()
return
}
}
Toast.makeText(this, R.string.geofile_import_failed, Toast.LENGTH_LONG).show()
}
}
@@ -0,0 +1,37 @@
package com.github.kr328.clash
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.BaseListActivity
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.adapter.OrdersDataAdapter
import com.github.kr328.clash.design.adapter.PlanDataAdapter
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.OrderData
import com.github.kr328.clash.network.PlanData
import com.github.kr328.clash.network.safeApiRequestCall
class MyOrdersActivity : BaseListActivity<OrderData>() {
override fun createAdapter(): RecyclerView.Adapter<*> {
return OrdersDataAdapter(){ item ->
startActivity(SubmitOrderActivity::class.intent.putExtra("trade_no",item.trade_no))
}
}
override fun onCreate() {}
override suspend fun loadData(page: Int, callback: (List<OrderData>?, Throwable?) -> Unit) {
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.getOrders(PreferenceManager.loginauthData)}.let {
if (it!= null && it.isSuccessful) {
callback(it.body()?.data,null)
}else{
callback(null, Throwable("请求失败"))
}
}
}
}
@@ -0,0 +1,64 @@
package com.github.kr328.clash
import android.content.pm.PackageManager
import com.github.kr328.clash.common.util.componentName
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.NetworkSettingsDesign
import com.github.kr328.clash.design.model.Behavior
import com.github.kr328.clash.service.store.ServiceStore
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class NetworkSettingsActivity : BaseActivity<NetworkSettingsDesign>(), Behavior {
override suspend fun main() {
val design = NetworkSettingsDesign(
this,
uiStore,
this,
ServiceStore(this),
clashRunning,
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ClashStart, Event.ClashStop, Event.ServiceRecreated ->
recreate()
else -> Unit
}
}
design.requests.onReceive {
when (it) {
NetworkSettingsDesign.Request.StartAccessControlList ->
startActivity(AccessControlActivity::class.intent)
}
}
}
}
}
override var autoRestart: Boolean
get() {
val status = packageManager.getComponentEnabledSetting(
RestartReceiver::class.componentName
)
return status == PackageManager.COMPONENT_ENABLED_STATE_ENABLED
}
set(value) {
val status = if (value)
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
else
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
packageManager.setComponentEnabledSetting(
RestartReceiver::class.componentName,
status,
PackageManager.DONT_KILL_APP,
)
}
}
@@ -0,0 +1,51 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.Intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.HelpDesign
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
class NewGongdanActivity : BaseActivity<HelpDesign>() {
override suspend fun main() {
val design = HelpDesign(this) {
startActivity(Intent(Intent.ACTION_VIEW).setData(it))
}
setContentDesign(design)
//
// while (isActive) {
// events.receive()
// }
//val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
println("events : " + it.name)
}
design.requests.onReceive {
println("requests : " + it.name)
when (it) {
HelpDesign.Request.CommitSuccess -> {
withContext(Dispatchers.Main) {
val resultIntent = Intent()
resultIntent.putExtra("resultKey", "shuaxin")
setResult(Activity.RESULT_OK, resultIntent)
finish()
}
}
}
}
}
}
}
}
@@ -0,0 +1,145 @@
package com.github.kr328.clash
import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.design.NewProfileDesign
import com.github.kr328.clash.design.model.ProfileProvider
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.*
class NewProfileActivity : BaseActivity<NewProfileDesign>() {
private val self: NewProfileActivity
get() = this
override suspend fun main() {
val design = NewProfileDesign(this)
design.patchProviders(queryProfileProviders())
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
is NewProfileDesign.Request.Create -> {
withProfile {
val name = getString(R.string.new_profile)
val uuid: UUID? = when (val p = it.provider) {
is ProfileProvider.File ->
create(Profile.Type.File, name)
is ProfileProvider.Url ->
create(Profile.Type.Url, name)
is ProfileProvider.External -> {
val data = p.get()
if (data != null) {
val (uri, initialName) = data
create(
Profile.Type.External,
initialName ?: name,
uri.toString()
)
} else {
null
}
}
}
if (uuid != null)
launchProperties(uuid)
}
}
is NewProfileDesign.Request.OpenDetail -> {
launchAppDetailed(it.provider)
}
}
}
}
}
}
private fun launchAppDetailed(provider: ProfileProvider.External) {
val data = Uri.fromParts(
"package",
provider.intent.component?.packageName ?: return,
null
)
startActivity(Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).setData(data))
}
private suspend fun launchProperties(uuid: UUID) {
Log.i("launchProperties",uuid.toString())
val r = startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
PropertiesActivity::class.intent.setUUID(uuid)
)
if (r.resultCode == Activity.RESULT_OK)
finish()
}
private suspend fun ProfileProvider.External.get(): Pair<Uri, String?>? {
val result = startActivityForResult(
ActivityResultContracts.StartActivityForResult(),
intent
)
if (result.resultCode != RESULT_OK)
return null
val uri = result.data?.data
val name = result.data?.getStringExtra(Intents.EXTRA_NAME)
if (uri != null) {
return uri to name
}
return null
}
private suspend fun queryProfileProviders(): List<ProfileProvider> {
return withContext(Dispatchers.IO) {
val providers = packageManager.queryIntentActivities(
Intent(Intents.ACTION_PROVIDE_URL),
0
).map {
val activity = it.activityInfo
val name = activity.applicationInfo.loadLabel(packageManager)
val summary = activity.loadLabel(packageManager)
val icon = activity.loadIcon(packageManager)
val intent = Intent(Intents.ACTION_PROVIDE_URL)
.setComponent(
ComponentName(
activity.packageName,
activity.name
)
)
ProfileProvider.External(name.toString(), summary.toString(), icon, intent)
}
listOf(ProfileProvider.File(self), ProfileProvider.Url(self)) + providers
}
}
}
@@ -0,0 +1,58 @@
package com.github.kr328.clash
import android.content.pm.PackageManager
import com.github.kr328.clash.common.compat.getDrawableCompat
import com.github.kr328.clash.common.constants.Metadata
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.design.OverrideSettingsDesign
import com.github.kr328.clash.design.model.AppInfo
import com.github.kr328.clash.design.util.toAppInfo
import com.github.kr328.clash.service.store.ServiceStore
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
class OverrideSettingsActivity : BaseActivity<OverrideSettingsDesign>() {
override suspend fun main() {
val configuration = withClash { queryOverride(Clash.OverrideSlot.Persist) }
val service = ServiceStore(this)
defer {
withClash {
patchOverride(Clash.OverrideSlot.Persist, configuration)
}
}
val design = OverrideSettingsDesign(
this,
configuration
)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
OverrideSettingsDesign.Request.ResetOverride -> {
if (design.requestResetConfirm()) {
defer {
withClash {
clearOverride(Clash.OverrideSlot.Persist)
}
}
finish()
}
}
}
}
}
}
}
}
@@ -0,0 +1,48 @@
package com.github.kr328.clash
import android.content.Intent
import android.widget.Toast
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.BaseListActivity
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.adapter.PlanDataAdapter
import com.github.kr328.clash.design.view.ActivityBarLayout
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.PlanData
import com.github.kr328.clash.network.safeApiRequestCall
class PlansActivity : BaseListActivity<PlanData>() {
override fun createAdapter(): RecyclerView.Adapter<*> {
return PlanDataAdapter(){ planData ->
// 在这里处理 item 点击事件
val intent = Intent(this, ConfigOrderActivity::class.java)
intent.putExtra("planData", planData) // 使用 Serializable 传递对象
startActivity(intent)
}
}
override fun onCreate() {
//
setRightButtonVisible {
startActivity(MyOrdersActivity::class.intent)
}
}
override suspend fun loadData(page: Int, callback: (List<PlanData>?, Throwable?) -> Unit) {
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.getplans(PreferenceManager.loginauthData)}.let {
if (it != null && it.isSuccessful) {
callback(it.body()?.data,null)
}else{
callback(null, Throwable("请求失败"))
}
}
}
}
@@ -0,0 +1,133 @@
package com.github.kr328.clash
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.PersistableBundle
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.core.bridge.Bridge
import com.github.kr328.clash.design.MainDesign
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.ProfileDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.showCustomDialog
import com.github.kr328.clash.util.stopClashService
import com.github.kr328.clash.util.withProfile
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
class ProfileActivity : BaseActivity<ProfileDesign>() {
private suspend fun queryAppVersionName(): String {
return withContext(Dispatchers.IO) {
packageManager.getPackageInfo(packageName, 0).versionName //+ "\n" + Bridge.nativeCoreVersion().replace("_", "-")
}
}
override suspend fun main() {
PreferenceManager.init(this)
val design = ProfileDesign(this)
setContentDesign(design)
design.requestdataDisible()
val ticker = ticker(TimeUnit.SECONDS.toMillis(1))
design.queryVersion(queryAppVersionName())
while (isActive) {
select<Unit> {
events.onReceive {
println("events : " + it.name)
}
design.requests.onReceive {
println("requests : " + it.name)
when (it) {
ProfileDesign.Request.OpenBuyPlan -> startActivity(PlansActivity::class.intent)
ProfileDesign.Request.OpenMyOrders -> startActivity(MyOrdersActivity::class.intent)
ProfileDesign.Request.OpenMyBlance -> startActivity(SubmitOrderActivity::class.intent)
ProfileDesign.Request.OpenMyInvites ->startActivity(InvitationActivity::class.intent)
ProfileDesign.Request.OpenCustomView ->{ startActivity(H5WebActivity::class.intent) }
/*try {
withContext(Dispatchers.Main) {
val intent = Intent(Intent.ACTION_VIEW)
intent.addCategory(Intent.CATEGORY_BROWSABLE);
intent.setData(Uri.parse(PreferenceManager.getConfigFromPreferences(this@ProfileActivity)?.telegramurl))
startActivity(intent)
}
} catch (e: Exception) {
design.showToast("当前手机未安装浏览器",ToastDuration.Long)
} */
ProfileDesign.Request.OpenLogout -> {
showCustomDialog(
title = "提示",
message = "确定要退出吗?",
positiveButtonText = "确定",
negativeButtonText = "取消",
onPositiveClick = {
LoadingDialog.show(this@ProfileActivity, "正在退出...")
// 执行确认操作
stopClashService()
// 清理缓存,清除节点信息,切换界面
launch {
withProfile {
var allProfiles = queryAll()
println(allProfiles)
if (allProfiles.isNotEmpty() && allProfiles.count() > 0) {
allProfiles.forEach {
delete(it.uuid)
println("删除节点:${it.source}")
}
//删除所有节点
}
}
}
PreferenceManager.clearData()
//2s 后执行
// 在协程作用域中执行
GlobalScope.launch(Dispatchers.Main) {
delay(2000) // 协程延迟2000毫秒(2秒)
println("协程中的延迟执行")
LoadingDialog.hide()
startActivity(LoginActivity::class.intent)
finish()
}
},
onNegativeClick = {
// 执行取消操作
}
)
}
ProfileDesign.Request.OpenGSettings -> {
//查看是否有更新
design.showToast("当前版本是最新版本",ToastDuration.Long)
}// startActivity(NetworkSettingsActivity::class.intent)
ProfileDesign.Request.OpenMyGongdan ->startActivity(GongdanActivity::class.intent)
ProfileDesign.Request.OpenCustomViewIPtest -> startActivity(H5WebActivity::class.intent.putExtra("url","https://ipcelou.com"))
ProfileDesign.Request.OpenCustomViewSpeed -> startActivity(H5WebActivity::class.intent.putExtra("url","https://fast.com/"))
}
}
}
}
}
}
@@ -0,0 +1,127 @@
package com.github.kr328.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.ProfilesDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.R
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.withContext
import java.util.*
import java.util.concurrent.TimeUnit
class ProfilesActivity : BaseActivity<ProfilesDesign>() {
override suspend fun main() {
val design = ProfilesDesign(this)
setContentDesign(design)
val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStart, Event.ProfileChanged -> {
design.fetch()
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
ProfilesDesign.Request.Create ->
startActivity(NewProfileActivity::class.intent)
ProfilesDesign.Request.UpdateAll ->
withProfile {
try {
queryAll().forEach { p ->
if (p.imported && p.type != Profile.Type.File)
update(p.uuid)
}
}
finally {
withContext(Dispatchers.Main) {
design.finishUpdateAll();
}
}
}
is ProfilesDesign.Request.Update ->
withProfile { update(it.profile.uuid) }
is ProfilesDesign.Request.Delete ->
withProfile { delete(it.profile.uuid) }
is ProfilesDesign.Request.Edit ->
startActivity(PropertiesActivity::class.intent.setUUID(it.profile.uuid))
is ProfilesDesign.Request.Active -> {
withProfile {
if (it.profile.imported)
setActive(it.profile)
else
design.requestSave(it.profile)
}
}
is ProfilesDesign.Request.Duplicate -> {
val uuid = withProfile { clone(it.profile.uuid) }
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
}
}
}
if (activityStarted) {
ticker.onReceive {
design.updateElapsed()
}
}
}
}
}
private suspend fun ProfilesDesign.fetch() {
withProfile {
patchProfiles(queryAll())
}
}
override fun onProfileUpdateCompleted(uuid: UUID?) {
if(uuid == null)
return;
launch {
var name: String? = null;
withProfile {
name = queryByUUID(uuid)?.name
}
design?.showToast(
getString(R.string.toast_profile_updated_complete, name),
ToastDuration.Long
)
}
}
override fun onProfileUpdateFailed(uuid: UUID?, reason: String?) {
if(uuid == null)
return;
launch {
var name: String? = null;
withProfile {
name = queryByUUID(uuid)?.name
}
design?.showToast(
getString(R.string.toast_profile_updated_failed, name, reason),
ToastDuration.Long
){
setAction(R.string.edit) {
startActivity(PropertiesActivity::class.intent.setUUID(uuid))
}
}
}
}
}
@@ -0,0 +1,117 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.setUUID
import com.github.kr328.clash.common.util.uuid
import com.github.kr328.clash.design.PropertiesDesign
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.service.model.Profile
import com.github.kr328.clash.util.withProfile
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
class PropertiesActivity : BaseActivity<PropertiesDesign>() {
private var canceled: Boolean = false
override suspend fun main() {
setResult(RESULT_CANCELED)
val uuid = intent.uuid ?: return finish()
val design = PropertiesDesign(this)
val original = withProfile { queryByUUID(uuid) } ?: return finish()
design.profile = original
setContentDesign(design)
defer {
canceled = true
withProfile { release(uuid) }
}
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ActivityStop -> {
val profile = design.profile
if (!canceled && profile != original) {
withProfile {
patch(profile.uuid, profile.name, profile.source, profile.interval)
}
}
}
Event.ServiceRecreated -> {
finish()
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
PropertiesDesign.Request.BrowseFiles -> {
startActivity(FilesActivity::class.intent.setUUID(uuid))
}
PropertiesDesign.Request.Commit -> {
design.verifyAndCommit()
}
}
}
}
}
}
override fun onBackPressed() {
design?.apply {
launch {
if (!progressing) {
if (requestExitWithoutSaving())
finish()
}
}
} ?: return super.onBackPressed()
}
private suspend fun PropertiesDesign.verifyAndCommit() {
when {
profile.name.isBlank() -> {
showToast(R.string.empty_name, ToastDuration.Long)
}
profile.type != Profile.Type.File && profile.source.isBlank() -> {
showToast(R.string.invalid_url, ToastDuration.Long)
}
else -> {
try {
withProcessing { updateStatus ->
withProfile {
patch(profile.uuid, profile.name, profile.source, profile.interval)
coroutineScope {
commit(profile.uuid) {
launch {
withProfile {
setActive(queryAll().first())
}
updateStatus(it)
}
}
}
}
}
setResult(RESULT_OK)
finish()
} catch (e: Exception) {
showExceptionToast(e)
}
}
}
}
}
@@ -0,0 +1,71 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.common.util.ticker
import com.github.kr328.clash.design.ProvidersDesign
import com.github.kr328.clash.design.util.showExceptionToast
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import java.util.concurrent.TimeUnit
class ProvidersActivity : BaseActivity<ProvidersDesign>() {
override suspend fun main() {
val providers = withClash { queryProviders().sorted() }
val design = ProvidersDesign(this, providers)
setContentDesign(design)
val ticker = ticker(TimeUnit.MINUTES.toMillis(1))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ProfileLoaded -> {
val newList = withClash { queryProviders().sorted() }
if (newList != providers) {
startActivity(ProvidersActivity::class.intent)
finish()
}
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
is ProvidersDesign.Request.Update -> {
launch {
try {
withClash {
updateProvider(it.provider.type, it.provider.name)
}
design.notifyChanged(it.index)
} catch (e: Exception) {
design.showExceptionToast(
getString(
R.string.format_update_provider_failure,
it.provider.name,
e.message
)
)
design.notifyUpdated(it.index)
}
}
}
}
}
if (activityStarted) {
ticker.onReceive {
design.updateElapsed()
}
}
}
}
}
}
@@ -0,0 +1,191 @@
package com.github.kr328.clash
import android.os.Handler
import android.os.Looper
import android.util.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.core.Clash
import com.github.kr328.clash.core.model.Proxy
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.ProxyDesign
import com.github.kr328.clash.design.model.ProxyState
import com.github.kr328.clash.util.withClash
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.selects.select
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import kotlinx.coroutines.withContext
class ProxyActivity : BaseActivity<ProxyDesign>() {
override suspend fun main() {
val mode = withClash { queryOverride(Clash.OverrideSlot.Session).mode }
val names = withClash { queryProxyGroupNames(uiStore.proxyExcludeNotSelectable) }
val states = List(names.size) { ProxyState("?") }
val unorderedStates = names.indices.map { names[it] to states[it] }.toMap()
val reloadLock = Semaphore(10)
setTitle("选择节点")
val design = ProxyDesign(
this,
mode,
names,
uiStore
)
setContentDesign(design)
//刷新所以Groups
//design.requests.send(ProxyDesign.Request.ReloadAll)
design.requests.send(ProxyDesign.Request.Reload(0))
while (isActive) {
select<Unit> {
events.onReceive {
when (it) {
Event.ProfileLoaded -> {
val newNames = withClash {
queryProxyGroupNames(uiStore.proxyExcludeNotSelectable)
}
if (newNames != names) {
startActivity(ProxyActivity::class.intent)
finish()
}
}
else -> Unit
}
}
design.requests.onReceive {
when (it) {
ProxyDesign.Request.ReLaunch -> {
startActivity(ProxyActivity::class.intent)
finish()
}
ProxyDesign.Request.ReloadAll -> {
names.indices.forEach { idx ->
design.requests.trySend(ProxyDesign.Request.Reload(idx))
}
}
is ProxyDesign.Request.Reload -> {
launch {
design.urlTesting = false
val group = reloadLock.withPermit {
withClash {
queryProxyGroup(names[it.index], uiStore.proxySort)
}
}
var state = states[it.index]
println(">>>>>>>>>>>> ${names}")
println(">>>>>>>>>>>> ${it.index} group: "+group + " ${group.type.name} ${group.proxies.size}")
if (group.proxies.first().name == "GLOBAL"){
println(">>>>>>>>>>>> GLOBAL ")
}
if (PreferenceManager.selectnodeName.length > 0){
state.now = PreferenceManager.selectnodeName //group.now
}else{
state.now = group.now
}
println(">>>>>>>>>>>>unorderedStates : ${unorderedStates}")
println(">>>>>>>>>>>> state: "+state)
//PreferenceManager.selectnodeName = state.now //刷新目前使用的节点名称
// >>>>>>>>>>>> state: ProxyState(now=?)
// >>>>>>>>>>>> 0 group: 距离下次重置剩余:18 天 Selector
// >>>>>>>>>>>>{代理=ProxyState(now=距离下次重置剩余:18 天), 电报=ProxyState(now=?), 油管=ProxyState(now=?), 奈飞=ProxyState(now=?), b站=ProxyState(now=?), 规则外路由选择=ProxyState(now=?)}
design.updateGroup(
it.index,
group.proxies,
group.type == Proxy.Type.Selector,
state,
unorderedStates
)
design.finishreferesh()
/*val group = reloadLock.withPermit {
withClash {
queryProxyGroup(names[it.index], uiStore.proxySort)
}
}
val state = states[it.index]
state.now = group.now
design.updateGroup(
it.index,
group.proxies,
group.type == Proxy.Type.Selector,
state,
unorderedStates
) */
}
}
is ProxyDesign.Request.Select -> {
withClash {
patchSelector(names[it.index], it.name)
states[it.index].now = it.name
}
PreferenceManager.selectnodeName = it.name
design.requestRedrawVisible()
launch {
finish()
}
}
is ProxyDesign.Request.UrlTest -> {
launch {
withClash {
healthCheck(names[it.index])
}
design.requests.send(ProxyDesign.Request.Reload(it.index))
}
}
is ProxyDesign.Request.PatchMode -> {
// design.showModeSwitchTips()
withClash {
val o = queryOverride(Clash.OverrideSlot.Session)
o.mode = it.mode
patchOverride(Clash.OverrideSlot.Session, o)
}
}
}
}
}
}
// 使用协程延迟 500ms 后执行
/* GlobalScope.launch(Dispatchers.Main) {
delay(1500) // 延迟 500ms
launch {
withClash {
healthCheck(names[0])
}
design.requests.send(ProxyDesign.Request.Reload(0))
}
}*/
}
}
@@ -0,0 +1,217 @@
package com.github.kr328.clash
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
import android.os.Build
import android.os.Bundle
import com.github.kr328.clash.common.util.intent
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.compat.isAllowForceDarkCompat
import com.github.kr328.clash.common.compat.isLightNavigationBarCompat
import com.github.kr328.clash.common.compat.isLightStatusBarsCompat
import com.github.kr328.clash.common.compat.isSystemBarsTranslucentCompat
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.databinding.ActivityLoginBinding
import com.github.kr328.clash.design.databinding.ActivityProfileBinding
import com.github.kr328.clash.design.databinding.ActivityRegisterBinding
import com.github.kr328.clash.design.ui.DayNight
import com.github.kr328.clash.design.util.layoutInflater
import com.github.kr328.clash.design.util.resolveThemedBoolean
import com.github.kr328.clash.design.util.resolveThemedColor
import com.github.kr328.clash.design.util.root
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiClientConfig
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.ConfigResponse
import com.github.kr328.clash.network.LoginRequest
import com.github.kr328.clash.network.LoginResponse
import com.github.kr328.clash.network.safeApiRequestCall
import com.github.kr328.clash.utity.LoadingDialog
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.json.JSONException
import org.json.JSONObject
class RegisterActivity : AppCompatActivity() {
private lateinit var apiService: ApiService
private var dayNight: DayNight = DayNight.Day
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivityRegisterBinding
.inflate(this.layoutInflater, this.root, false)
setContentView(binding.root)
applyDayNight()
PreferenceManager.init(this)
binding.loginLoginButton.setOnClickListener {
val email = binding.loginEmailEditText.text.toString()
val password = binding.loginPasswordEditText.text.toString()
val password2 = binding.loginPasswordEditText2.text.toString()
val isAgreementChecked = binding.loginAgreementCheckBox.isChecked
if (email.isEmpty() || password.isEmpty() || password2.isEmpty()) {
Toast.makeText(this, "请输入邮箱和密码", Toast.LENGTH_SHORT).show()
} else if (!isAgreementChecked) {
Toast.makeText(this, "请同意隐私政策和用户协议", Toast.LENGTH_SHORT).show()
} else {
// Hide the keyboard
val inputMethodManager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
val currentFocusView = currentFocus
if (currentFocusView != null) {
inputMethodManager.hideSoftInputFromWindow(currentFocusView.windowToken, 0)
}
performLogin(email, password)
}
}
binding.loginForgotPassword.setOnClickListener {
// Navigate to Forgot Password screen or show dialog
}
binding.loginRegister.setOnClickListener {
// Navigate to Register screen
finish()
}
apiService = ApiClientConfig.retrofit.create(ApiService::class.java)
//获取 Config 数据
CoroutineScope(Dispatchers.IO).launch {
safeApiRequestCall { apiService.getConfig()}.let {
if (it != null && it.isSuccessful) {
val response: ConfigResponse = it.body() ?: throw Exception("Response body is null")
if (response.code == 1 ) {
PreferenceManager.saveConfigToPreferences( response)
//重新初始化 ApiClint
}
println("API Response: ${it.body()}")
} else {
println("API Error: 请求失败")
}
}
}
}
/**
* withContext(Dispatchers.Main) {
* if (response.isSuccessful) {
* val userData = response.body()
* // 更新 UI
* } else {
* Toast.makeText(this@MainActivity, "Failed to fetch user data", Toast.LENGTH_SHORT).show()
* }
* }
* */
private fun applyDayNight(config: Configuration = resources.configuration) {
//val dayNight = theme.applyStyle(R.style.AppThemeLight, true) //默认白天模式
/*
val dayNight = queryDayNight(config)
when (dayNight) {
DayNight.Night -> theme.applyStyle(R.style.AppThemeDark, true)
DayNight.Day -> theme.applyStyle(R.style.AppThemeLight, true)
}*/
window.isAllowForceDarkCompat = false
window.isSystemBarsTranslucentCompat = true
window.statusBarColor = resolveThemedColor(android.R.attr.statusBarColor)
window.navigationBarColor = resolveThemedColor(android.R.attr.navigationBarColor)
if (Build.VERSION.SDK_INT >= 23) {
window.isLightStatusBarsCompat = resolveThemedBoolean(android.R.attr.windowLightStatusBar)
}
if (Build.VERSION.SDK_INT >= 27) {
window.isLightNavigationBarCompat = resolveThemedBoolean(android.R.attr.windowLightNavigationBar)
}
this.dayNight = DayNight.Night //dayNight
}
private fun performLogin(email: String, password: String) {
// Perform login logic here, possibly calling an API
// Show the loading indicator with a custom message
LoadingDialog.show(this, "正在注册...")
val apiServiceApp = ApiClient.retrofit.create(ApiService::class.java)
//获取 Config 数据
CoroutineScope(Dispatchers.IO).launch {
safeApiRequestCall {
apiServiceApp.registerUser(LoginRequest(email,password))}.let {
withContext(Dispatchers.Main) {
LoadingDialog.hide()
}
if (it != null &&
it.isSuccessful) {
println("API Response: ${it.code()} ${it.body()} ${it.raw().body}")
val response: LoginResponse = it.body() ?: throw Exception("Response body is null")
if (response.data != null){
PreferenceManager.loginemail = email
PreferenceManager.loginToken = response.data?.token ?: ""
PreferenceManager.loginauthData = response.data?.auth_data ?: ""
PreferenceManager.isLoginin = true
// saveLoginMailToken(this@LoginActivity,email)
// saveLoginToken(this@LoginActivity, response.data.token ?: "")
// saveLoginAuthData(this@LoginActivity, response.data.auth_data ?: "")
// saveLoginStatus(this@LoginActivity,true)
//获取用户信息
withContext(Dispatchers.Main) {
val intent = Intent(this@RegisterActivity, MainActivity::class.java)
intent.flags =
Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
startActivity(intent)
finish() // It
}
}
}else{
// Handle error response, even if it's 422
try {
val errorResponse = it?.errorBody()?.string() ?: ""
val errorJson = JSONObject(errorResponse)
val message = errorJson.optString("message")
// Process the error message or show it to the user
if (!message.isNullOrEmpty()) {
withContext(Dispatchers.Main) {
Toast.makeText(this@RegisterActivity, "注册失败:${message}", Toast.LENGTH_LONG).show()
}
}
println( "Message: $message, ")
} catch (e: JSONException) {
// Handle JSON parsing error
withContext(Dispatchers.Main) {
Toast.makeText(this@RegisterActivity, "注册失败:${e.message}", Toast.LENGTH_LONG).show()
}
}
}
}
}
}
}
@@ -0,0 +1,18 @@
package com.github.kr328.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.service.StatusProvider
import com.github.kr328.clash.util.startClashService
class RestartReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_MY_PACKAGE_REPLACED -> {
if (StatusProvider.shouldStartClashOnBoot)
context.startClashService()
}
}
}
}
@@ -0,0 +1,34 @@
package com.github.kr328.clash
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.SettingsDesign
import kotlinx.coroutines.isActive
import kotlinx.coroutines.selects.select
class SettingsActivity : BaseActivity<SettingsDesign>() {
override suspend fun main() {
val design = SettingsDesign(this)
setContentDesign(design)
while (isActive) {
select<Unit> {
events.onReceive {
}
design.requests.onReceive {
when (it) {
SettingsDesign.Request.StartApp ->
startActivity(AppSettingsActivity::class.intent)
SettingsDesign.Request.StartNetwork ->
startActivity(NetworkSettingsActivity::class.intent)
SettingsDesign.Request.StartOverride ->
startActivity(OverrideSettingsActivity::class.intent)
SettingsDesign.Request.StartMetaFeature ->
startActivity(MetaFeatureSettingsActivity::class.intent)
}
}
}
}
}
}
@@ -0,0 +1,36 @@
package com.github.kr328.clash
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import androidx.appcompat.app.AppCompatActivity
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.databinding.ActivityLoginBinding
import com.github.kr328.clash.design.databinding.ActivitySplashBinding
import com.github.kr328.clash.design.util.root
import com.github.kr328.clash.network.ApiService
class SplashActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = ActivitySplashBinding
.inflate(this.layoutInflater, this.root, false)
setContentView(binding.root)
PreferenceManager.init(this)
Handler(Looper.getMainLooper()).postDelayed({
// Hide the loading indicator
startActivity(LoginActivity::class.intent)
finish()
}, 1000) // Simulating a network delay
}
}
@@ -0,0 +1,16 @@
package com.github.kr328.clash
import com.github.kr328.clash.design.SubmitOrderViewDesign
class SubmitOrderActivity : BaseActivity<SubmitOrderViewDesign>() {
override suspend fun main() {
val design = SubmitOrderViewDesign(this)
setContentDesign(design)
//
val trade_no = intent.getStringExtra("trade_no")
design.fetchTradeInfo(trade_no)
}
}
@@ -0,0 +1,258 @@
package com.github.kr328.clash
import android.graphics.Color
import android.graphics.Rect
import android.provider.Settings.Global
import android.view.Gravity
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.FrameLayout
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.RecyclerView
import com.github.kr328.clash.design.BaseListActivity
import com.github.kr328.clash.design.PreferenceManager
import com.github.kr328.clash.design.adapter.TicketDetailAdapter
import com.github.kr328.clash.design.adapter.TicketsDataAdapter
import com.github.kr328.clash.design.ui.ToastDuration
import com.github.kr328.clash.network.ApiClient
import com.github.kr328.clash.network.ApiService
import com.github.kr328.clash.network.PublicResponse
import com.github.kr328.clash.network.TicketMessage
import com.github.kr328.clash.network.safeApiRequestCall
import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.URLEncoder
class TicketDetailActivity : BaseListActivity<TicketMessage>() {
override fun createAdapter(): RecyclerView.Adapter<*> {
return TicketDetailAdapter(){ planData ->
// 在这里处理 item 点击事件
}
}
override fun onCreate() {
}
fun encodeString(input: String): String {
val encoded = StringBuilder()
input.forEach { char ->
when {
char.isWhitespace() -> {
// 编码空格为 %20
encoded.append("")
}
char.isLetterOrDigit() || char.toInt() in 0x4E00..0x9FFF -> {
// 保留字母、数字和中文字符
encoded.append(char)
}
else -> {
// 其他字符使用 URLEncoder 编码
encoded.append(URLEncoder.encode(char.toString(), "UTF-8"))
}
}
}
return encoded.toString()
}
fun onCreateInputView() {
// 将 dp 转换为 px 的实用函数
fun dpToPx(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}
// 创建顶部分割线
val topDivider = View(this).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
dpToPx(1) // 设置分割线高度为 1dp
) .apply {
gravity = Gravity.TOP // 将分割线置于顶部
}
setBackgroundColor(ContextCompat.getColor(this@TicketDetailActivity, android.R.color.darker_gray)) // 设置分割线颜色
}
val rootLayout = FrameLayout(this).apply {
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
addView(topDivider) // 将分割线添加到根布局
}
// Create the input container layout programmatically
// Create the input container layout programmatically
val inputContainer = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
setPadding(8, 8, 8, 8)
// setBackgroundColor(ContextCompat.getColor(this@TicketDetailActivity, android.R.color.white))
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.WRAP_CONTENT,
Gravity.BOTTOM // Aligns the input container to the bottom
)
setBackgroundColor(ContextCompat.getColor(this@TicketDetailActivity, R.color.white))
}
// Create the EditText for message input
val editText = EditText(this).apply {
hint = "输入内容回复工单..."
setHintTextColor(ContextCompat.getColor(this@TicketDetailActivity, R.color.gray)) // Set the placeholder color
// Utility function to convert dp to pixels
fun dpToPx2(dp: Int): Int {
return (dp * resources.displayMetrics.density).toInt()
}
textSize = 14f
// Set other EditText properties
setTextColor(Color.BLACK) // Text color
inputType = android.text.InputType.TYPE_TEXT_FLAG_MULTI_LINE
setTextColor(ContextCompat.getColor(this@TicketDetailActivity, R.color.black))
maxLines = 1
// background = ContextCompat.getDrawable(this@TicketDetailActivity, R.drawable.rounded_button_background)
// 设置内边距为 4dp(上下左右)
// val padding = dpToPx(8)
// setPadding(padding,0 , padding, 0)
layoutParams = LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f // Weight to make it fill available width
)
}
// Create the Send Button
val sendButton = Button(this).apply {
text = "回复"
setTextColor(ContextCompat.getColor(this@TicketDetailActivity, android.R.color.white)) // Set text color for better contrast
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.WRAP_CONTENT,
LinearLayout.LayoutParams.WRAP_CONTENT
)
}
// Set the background tint programmatically
ViewCompat.setBackgroundTintList(
sendButton,
ContextCompat.getColorStateList(this, R.color.blue)
)
// Add EditText and Button to the input container
inputContainer.addView(editText)
inputContainer.addView(sendButton)
sendButton.setOnClickListener {
val message = editText.text.toString()
if (message.isNullOrEmpty()){
}else{
val fieldMap = mutableMapOf<String, String>()
val messageProperty1 = encodeString(message)
val ticketid = intent.getIntExtra("ticketid",0)
fieldMap.put("message",""+messageProperty1)
fieldMap.put("id",""+ticketid)
GlobalScope.launch {
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.replyTicket(PreferenceManager.loginauthData, fieldMap)}.let {
if (it != null && it.isSuccessful) {
withContext(Dispatchers.Main) {
Toast.makeText(this@TicketDetailActivity, "回复成功", Toast.LENGTH_SHORT).show()
editText.setText("")
}
refereshData()
}else{
val errorinfo = it?.errorBody()?.string()
if (errorinfo != null){
//解析错错误信息
val gson = Gson()
val submitResponse = gson.fromJson(errorinfo, PublicResponse::class.java)
if (submitResponse?.message != null){
withContext(Dispatchers.Main) {
Toast.makeText(this@TicketDetailActivity, "回复失败: ${submitResponse.message}", Toast.LENGTH_SHORT).show()
}
}
}else{
withContext(Dispatchers.Main) {
Toast.makeText(this@TicketDetailActivity, "回复失败", Toast.LENGTH_SHORT).show()
}
}
}
}
}
}
}
// 将分割线添加到输入容器顶部
rootLayout.addView(inputContainer)
// rootLayout.addView(topDivider) // 将分割线添加到根布局
// Add the input container to the root layout
mainBinding?.mainview?.addView(rootLayout)
rootLayout.viewTreeObserver.addOnGlobalLayoutListener {
val rect = Rect()
rootLayout.getWindowVisibleDisplayFrame(rect)
val screenHeight = rootLayout.rootView.height
val keypadHeight = screenHeight - rect.bottom
val params = inputContainer.layoutParams as FrameLayout.LayoutParams
if (keypadHeight > screenHeight * 0.15) {
// Keyboard is visible; adjust inputContainer to stay above it
params.bottomMargin = keypadHeight
} else {
// Keyboard is hidden; reset inputContainer to bottom
params.bottomMargin = 0
}
inputContainer.layoutParams = params
}
}
override suspend fun loadData(page: Int, callback: (List<TicketMessage>?, Throwable?) -> Unit) {
val ticketid = intent.getIntExtra("ticketid",0)
val apiService = ApiClient.retrofit.create(ApiService::class.java)
safeApiRequestCall { apiService.getTicketsByTID(PreferenceManager.loginauthData,ticketid)}.let {
if (it != null && it.isSuccessful) {
withContext(Dispatchers.Main) {
referTitle(it.body()?.data?.subject ?: "")
}
if(it.body()?.data?.status == 0){
withContext(Dispatchers.Main) {
onCreateInputView()
}
}
callback(it.body()?.data?.message,null)
}else{
callback(null, Throwable("请求失败"))
}
}
}
}
@@ -0,0 +1,104 @@
package com.github.kr328.clash
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.drawable.Icon
import android.os.Build
import android.service.quicksettings.Tile
import android.service.quicksettings.TileService
import androidx.annotation.RequiresApi
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.constants.Permissions
import com.github.kr328.clash.remote.StatusClient
import com.github.kr328.clash.util.startClashService
import com.github.kr328.clash.util.stopClashService
@RequiresApi(Build.VERSION_CODES.N)
class TileService : TileService() {
private var currentProfile = ""
private var clashRunning = false
override fun onClick() {
val tile = qsTile ?: return
when (tile.state) {
Tile.STATE_INACTIVE -> {
startClashService()
}
Tile.STATE_ACTIVE -> {
stopClashService()
}
}
}
override fun onStartListening() {
super.onStartListening()
registerReceiver(
receiver,
IntentFilter().apply {
addAction(Intents.ACTION_CLASH_STARTED)
addAction(Intents.ACTION_CLASH_STOPPED)
addAction(Intents.ACTION_PROFILE_LOADED)
addAction(Intents.ACTION_SERVICE_RECREATED)
},
Permissions.RECEIVE_SELF_BROADCASTS,
null
)
val name = StatusClient(this).currentProfile()
clashRunning = name != null
currentProfile = name ?: ""
updateTile()
}
override fun onStopListening() {
super.onStopListening()
unregisterReceiver(receiver)
}
private fun updateTile() {
val tile = qsTile ?: return
tile.state = if (clashRunning)
Tile.STATE_ACTIVE
else
Tile.STATE_INACTIVE
tile.label = if (currentProfile.isEmpty())
getText(R.string.launch_name)
else
currentProfile
tile.icon = Icon.createWithResource(this, R.drawable.ic_logo_service)
tile.updateTile()
}
private val receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.action) {
Intents.ACTION_CLASH_STARTED -> {
clashRunning = true
currentProfile = ""
}
Intents.ACTION_CLASH_STOPPED, Intents.ACTION_SERVICE_RECREATED -> {
clashRunning = false
currentProfile = ""
}
Intents.ACTION_PROFILE_LOADED -> {
currentProfile = StatusClient(this@TileService).currentProfile() ?: ""
}
}
updateTile()
}
}
}
@@ -0,0 +1,52 @@
package com.github.kr328.clash.log
import androidx.collection.CircularArray
import com.github.kr328.clash.core.model.LogMessage
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
class LogcatCache {
data class Snapshot(val messages: List<LogMessage>, val removed: Int, val appended: Int)
private val array = CircularArray<LogMessage>(CAPACITY)
private val lock = Mutex()
private var removed: Int = 0
private var appended: Int = 0
suspend fun append(msg: LogMessage) {
lock.withLock {
if (array.size() >= CAPACITY) {
array.removeFromStart(1)
removed++
appended--
}
array.addLast(msg)
appended++
}
}
suspend fun snapshot(full: Boolean): Snapshot? {
return lock.withLock {
if (!full && removed == 0 && appended == 0) {
return@withLock null
}
Snapshot(
List(array.size()) { array[it] },
removed,
if (full) array.size() + appended else appended
).also {
removed = 0
appended = 0
}
}
}
companion object {
const val CAPACITY = 128
}
}
@@ -0,0 +1,25 @@
package com.github.kr328.clash.log
import android.content.Context
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.util.format
import java.io.BufferedWriter
import java.io.Writer
import java.util.*
class LogcatFilter(output: Writer, private val context: Context) : BufferedWriter(output) {
fun writeHeader(time: Date) {
appendLine("# Capture on ${time.format(context)}")
}
fun writeMessage(message: LogMessage) {
val time = message.time.format(context, includeDate = false)
val level = message.level.name
appendLine(FORMAT.format(time, level, message.message))
}
companion object {
private const val FORMAT = "%12s %7s: %s"
}
}
@@ -0,0 +1,44 @@
package com.github.kr328.clash.log
import android.content.Context
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.util.logsDir
import java.io.BufferedReader
import java.io.FileReader
import java.util.*
class LogcatReader(context: Context, file: LogFile) : AutoCloseable {
private val reader = BufferedReader(FileReader(context.logsDir.resolve(file.fileName)))
override fun close() {
reader.close()
}
fun readAll(): List<LogMessage> {
var lastTime = Date(0)
return reader.lineSequence()
.map { it.trim() }
.filter { !it.startsWith("#") }
.map { it.split(":", limit = 3) }
.map {
val time = it[0].toLongOrNull()?.let { Date(it) } ?: lastTime
val logMessage = if (it[0].toLongOrNull() != null) {
LogMessage(
time = time,
level = LogMessage.Level.valueOf(it[1]),
message = it[2]
)
} else {
LogMessage(
time = time,
level = LogMessage.Level.Warning, // or any default level
message = it.joinToString(":")
)
}
lastTime = time
logMessage
}
.toList()
}
}
@@ -0,0 +1,25 @@
package com.github.kr328.clash.log
import android.content.Context
import com.github.kr328.clash.core.model.LogMessage
import com.github.kr328.clash.design.model.LogFile
import com.github.kr328.clash.util.logsDir
import java.io.BufferedWriter
import java.io.FileWriter
class LogcatWriter(context: Context) : AutoCloseable {
private val file = LogFile.generate()
private val writer = BufferedWriter(FileWriter(context.logsDir.resolve(file.fileName)))
override fun close() {
writer.close()
}
fun appendMessage(message: LogMessage) {
writer.appendLine(FORMAT.format(message.time.time, message.level.name, message.message))
}
companion object {
private const val FORMAT = "%d:%s:%s"
}
}
@@ -0,0 +1,32 @@
package com.github.kr328.clash.log
object SystemLogcat {
private val command = arrayOf(
"logcat",
"-d",
"-s",
"Go",
"DEBUG",
"AndroidRuntime",
"UUVPN",
"LwIP",
)
fun dumpCrash(): String {
return try {
val process = Runtime.getRuntime().exec(command)
val result = process.inputStream.use { stream ->
stream.reader().readLines()
.filterNot { it.startsWith("------") }
.joinToString("\n")
}
process.waitFor()
result.trim()
} catch (e: Exception) {
""
}
}
}
@@ -0,0 +1,130 @@
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 android.os.Build
import androidx.core.content.ContextCompat
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.log.Log
import java.util.*
class Broadcasts(private val context: Application) {
interface Observer {
fun onServiceRecreated()
fun onStarted()
fun onStopped(cause: String?)
fun onProfileChanged()
fun onProfileUpdateCompleted(uuid: UUID?)
fun onProfileUpdateFailed(uuid: UUID?, reason: String?)
fun onProfileLoaded()
}
var clashRunning: Boolean = false
private var registered = false
private val receivers = mutableListOf<Observer>()
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.`package` != context?.packageName)
return
when (intent?.action) {
Intents.ACTION_SERVICE_RECREATED -> {
clashRunning = false
receivers.forEach {
it.onServiceRecreated()
}
}
Intents.ACTION_CLASH_STARTED -> {
clashRunning = true
receivers.forEach {
it.onStarted()
}
}
Intents.ACTION_CLASH_STOPPED -> {
clashRunning = false
receivers.forEach {
it.onStopped(intent.getStringExtra(Intents.EXTRA_STOP_REASON))
}
}
Intents.ACTION_PROFILE_CHANGED ->
receivers.forEach {
it.onProfileChanged()
}
Intents.ACTION_PROFILE_UPDATE_COMPLETED ->
receivers.forEach {
it.onProfileUpdateCompleted(
UUID.fromString(intent.getStringExtra(Intents.EXTRA_UUID)))
}
Intents.ACTION_PROFILE_UPDATE_FAILED ->
receivers.forEach {
it.onProfileUpdateFailed(
UUID.fromString(intent.getStringExtra(Intents.EXTRA_UUID)),
intent.getStringExtra(Intents.EXTRA_FAIL_REASON))
}
Intents.ACTION_PROFILE_LOADED -> {
receivers.forEach {
it.onProfileLoaded()
}
}
}
}
}
fun addObserver(observer: Observer) {
receivers.add(observer)
}
fun removeObserver(observer: Observer) {
receivers.remove(observer)
}
fun register() {
if (registered)
return
try {
val filter = IntentFilter().apply {
addAction(Intents.ACTION_SERVICE_RECREATED)
addAction(Intents.ACTION_CLASH_STARTED)
addAction(Intents.ACTION_CLASH_STOPPED)
addAction(Intents.ACTION_PROFILE_CHANGED)
addAction(Intents.ACTION_PROFILE_UPDATE_COMPLETED)
addAction(Intents.ACTION_PROFILE_UPDATE_FAILED)
addAction(Intents.ACTION_PROFILE_LOADED)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
context.registerReceiver(broadcastReceiver, filter, Context.RECEIVER_EXPORTED)
} else {
context.registerReceiver(broadcastReceiver, filter)
}
registered = true
clashRunning = StatusClient(context).currentProfile() != null
} catch (e: Exception) {
Log.w("Register global receiver: $e", e)
}
}
fun unregister() {
if (!registered)
return
try {
context.unregisterReceiver(broadcastReceiver)
registered = false
clashRunning = false
} catch (e: Exception) {
Log.w("Unregister global receiver: $e", e)
}
}
}
@@ -0,0 +1,94 @@
@file:Suppress("BlockingMethodInNonBlockingContext")
package com.github.kr328.clash.remote
import android.content.Context
import android.net.Uri
import com.github.kr328.clash.common.constants.Authorities
import com.github.kr328.clash.design.model.File
import com.github.kr328.clash.util.copyContentTo
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import android.provider.DocumentsContract as DC
class FilesClient(private val context: Context) {
suspend fun list(parentDocumentId: String): List<File> = withContext(Dispatchers.IO) {
val uri = DC.buildChildDocumentsUri(Authorities.FILES_PROVIDER, parentDocumentId)
context.contentResolver.query(uri, FilesProjection, null, null, null)?.use { cursor ->
val idIndex = cursor.getColumnIndex(DC.Document.COLUMN_DOCUMENT_ID)
val nameIndex = cursor.getColumnIndex(DC.Document.COLUMN_DISPLAY_NAME)
val sizeIndex = cursor.getColumnIndex(DC.Document.COLUMN_SIZE)
val lastModified = cursor.getColumnIndex(DC.Document.COLUMN_LAST_MODIFIED)
val mimeTypeIndex = cursor.getColumnIndex(DC.Document.COLUMN_MIME_TYPE)
cursor.moveToFirst()
List(cursor.count) {
File(
id = cursor.getString(idIndex),
name = cursor.getString(nameIndex),
size = cursor.getLong(sizeIndex),
lastModified = cursor.getLong(lastModified),
isDirectory = cursor.getString(mimeTypeIndex) == DC.Document.MIME_TYPE_DIR,
).also {
cursor.moveToNext()
}
}.sortedWith(compareBy({ !it.isDirectory }, { it.name }))
} ?: emptyList()
}
suspend fun renameDocument(documentId: String, name: String) = withContext(Dispatchers.IO) {
val uri = buildDocumentUri(documentId)
DC.renameDocument(context.contentResolver, uri, name)
}
suspend fun deleteDocument(documentId: String) = withContext(Dispatchers.IO) {
val uri = buildDocumentUri(documentId)
DC.deleteDocument(context.contentResolver, uri)
}
suspend fun importDocument(
parentDocumentId: String,
source: Uri,
name: String
) = withContext(Dispatchers.IO) {
val target = buildDocumentUri("$parentDocumentId/$name")
context.contentResolver.copyContentTo(source, target)
}
suspend fun copyDocument(
documentId: String,
source: Uri
) {
val target = buildDocumentUri(documentId)
context.contentResolver.copyContentTo(source, target)
}
suspend fun copyDocument(
target: Uri,
documentId: String
) {
val source = buildDocumentUri(documentId)
context.contentResolver.copyContentTo(source, target)
}
fun buildDocumentUri(documentId: String): Uri {
return DC.buildDocumentUri(Authorities.FILES_PROVIDER, documentId)
}
companion object {
private val FilesProjection = arrayOf(
DC.Document.COLUMN_DOCUMENT_ID,
DC.Document.COLUMN_DISPLAY_NAME,
DC.Document.COLUMN_SIZE,
DC.Document.COLUMN_LAST_MODIFIED,
DC.Document.COLUMN_MIME_TYPE,
)
}
}
@@ -0,0 +1,73 @@
package com.github.kr328.clash.remote
import android.content.Context
import android.content.Intent
import com.github.kr328.clash.ApkBrokenActivity
import com.github.kr328.clash.AppCrashedActivity
import com.github.kr328.clash.common.Global
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.store.AppStore
import com.github.kr328.clash.util.ApplicationObserver
import com.github.kr328.clash.util.verifyApk
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
object Remote {
val broadcasts: Broadcasts = Broadcasts(Global.application)
val service: Service = Service(Global.application) {
ApplicationObserver.createdActivities.forEach { it.finish() }
val intent = AppCrashedActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
Global.application.startActivity(intent)
}
private val visible = Channel<Boolean>(Channel.CONFLATED)
fun launch() {
ApplicationObserver.attach(Global.application)
ApplicationObserver.onVisibleChanged {
if(it) {
Log.d("App becomes visible")
service.bind()
broadcasts.register()
}
else {
Log.d("App becomes invisible")
service.unbind()
broadcasts.unregister()
}
}
Global.launch(Dispatchers.IO) {
verifyApp()
}
}
private suspend fun verifyApp() {
val context = Global.application
val store = AppStore(context)
val updatedAt = getLastUpdated(context)
if (store.updatedAt != updatedAt) {
if (!context.verifyApk()) {
ApplicationObserver.createdActivities.forEach { it.finish() }
val intent = ApkBrokenActivity::class.intent
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
return context.startActivity(intent)
} else {
store.updatedAt = updatedAt
}
}
}
private fun getLastUpdated(context: Context): Long {
return context.packageManager.getPackageInfo(context.packageName, 0).lastUpdateTime
}
}
@@ -0,0 +1,74 @@
package com.github.kr328.clash.remote
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
class Resource<T> {
private interface Callback<T> {
fun accept(value: T)
}
private val pending: MutableSet<Callback<T>> = mutableSetOf()
private var value: T? = null
suspend fun get(): T {
return suspendCancellableCoroutine { ctx ->
val callback = object : Callback<T> {
override fun accept(value: T) {
ctx.resume(value)
}
}
ctx.invokeOnCancellation {
cancel(callback)
}
get(callback)
}
}
fun set(v: T?) {
setAndNotify(v)
}
fun reset(v: T) {
resetIfMatched(v)
}
@Synchronized
private fun get(callback: Callback<T>) {
val v = value
if (v == null) {
pending.add(callback)
} else {
callback.accept(v)
}
}
@Synchronized
private fun setAndNotify(value: T?) {
this.value = value
if (value != null) {
pending.forEach {
it.accept(value)
}
pending.clear()
}
}
@Synchronized
private fun resetIfMatched(value: T) {
if (this.value === value) {
this.value = null
}
}
@Synchronized
private fun cancel(callback: Callback<T>) {
pending.remove(callback)
}
}
@@ -0,0 +1,59 @@
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 com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.service.RemoteService
import com.github.kr328.clash.service.remote.IRemoteService
import com.github.kr328.clash.service.remote.unwrap
import com.github.kr328.clash.util.unbindServiceSilent
import java.util.concurrent.TimeUnit
class Service(private val context: Application, val crashed: () -> Unit) {
val remote = Resource<IRemoteService>()
private val connection = object : ServiceConnection {
private var lastCrashed: Long = -1
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
remote.set(service.unwrap(IRemoteService::class))
}
override fun onServiceDisconnected(name: ComponentName?) {
remote.set(null)
if (System.currentTimeMillis() - lastCrashed < TOGGLE_CRASHED_INTERVAL) {
unbind()
crashed()
}
lastCrashed = System.currentTimeMillis()
Log.w("RemoteService killed or crashed")
}
}
fun bind() {
try {
context.bindService(RemoteService::class.intent, connection, Context.BIND_AUTO_CREATE)
} catch (e: Exception) {
unbind()
crashed()
}
}
fun unbind() {
context.unbindServiceSilent(connection)
remote.set(null)
}
companion object {
private val TOGGLE_CRASHED_INTERVAL = TimeUnit.SECONDS.toMillis(10)
}
}
@@ -0,0 +1,34 @@
package com.github.kr328.clash.remote
import android.content.Context
import android.net.Uri
import com.github.kr328.clash.common.constants.Authorities
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.service.StatusProvider
class StatusClient(private val context: Context) {
private val uri: Uri
get() {
return Uri.Builder()
.scheme("content")
.authority(Authorities.STATUS_PROVIDER)
.build()
}
fun currentProfile(): String? {
return try {
val result = context.contentResolver.call(
uri,
StatusProvider.METHOD_CURRENT_PROFILE,
null,
null
)
result?.getString("name")
} catch (e: Exception) {
Log.w("Query current profile: $e", e)
null
}
}
}
@@ -0,0 +1,22 @@
package com.github.kr328.clash.store
import android.content.Context
import com.github.kr328.clash.common.store.Store
import com.github.kr328.clash.common.store.asStoreProvider
class AppStore(context: Context) {
private val store = Store(
context
.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
.asStoreProvider()
)
var updatedAt: Long by store.long(
key = "updated_at",
defaultValue = -1,
)
companion object {
private const val FILE_NAME = "app"
}
}
@@ -0,0 +1,29 @@
package com.github.kr328.clash.store
import android.content.Context
import com.github.kr328.clash.common.store.Store
import com.github.kr328.clash.common.store.asStoreProvider
class TipsStore(context: Context) {
private val store = Store(
context
.getSharedPreferences(FILE_NAME, Context.MODE_PRIVATE)
.asStoreProvider()
)
var requestDonate: Boolean by store.boolean(
key = "request_donate",
defaultValue = true,
)
var primaryVersion: Int by store.int(
key = "primary_version",
defaultValue = -1,
)
companion object {
const val CURRENT_PRIMARY_VERSION = 1
private const val FILE_NAME = "tips"
}
}
@@ -0,0 +1,44 @@
package com.github.kr328.clash.util
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import kotlinx.coroutines.NonCancellable
import kotlinx.coroutines.withContext
class ActivityResultLifecycle : LifecycleOwner {
private val lifecycle = LifecycleRegistry(this)
init {
lifecycle.currentState = Lifecycle.State.INITIALIZED
}
override fun getLifecycle(): Lifecycle {
return lifecycle
}
suspend fun <T> use(block: suspend (lifecycle: ActivityResultLifecycle, start: () -> Unit) -> T): T {
return try {
markCreated()
block(this, this::markStarted)
} finally {
withContext(NonCancellable) {
markDestroy()
}
}
}
private fun markCreated() {
lifecycle.currentState = Lifecycle.State.CREATED
}
private fun markStarted() {
lifecycle.currentState = Lifecycle.State.STARTED
lifecycle.currentState = Lifecycle.State.RESUMED
}
private fun markDestroy() {
lifecycle.currentState = Lifecycle.State.DESTROYED
}
}
@@ -0,0 +1,85 @@
package com.github.kr328.clash.util
import android.app.Activity
import android.app.Application
import android.content.Context
import android.os.Build
import android.os.Bundle
import java.io.File
import java.util.zip.ZipFile
object ApplicationObserver {
private val _createdActivities: MutableSet<Activity> = mutableSetOf()
private val _visibleActivities: MutableSet<Activity> = mutableSetOf()
private var visibleChanged: (Boolean) -> Unit = {}
private var appVisible = false
private set(value) {
if (field != value) {
field = value
visibleChanged(value)
}
}
val createdActivities: Set<Activity>
get() = _createdActivities
private val activityObserver = object : Application.ActivityLifecycleCallbacks {
@Synchronized
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
_createdActivities.add(activity)
}
@Synchronized
override fun onActivityDestroyed(activity: Activity) {
_createdActivities.remove(activity)
_visibleActivities.remove(activity)
appVisible = _visibleActivities.isNotEmpty()
}
override fun onActivityStarted(activity: Activity) {
_visibleActivities.add(activity)
appVisible = true
}
override fun onActivityStopped(activity: Activity) {
_visibleActivities.remove(activity)
appVisible = _visibleActivities.isNotEmpty()
}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
}
fun onVisibleChanged(visibleChanged: (Boolean) -> Unit) {
this.visibleChanged = visibleChanged
}
fun attach(application: Application) {
application.registerActivityLifecycleCallbacks(activityObserver)
}
}
fun Context.verifyApk(): Boolean {
return try {
val info = applicationInfo
val sources = info.splitSourceDirs ?: arrayOf(info.sourceDir) ?: return false
val regexNativeLibrary = Regex("lib/(\\S+)/libclash.so")
val availableAbi = Build.SUPPORTED_ABIS.toSet()
val apkAbi = sources
.asSequence()
.filter { File(it).exists() }
.flatMap { ZipFile(it).entries().asSequence() }
.mapNotNull { regexNativeLibrary.matchEntire(it.name) }
.mapNotNull { it.groups[1]?.value }
.toSet()
availableAbi.intersect(apkAbi).isNotEmpty()
} catch (e: Exception) {
false
}
}
@@ -0,0 +1,32 @@
package com.github.kr328.clash.util
import android.content.Context
import android.content.Intent
import android.net.VpnService
import com.github.kr328.clash.common.compat.startForegroundServiceCompat
import com.github.kr328.clash.common.constants.Intents
import com.github.kr328.clash.common.util.intent
import com.github.kr328.clash.design.store.UiStore
import com.github.kr328.clash.service.ClashService
import com.github.kr328.clash.service.TunService
import com.github.kr328.clash.service.util.sendBroadcastSelf
fun Context.startClashService(): Intent? {
val startTun = UiStore(this).enableVpn
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.ACTION_CLASH_REQUEST_STOP))
}
@@ -0,0 +1,25 @@
package com.github.kr328.clash.util
import android.content.ContentResolver
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.FileNotFoundException
private fun fileNotFound(file: Uri): FileNotFoundException {
return FileNotFoundException("$file not found")
}
@Suppress("BlockingMethodInNonBlockingContext")
suspend fun ContentResolver.copyContentTo(
source: Uri,
target: Uri
) {
withContext(Dispatchers.IO) {
(openInputStream(source) ?: throw fileNotFound(source)).use { input ->
(openOutputStream(target, "rwt") ?: throw fileNotFound(target)).use { output ->
input.copyTo(output)
}
}
}
}
@@ -0,0 +1,10 @@
package com.github.kr328.clash.util
import android.content.Context
import java.io.File
val Context.logsDir: File
get() = cacheDir.resolve("logs")
val Context.clashDir: File
get() = filesDir.resolve("clash")
@@ -0,0 +1,46 @@
package com.github.kr328.clash.util
import android.os.DeadObjectException
import com.github.kr328.clash.common.log.Log
import com.github.kr328.clash.remote.Remote
import com.github.kr328.clash.service.remote.IClashManager
import com.github.kr328.clash.service.remote.IProfileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
suspend fun <T> withClash(
context: CoroutineContext = Dispatchers.IO,
block: suspend IClashManager.() -> T
): T {
while (true) {
val remote = Remote.service.remote.get()
val client = remote.clash()
try {
return withContext(context) { client.block() }
} catch (e: DeadObjectException) {
Log.w("Remote services panic")
Remote.service.remote.reset(remote)
}
}
}
suspend fun <T> withProfile(
context: CoroutineContext = Dispatchers.IO,
block: suspend IProfileManager.() -> T
): T {
while (true) {
val remote = Remote.service.remote.get()
val client = remote.profile()
try {
return withContext(context) { client.block() }
} catch (e: DeadObjectException) {
Log.w("Remote services panic")
Remote.service.remote.reset(remote)
}
}
}
@@ -0,0 +1,12 @@
package com.github.kr328.clash.util
import android.content.Context
import android.content.ServiceConnection
fun Context.unbindServiceSilent(connection: ServiceConnection) {
try {
unbindService(connection)
} catch (e: Exception) {
// ignore
}
}
@@ -0,0 +1,6 @@
package com.github.kr328.clash.util
import android.net.Uri
val Uri.fileName: String?
get() = schemeSpecificPart.split("/").lastOrNull()
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#1e1f22"
android:pathData="M480,640Q555,640 607.5,587.5Q660,535 660,460Q660,385 607.5,332.5Q555,280 480,280Q405,280 352.5,332.5Q300,385 300,460Q300,535 352.5,587.5Q405,640 480,640ZM480,568Q435,568 403.5,536.5Q372,505 372,460Q372,415 403.5,383.5Q435,352 480,352Q525,352 556.5,383.5Q588,415 588,460Q588,505 556.5,536.5Q525,568 480,568ZM480,760Q334,760 214,678.5Q94,597 40,460Q94,323 214,241.5Q334,160 480,160Q626,160 746,241.5Q866,323 920,460Q866,597 746,678.5Q626,760 480,760ZM480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460Q480,460 480,460ZM480,680Q593,680 687.5,620.5Q782,561 832,460Q782,359 687.5,299.5Q593,240 480,240Q367,240 272.5,299.5Q178,359 128,460Q178,561 272.5,620.5Q367,680 480,680Z"/>
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#1e1f22"
android:pathData="M644,532L586,474Q595,427 559,386Q523,345 466,354L408,296Q425,288 442.5,284Q460,280 480,280Q555,280 607.5,332.5Q660,385 660,460Q660,480 656,497.5Q652,515 644,532ZM772,658L714,602Q752,573 781.5,538.5Q811,504 832,460Q782,359 688.5,299.5Q595,240 480,240Q451,240 423,244Q395,248 368,256L306,194Q347,177 390,168.5Q433,160 480,160Q631,160 749,243.5Q867,327 920,460Q897,519 859.5,569.5Q822,620 772,658ZM792,904L624,738Q589,749 553.5,754.5Q518,760 480,760Q329,760 211,676.5Q93,593 40,460Q61,407 93,361.5Q125,316 166,280L56,168L112,112L848,848L792,904ZM222,336Q193,362 169,393Q145,424 128,460Q178,561 271.5,620.5Q365,680 480,680Q500,680 519,677.5Q538,675 558,672L522,634Q511,637 501,638.5Q491,640 480,640Q405,640 352.5,587.5Q300,535 300,460Q300,449 301.5,439Q303,429 306,418L222,336ZM541,429L541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429Q541,429 541,429ZM390,504Q390,504 390,504Q390,504 390,504L390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Q390,504 390,504Z"/>
</vector>
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="BootstrapTheme" parent="AppThemeDark" />
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="color_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_banner_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFF</color>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="nf_logcat_status" type="id" />
</resources>
@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<style name="BootstrapTheme" parent="AppThemeLight" />
</resources>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<full-backup-content>
<include
domain="sharedpref"
path="." />
<include
domain="database"
path="." />
<include
domain="file"
path="imported" />
<include
domain="file"
path="pending" />
<include
domain="file"
path="clash/override.json" />
</full-backup-content>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools"
tools:ignore="AcceptsUserCertificates">
<base-config>
<trust-anchors>
<certificates src="system" />
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>