From 5b2d9512c62ffcc7edec758fb2d5f249c54c7de2 Mon Sep 17 00:00:00 2001 From: sleshep <9625413+sleshep@users.noreply.github.com> Date: Wed, 4 Mar 2026 18:06:49 +0800 Subject: [PATCH] Add dynamic App Shortcuts for external automation (Tasker/Samsung Routines) Register toggle/start/stop shortcuts via ShortcutManagerCompat on app launch, enabling Samsung Routines and other launchers to control the Clash service through long-press app icon shortcuts. Use launcher icon for shortcut display; suppress activity transitions and exclude ExternalControlActivity from recents for seamless background control. --- app/src/main/AndroidManifest.xml | 2 + .../kr328/clash/ExternalControlActivity.kt | 8 +++ .../com/github/kr328/clash/MainApplication.kt | 52 +++++++++++++++++++ design/src/main/res/values-zh/strings.xml | 7 +++ design/src/main/res/values/strings.xml | 7 +++ 5 files changed, 76 insertions(+) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f6fc1ab655..addc01ee82 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -65,8 +65,10 @@ diff --git a/app/src/main/java/com/github/kr328/clash/ExternalControlActivity.kt b/app/src/main/java/com/github/kr328/clash/ExternalControlActivity.kt index 07645806ca..6efa5060e6 100644 --- a/app/src/main/java/com/github/kr328/clash/ExternalControlActivity.kt +++ b/app/src/main/java/com/github/kr328/clash/ExternalControlActivity.kt @@ -25,6 +25,8 @@ import com.github.kr328.clash.design.R class ExternalControlActivity : Activity(), CoroutineScope by MainScope() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + @Suppress("DEPRECATION") + overridePendingTransition(0, 0) when(intent.action) { Intent.ACTION_VIEW -> { @@ -90,4 +92,10 @@ class ExternalControlActivity : Activity(), CoroutineScope by MainScope() { stopClashService() Toast.makeText(this, R.string.external_control_stopped, Toast.LENGTH_LONG).show() } + + override fun finish() { + super.finish() + @Suppress("DEPRECATION") + overridePendingTransition(0, 0) + } } \ No newline at end of file diff --git a/app/src/main/java/com/github/kr328/clash/MainApplication.kt b/app/src/main/java/com/github/kr328/clash/MainApplication.kt index 6bef37f9b6..5e0d72f2f1 100644 --- a/app/src/main/java/com/github/kr328/clash/MainApplication.kt +++ b/app/src/main/java/com/github/kr328/clash/MainApplication.kt @@ -2,14 +2,20 @@ package com.github.kr328.clash import android.app.Application import android.content.Context +import android.content.Intent +import androidx.core.content.pm.ShortcutInfoCompat +import androidx.core.content.pm.ShortcutManagerCompat +import androidx.core.graphics.drawable.IconCompat import com.github.kr328.clash.common.Global import com.github.kr328.clash.common.compat.currentProcessName +import com.github.kr328.clash.common.constants.Intents import com.github.kr328.clash.common.log.Log import com.github.kr328.clash.remote.Remote import com.github.kr328.clash.service.util.sendServiceRecreated import com.github.kr328.clash.util.clashDir import java.io.File import java.io.FileOutputStream +import com.github.kr328.clash.design.R as DesignR @Suppress("unused") @@ -30,11 +36,57 @@ class MainApplication : Application() { if (processName == packageName) { Remote.launch() + setupShortcuts() } else { sendServiceRecreated() } } + private fun setupShortcuts() { + val icon = IconCompat.createWithResource(this, R.mipmap.ic_launcher) + val flags = Intent.FLAG_ACTIVITY_NEW_TASK or + Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS or + Intent.FLAG_ACTIVITY_NO_ANIMATION + + val toggle = ShortcutInfoCompat.Builder(this, "toggle_clash") + .setShortLabel(getString(DesignR.string.shortcut_toggle_short)) + .setLongLabel(getString(DesignR.string.shortcut_toggle_long)) + .setIcon(icon) + .setIntent( + Intent(Intents.ACTION_TOGGLE_CLASH) + .setClassName(this, ExternalControlActivity::class.java.name) + .addFlags(flags) + ) + .setRank(0) + .build() + + val start = ShortcutInfoCompat.Builder(this, "start_clash") + .setShortLabel(getString(DesignR.string.shortcut_start_short)) + .setLongLabel(getString(DesignR.string.shortcut_start_long)) + .setIcon(icon) + .setIntent( + Intent(Intents.ACTION_START_CLASH) + .setClassName(this, ExternalControlActivity::class.java.name) + .addFlags(flags) + ) + .setRank(1) + .build() + + val stop = ShortcutInfoCompat.Builder(this, "stop_clash") + .setShortLabel(getString(DesignR.string.shortcut_stop_short)) + .setLongLabel(getString(DesignR.string.shortcut_stop_long)) + .setIcon(icon) + .setIntent( + Intent(Intents.ACTION_STOP_CLASH) + .setClassName(this, ExternalControlActivity::class.java.name) + .addFlags(flags) + ) + .setRank(2) + .build() + + ShortcutManagerCompat.setDynamicShortcuts(this, listOf(toggle, start, stop)) + } + private fun extractGeoFiles() { clashDir.mkdirs() diff --git a/design/src/main/res/values-zh/strings.xml b/design/src/main/res/values-zh/strings.xml index 48daff432b..31c1371995 100644 --- a/design/src/main/res/values-zh/strings.xml +++ b/design/src/main/res/values-zh/strings.xml @@ -262,4 +262,11 @@ Clash.Meta 服务已停止 相机权限受限,请前往设置开启。 发生系统未知异常,操作失败。 + + 切换 Clash + 切换 Clash 服务启停 + 启动 Clash + 启动 Clash 服务 + 停止 Clash + 停止 Clash 服务 diff --git a/design/src/main/res/values/strings.xml b/design/src/main/res/values/strings.xml index 518af90d0d..0808ae4bd9 100644 --- a/design/src/main/res/values/strings.xml +++ b/design/src/main/res/values/strings.xml @@ -350,4 +350,11 @@ Hide app from the Recent apps screen Camera access is restricted. Please enable it in Settings. An unhandled system exception occurred. + + Toggle Clash + Toggle Clash service on/off + Start Clash + Start Clash service + Stop Clash + Stop Clash service