Skip to content

Commit 72bef5f

Browse files
committed
Add android shortcuts
Fix init params issues Fix dynamic color issues Optimize navigator animate Optimize window init Optimize fab Optimize save
1 parent 526ccdf commit 72bef5f

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

52 files changed

+610
-458
lines changed

.github/workflows/build.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ jobs:
7979
- name: Setup Go
8080
uses: actions/setup-go@v5
8181
with:
82-
go-version-file: 'core/go.mod'
82+
go-version: 'stable'
8383
cache-dependency-path: |
8484
core/go.sum
8585

android/app/src/main/AndroidManifest.xml

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@
1010

1111
<uses-permission android:name="android.permission.INTERNET" />
1212
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
13-
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
13+
<uses-permission
14+
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
1415
tools:ignore="SystemPermissionTypo" />
1516
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
1617
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
@@ -23,8 +24,8 @@
2324

2425
<application
2526
android:name="${applicationName}"
26-
android:icon="@mipmap/ic_launcher"
2727
android:hardwareAccelerated="true"
28+
android:icon="@mipmap/ic_launcher"
2829
android:label="FlClash">
2930
<activity
3031
android:name="com.follow.clash.MainActivity"
@@ -73,11 +74,11 @@
7374
android:theme="@style/TransparentTheme">
7475
<intent-filter>
7576
<category android:name="android.intent.category.DEFAULT" />
76-
<action android:name="com.follow.clash.action.START" />
77+
<action android:name="${applicationId}.action.STOP" />
7778
</intent-filter>
7879
<intent-filter>
7980
<category android:name="android.intent.category.DEFAULT" />
80-
<action android:name="com.follow.clash.action.STOP" />
81+
<action android:name="${applicationId}.action.CHANGE" />
8182
</intent-filter>
8283
</activity>
8384

android/app/src/main/kotlin/com/follow/clash/GlobalState.kt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import android.content.Context
44
import androidx.lifecycle.MutableLiveData
55
import com.follow.clash.plugins.AppPlugin
66
import com.follow.clash.plugins.ServicePlugin
7-
import com.follow.clash.plugins.VpnPlugin
87
import com.follow.clash.plugins.TilePlugin
8+
import com.follow.clash.plugins.VpnPlugin
99
import io.flutter.FlutterInjector
1010
import io.flutter.embedding.engine.FlutterEngine
1111
import io.flutter.embedding.engine.dart.DartExecutor
@@ -33,6 +33,10 @@ object GlobalState {
3333
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
3434
}
3535

36+
fun getText(text: String): String {
37+
return getCurrentAppPlugin()?.getText(text) ?: ""
38+
}
39+
3640
fun getCurrentTilePlugin(): TilePlugin? {
3741
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
3842
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
@@ -42,6 +46,27 @@ object GlobalState {
4246
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
4347
}
4448

49+
fun handleToggle(context: Context) {
50+
if (runState.value == RunState.STOP) {
51+
runState.value = RunState.PENDING
52+
val tilePlugin = getCurrentTilePlugin()
53+
if (tilePlugin != null) {
54+
tilePlugin.handleStart()
55+
} else {
56+
initServiceEngine(context)
57+
}
58+
} else {
59+
handleStop()
60+
}
61+
}
62+
63+
fun handleStop() {
64+
if (runState.value == RunState.START) {
65+
runState.value = RunState.PENDING
66+
getCurrentTilePlugin()?.handleStop()
67+
}
68+
}
69+
4570
fun destroyServiceEngine() {
4671
serviceEngine?.destroy()
4772
serviceEngine = null

android/app/src/main/kotlin/com/follow/clash/MainActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ package com.follow.clash
33

44
import com.follow.clash.plugins.AppPlugin
55
import com.follow.clash.plugins.ServicePlugin
6-
import com.follow.clash.plugins.VpnPlugin
76
import com.follow.clash.plugins.TilePlugin
7+
import com.follow.clash.plugins.VpnPlugin
88
import io.flutter.embedding.android.FlutterActivity
99
import io.flutter.embedding.engine.FlutterEngine
1010

android/app/src/main/kotlin/com/follow/clash/TempActivity.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@ package com.follow.clash
22

33
import android.app.Activity
44
import android.os.Bundle
5+
import com.follow.clash.extensions.wrapAction
56

67
class TempActivity : Activity() {
78
override fun onCreate(savedInstanceState: Bundle?) {
89
super.onCreate(savedInstanceState)
910
when (intent.action) {
10-
"com.follow.clash.action.START" -> {
11-
GlobalState.getCurrentTilePlugin()?.handleStart()
11+
wrapAction("STOP") -> {
12+
GlobalState.handleStop()
1213
}
1314

14-
"com.follow.clash.action.STOP" -> {
15-
GlobalState.getCurrentTilePlugin()?.handleStop()
15+
wrapAction("CHANGE") -> {
16+
GlobalState.handleToggle(applicationContext)
1617
}
1718
}
1819
finishAndRemoveTask()

android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,29 @@
11
package com.follow.clash.extensions
22

3+
import android.app.PendingIntent
4+
import android.content.Context
5+
import android.content.Intent
36
import android.graphics.Bitmap
47
import android.graphics.drawable.Drawable
58
import android.net.ConnectivityManager
69
import android.net.Network
10+
import android.os.Build
711
import android.system.OsConstants.IPPROTO_TCP
812
import android.system.OsConstants.IPPROTO_UDP
913
import android.util.Base64
1014
import androidx.core.graphics.drawable.toBitmap
15+
import com.follow.clash.TempActivity
1116
import com.follow.clash.models.CIDR
1217
import com.follow.clash.models.Metadata
18+
import io.flutter.plugin.common.MethodChannel
1319
import kotlinx.coroutines.Dispatchers
1420
import kotlinx.coroutines.withContext
1521
import java.io.ByteArrayOutputStream
1622
import java.net.Inet4Address
1723
import java.net.Inet6Address
1824
import java.net.InetAddress
25+
import kotlin.coroutines.resume
26+
import kotlin.coroutines.suspendCoroutine
1927

2028

2129
suspend fun Drawable.getBase64(): String {
@@ -71,6 +79,34 @@ fun InetAddress.asSocketAddressText(port: Int): String {
7179
}
7280
}
7381

82+
fun Context.wrapAction(action: String):String{
83+
return "${this.packageName}.action.$action"
84+
}
85+
86+
fun Context.getActionIntent(action: String): Intent {
87+
val actionIntent = Intent(this, TempActivity::class.java)
88+
actionIntent.action = wrapAction(action)
89+
return actionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
90+
}
91+
92+
fun Context.getActionPendingIntent(action: String): PendingIntent {
93+
return if (Build.VERSION.SDK_INT >= 31) {
94+
PendingIntent.getActivity(
95+
this,
96+
0,
97+
getActionIntent(action),
98+
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
99+
)
100+
} else {
101+
PendingIntent.getActivity(
102+
this,
103+
0,
104+
getActionIntent(action),
105+
PendingIntent.FLAG_UPDATE_CURRENT
106+
)
107+
}
108+
}
109+
74110

75111
private fun numericToTextFormat(src: ByteArray): String {
76112
val sb = StringBuilder(39)
@@ -87,3 +123,25 @@ private fun numericToTextFormat(src: ByteArray): String {
87123
}
88124
return sb.toString()
89125
}
126+
127+
suspend fun <T> MethodChannel.awaitResult(
128+
method: String,
129+
arguments: Any? = null
130+
): T? = withContext(Dispatchers.Main) { // 切换到主线程
131+
suspendCoroutine { continuation ->
132+
invokeMethod(method, arguments, object : MethodChannel.Result {
133+
override fun success(result: Any?) {
134+
@Suppress("UNCHECKED_CAST")
135+
continuation.resume(result as T)
136+
}
137+
138+
override fun error(code: String, message: String?, details: Any?) {
139+
continuation.resume(null)
140+
}
141+
142+
override fun notImplemented() {
143+
continuation.resume(null)
144+
}
145+
})
146+
}
147+
}

android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,15 @@ import android.widget.Toast
1414
import androidx.core.app.ActivityCompat
1515
import androidx.core.content.ContextCompat
1616
import androidx.core.content.ContextCompat.getSystemService
17-
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
1817
import androidx.core.content.FileProvider
18+
import androidx.core.content.pm.ShortcutInfoCompat
19+
import androidx.core.content.pm.ShortcutManagerCompat
20+
import androidx.core.graphics.drawable.IconCompat
21+
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
1922
import com.follow.clash.GlobalState
23+
import com.follow.clash.R
24+
import com.follow.clash.extensions.awaitResult
25+
import com.follow.clash.extensions.getActionIntent
2026
import com.follow.clash.extensions.getBase64
2127
import com.follow.clash.models.Package
2228
import com.google.gson.Gson
@@ -31,6 +37,7 @@ import kotlinx.coroutines.CoroutineScope
3137
import kotlinx.coroutines.Dispatchers
3238
import kotlinx.coroutines.cancel
3339
import kotlinx.coroutines.launch
40+
import kotlinx.coroutines.runBlocking
3441
import kotlinx.coroutines.withContext
3542
import java.io.File
3643
import java.util.zip.ZipFile
@@ -116,37 +123,48 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
116123

117124
override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
118125
scope = CoroutineScope(Dispatchers.Default)
119-
context = flutterPluginBinding.applicationContext;
126+
context = flutterPluginBinding.applicationContext
120127
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
121128
channel.setMethodCallHandler(this)
122129
}
123130

131+
private fun initShortcuts(label: String) {
132+
val shortcut = ShortcutInfoCompat.Builder(context, "toggle")
133+
.setShortLabel(label)
134+
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round))
135+
.setIntent(context.getActionIntent("CHANGE"))
136+
.build()
137+
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut))
138+
}
139+
140+
124141
override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
125142
channel.setMethodCallHandler(null)
126143
scope.cancel()
127144
}
128145

129146
private fun tip(message: String?) {
130147
if (GlobalState.flutterEngine == null) {
131-
if (toast != null) {
132-
toast!!.cancel()
133-
}
134-
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
135-
toast!!.show()
148+
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
136149
}
137150
}
138151

139152
override fun onMethodCall(call: MethodCall, result: Result) {
140153
when (call.method) {
141154
"moveTaskToBack" -> {
142155
activity?.moveTaskToBack(true)
143-
result.success(true);
156+
result.success(true)
144157
}
145158

146159
"updateExcludeFromRecents" -> {
147160
val value = call.argument<Boolean>("value")
148161
updateExcludeFromRecents(value)
149-
result.success(true);
162+
result.success(true)
163+
}
164+
165+
"initShortcuts" -> {
166+
initShortcuts(call.arguments as String)
167+
result.success(true)
150168
}
151169

152170
"getPackages" -> {
@@ -197,7 +215,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
197215
}
198216

199217
else -> {
200-
result.notImplemented();
218+
result.notImplemented()
201219
}
202220
}
203221
}
@@ -270,7 +288,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
270288

271289
private fun getPackages(): List<Package> {
272290
val packageManager = context.packageManager
273-
if (packages.isNotEmpty()) return packages;
291+
if (packages.isNotEmpty()) return packages
274292
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
275293
it.packageName != context.packageName
276294
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
@@ -284,7 +302,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
284302
firstInstallTime = it.firstInstallTime
285303
)
286304
}?.let { packages.addAll(it) }
287-
return packages;
305+
return packages
288306
}
289307

290308
private suspend fun getPackagesToJson(): String {
@@ -306,7 +324,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
306324
val intent = VpnService.prepare(context)
307325
if (intent != null) {
308326
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
309-
return;
327+
return
310328
}
311329
vpnCallBack?.invoke()
312330
}
@@ -330,6 +348,12 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
330348
}
331349
}
332350

351+
fun getText(text: String): String? {
352+
return runBlocking {
353+
channel.awaitResult<String>("getText", text)
354+
}
355+
}
356+
333357
private fun isChinaPackage(packageName: String): Boolean {
334358
val packageManager = context.packageManager ?: return false
335359
skipPrefixList.forEach {
@@ -398,7 +422,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
398422
}
399423

400424
override fun onAttachedToActivity(binding: ActivityPluginBinding) {
401-
activity = binding.activity;
425+
activity = binding.activity
402426
binding.addActivityResultListener(::onActivityResult)
403427
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
404428
}
@@ -408,7 +432,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
408432
}
409433

410434
override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
411-
activity = binding.activity;
435+
activity = binding.activity
412436
}
413437

414438
override fun onDetachedFromActivity() {

android/app/src/main/kotlin/com/follow/clash/plugins/ServicePlugin.kt

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.follow.clash.plugins
22

33
import android.content.Context
4-
import android.net.ConnectivityManager
5-
import androidx.core.content.getSystemService
64
import com.follow.clash.GlobalState
75
import io.flutter.embedding.engine.plugins.FlutterPlugin
86
import io.flutter.plugin.common.MethodCall

android/app/src/main/kotlin/com/follow/clash/services/FlClashService.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ import android.os.Build
1313
import android.os.IBinder
1414
import androidx.core.app.NotificationCompat
1515
import com.follow.clash.BaseServiceInterface
16+
import com.follow.clash.GlobalState
1617
import com.follow.clash.MainActivity
18+
import com.follow.clash.extensions.getActionPendingIntent
1719
import com.follow.clash.models.VpnOptions
1820

1921

@@ -64,6 +66,11 @@ class FlClashService : Service(), BaseServiceInterface {
6466
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
6567
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
6668
}
69+
addAction(
70+
0,
71+
GlobalState.getText("stop"),
72+
getActionPendingIntent("CHANGE")
73+
)
6774
setOngoing(true)
6875
setShowWhen(false)
6976
setOnlyAlertOnce(true)

0 commit comments

Comments
 (0)