Skip to content

Commit

Permalink
Add android shortcuts
Browse files Browse the repository at this point in the history
Fix init params issues

Fix dynamic color issues

Optimize navigator animate

Optimize window init

Optimize fab

Optimize save
  • Loading branch information
chen08209 committed Nov 9, 2024
1 parent 526ccdf commit 72bef5f
Show file tree
Hide file tree
Showing 52 changed files with 610 additions and 458 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version-file: 'core/go.mod'
go-version: 'stable'
cache-dependency-path: |
core/go.sum
Expand Down
9 changes: 5 additions & 4 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"
tools:ignore="SystemPermissionTypo" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
Expand All @@ -23,8 +24,8 @@

<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:label="FlClash">
<activity
android:name="com.follow.clash.MainActivity"
Expand Down Expand Up @@ -73,11 +74,11 @@
android:theme="@style/TransparentTheme">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="com.follow.clash.action.START" />
<action android:name="${applicationId}.action.STOP" />
</intent-filter>
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="com.follow.clash.action.STOP" />
<action android:name="${applicationId}.action.CHANGE" />
</intent-filter>
</activity>

Expand Down
27 changes: 26 additions & 1 deletion android/app/src/main/kotlin/com/follow/clash/GlobalState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import android.content.Context
import androidx.lifecycle.MutableLiveData
import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import io.flutter.FlutterInjector
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.dart.DartExecutor
Expand Down Expand Up @@ -33,6 +33,10 @@ object GlobalState {
return currentEngine?.plugins?.get(AppPlugin::class.java) as AppPlugin?
}

fun getText(text: String): String {
return getCurrentAppPlugin()?.getText(text) ?: ""
}

fun getCurrentTilePlugin(): TilePlugin? {
val currentEngine = if (flutterEngine != null) flutterEngine else serviceEngine
return currentEngine?.plugins?.get(TilePlugin::class.java) as TilePlugin?
Expand All @@ -42,6 +46,27 @@ object GlobalState {
return serviceEngine?.plugins?.get(VpnPlugin::class.java) as VpnPlugin?
}

fun handleToggle(context: Context) {
if (runState.value == RunState.STOP) {
runState.value = RunState.PENDING
val tilePlugin = getCurrentTilePlugin()
if (tilePlugin != null) {
tilePlugin.handleStart()
} else {
initServiceEngine(context)
}
} else {
handleStop()
}
}

fun handleStop() {
if (runState.value == RunState.START) {
runState.value = RunState.PENDING
getCurrentTilePlugin()?.handleStop()
}
}

fun destroyServiceEngine() {
serviceEngine?.destroy()
serviceEngine = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package com.follow.clash

import com.follow.clash.plugins.AppPlugin
import com.follow.clash.plugins.ServicePlugin
import com.follow.clash.plugins.VpnPlugin
import com.follow.clash.plugins.TilePlugin
import com.follow.clash.plugins.VpnPlugin
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine

Expand Down
9 changes: 5 additions & 4 deletions android/app/src/main/kotlin/com/follow/clash/TempActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@ package com.follow.clash

import android.app.Activity
import android.os.Bundle
import com.follow.clash.extensions.wrapAction

class TempActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
when (intent.action) {
"com.follow.clash.action.START" -> {
GlobalState.getCurrentTilePlugin()?.handleStart()
wrapAction("STOP") -> {
GlobalState.handleStop()
}

"com.follow.clash.action.STOP" -> {
GlobalState.getCurrentTilePlugin()?.handleStop()
wrapAction("CHANGE") -> {
GlobalState.handleToggle(applicationContext)
}
}
finishAndRemoveTask()
Expand Down
58 changes: 58 additions & 0 deletions android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
package com.follow.clash.extensions

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import android.net.ConnectivityManager
import android.net.Network
import android.os.Build
import android.system.OsConstants.IPPROTO_TCP
import android.system.OsConstants.IPPROTO_UDP
import android.util.Base64
import androidx.core.graphics.drawable.toBitmap
import com.follow.clash.TempActivity
import com.follow.clash.models.CIDR
import com.follow.clash.models.Metadata
import io.flutter.plugin.common.MethodChannel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import java.net.Inet4Address
import java.net.Inet6Address
import java.net.InetAddress
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine


suspend fun Drawable.getBase64(): String {
Expand Down Expand Up @@ -71,6 +79,34 @@ fun InetAddress.asSocketAddressText(port: Int): String {
}
}

fun Context.wrapAction(action: String):String{
return "${this.packageName}.action.$action"
}

fun Context.getActionIntent(action: String): Intent {
val actionIntent = Intent(this, TempActivity::class.java)
actionIntent.action = wrapAction(action)
return actionIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}

fun Context.getActionPendingIntent(action: String): PendingIntent {
return if (Build.VERSION.SDK_INT >= 31) {
PendingIntent.getActivity(
this,
0,
getActionIntent(action),
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
} else {
PendingIntent.getActivity(
this,
0,
getActionIntent(action),
PendingIntent.FLAG_UPDATE_CURRENT
)
}
}


private fun numericToTextFormat(src: ByteArray): String {
val sb = StringBuilder(39)
Expand All @@ -87,3 +123,25 @@ private fun numericToTextFormat(src: ByteArray): String {
}
return sb.toString()
}

suspend fun <T> MethodChannel.awaitResult(
method: String,
arguments: Any? = null
): T? = withContext(Dispatchers.Main) { // 切换到主线程
suspendCoroutine { continuation ->
invokeMethod(method, arguments, object : MethodChannel.Result {
override fun success(result: Any?) {
@Suppress("UNCHECKED_CAST")
continuation.resume(result as T)
}

override fun error(code: String, message: String?, details: Any?) {
continuation.resume(null)
}

override fun notImplemented() {
continuation.resume(null)
}
})
}
}
54 changes: 39 additions & 15 deletions android/app/src/main/kotlin/com/follow/clash/plugins/AppPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,15 @@ import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.getSystemService
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import androidx.core.content.FileProvider
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import com.android.tools.smali.dexlib2.dexbacked.DexBackedDexFile
import com.follow.clash.GlobalState
import com.follow.clash.R
import com.follow.clash.extensions.awaitResult
import com.follow.clash.extensions.getActionIntent
import com.follow.clash.extensions.getBase64
import com.follow.clash.models.Package
import com.google.gson.Gson
Expand All @@ -31,6 +37,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.File
import java.util.zip.ZipFile
Expand Down Expand Up @@ -116,37 +123,48 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware

override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
scope = CoroutineScope(Dispatchers.Default)
context = flutterPluginBinding.applicationContext;
context = flutterPluginBinding.applicationContext
channel = MethodChannel(flutterPluginBinding.binaryMessenger, "app")
channel.setMethodCallHandler(this)
}

private fun initShortcuts(label: String) {
val shortcut = ShortcutInfoCompat.Builder(context, "toggle")
.setShortLabel(label)
.setIcon(IconCompat.createWithResource(context, R.mipmap.ic_launcher_round))
.setIntent(context.getActionIntent("CHANGE"))
.build()
ShortcutManagerCompat.setDynamicShortcuts(context, listOf(shortcut))
}


override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
channel.setMethodCallHandler(null)
scope.cancel()
}

private fun tip(message: String?) {
if (GlobalState.flutterEngine == null) {
if (toast != null) {
toast!!.cancel()
}
toast = Toast.makeText(context, message, Toast.LENGTH_SHORT)
toast!!.show()
Toast.makeText(context, message, Toast.LENGTH_LONG).show()
}
}

override fun onMethodCall(call: MethodCall, result: Result) {
when (call.method) {
"moveTaskToBack" -> {
activity?.moveTaskToBack(true)
result.success(true);
result.success(true)
}

"updateExcludeFromRecents" -> {
val value = call.argument<Boolean>("value")
updateExcludeFromRecents(value)
result.success(true);
result.success(true)
}

"initShortcuts" -> {
initShortcuts(call.arguments as String)
result.success(true)
}

"getPackages" -> {
Expand Down Expand Up @@ -197,7 +215,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}

else -> {
result.notImplemented();
result.notImplemented()
}
}
}
Expand Down Expand Up @@ -270,7 +288,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware

private fun getPackages(): List<Package> {
val packageManager = context.packageManager
if (packages.isNotEmpty()) return packages;
if (packages.isNotEmpty()) return packages
packageManager?.getInstalledPackages(PackageManager.GET_META_DATA)?.filter {
it.packageName != context.packageName
|| it.requestedPermissions?.contains(Manifest.permission.INTERNET) == true
Expand All @@ -284,7 +302,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
firstInstallTime = it.firstInstallTime
)
}?.let { packages.addAll(it) }
return packages;
return packages
}

private suspend fun getPackagesToJson(): String {
Expand All @@ -306,7 +324,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
val intent = VpnService.prepare(context)
if (intent != null) {
activity?.startActivityForResult(intent, VPN_PERMISSION_REQUEST_CODE)
return;
return
}
vpnCallBack?.invoke()
}
Expand All @@ -330,6 +348,12 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}
}

fun getText(text: String): String? {
return runBlocking {
channel.awaitResult<String>("getText", text)
}
}

private fun isChinaPackage(packageName: String): Boolean {
val packageManager = context.packageManager ?: return false
skipPrefixList.forEach {
Expand Down Expand Up @@ -398,7 +422,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}

override fun onAttachedToActivity(binding: ActivityPluginBinding) {
activity = binding.activity;
activity = binding.activity
binding.addActivityResultListener(::onActivityResult)
binding.addRequestPermissionsResultListener(::onRequestPermissionsResultListener)
}
Expand All @@ -408,7 +432,7 @@ class AppPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAware
}

override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
activity = binding.activity;
activity = binding.activity
}

override fun onDetachedFromActivity() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package com.follow.clash.plugins

import android.content.Context
import android.net.ConnectivityManager
import androidx.core.content.getSystemService
import com.follow.clash.GlobalState
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.follow.clash.BaseServiceInterface
import com.follow.clash.GlobalState
import com.follow.clash.MainActivity
import com.follow.clash.extensions.getActionPendingIntent
import com.follow.clash.models.VpnOptions


Expand Down Expand Up @@ -64,6 +66,11 @@ class FlClashService : Service(), BaseServiceInterface {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
foregroundServiceBehavior = FOREGROUND_SERVICE_IMMEDIATE
}
addAction(
0,
GlobalState.getText("stop"),
getActionPendingIntent("CHANGE")
)
setOngoing(true)
setShowWhen(false)
setOnlyAlertOnce(true)
Expand Down
Loading

0 comments on commit 72bef5f

Please sign in to comment.