diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 69818c852..2b06b6f0a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -342,6 +342,19 @@
android:name="androidx.room.MultiInstanceInvalidationService"
android:process=":bg" />
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
index caf363f6f..3af9badd0 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/Constants.kt
@@ -184,4 +184,5 @@ object Action {
// const val SWITCH_WAKE_LOCK = "io.nekohasekai.sagernet.SWITCH_WAKELOCK"
const val RESET_UPSTREAM_CONNECTIONS = "moe.nb4a.RESET_UPSTREAM_CONNECTIONS"
+ const val WIDGET_UPDATE = "io.nekohasekai.sagernet.WIDGET_UPDATE"
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
index e760983dc..a981aebbf 100644
--- a/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
+++ b/app/src/main/java/io/nekohasekai/sagernet/bg/BaseService.kt
@@ -86,6 +86,9 @@ class BaseService {
state = s
DataStore.serviceState = s
binder.stateChanged(s, msg)
+ io.nekohasekai.sagernet.widget.ProxyToggleWidget.updateAll(
+ service as Context
+ )
}
}
diff --git a/app/src/main/java/io/nekohasekai/sagernet/widget/ProxyToggleWidget.kt b/app/src/main/java/io/nekohasekai/sagernet/widget/ProxyToggleWidget.kt
new file mode 100644
index 000000000..1fd815308
--- /dev/null
+++ b/app/src/main/java/io/nekohasekai/sagernet/widget/ProxyToggleWidget.kt
@@ -0,0 +1,89 @@
+package io.nekohasekai.sagernet.widget
+
+import android.app.PendingIntent
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProvider
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.widget.RemoteViews
+import io.nekohasekai.sagernet.R
+import io.nekohasekai.sagernet.SagerNet
+import io.nekohasekai.sagernet.bg.BaseService
+import io.nekohasekai.sagernet.database.DataStore
+
+class ProxyToggleWidget : AppWidgetProvider() {
+
+ companion object {
+ const val ACTION_TOGGLE = "io.nekohasekai.sagernet.WIDGET_TOGGLE"
+
+ fun updateAll(context: Context) {
+ val manager = AppWidgetManager.getInstance(context) ?: return
+ val ids = manager.getAppWidgetIds(
+ ComponentName(context, ProxyToggleWidget::class.java)
+ )
+ if (ids == null || ids.isEmpty()) return
+ val intent = Intent(context, ProxyToggleWidget::class.java)
+ .setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
+ .putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
+ context.sendBroadcast(intent)
+ }
+ }
+
+ override fun onUpdate(
+ context: Context,
+ appWidgetManager: AppWidgetManager,
+ appWidgetIds: IntArray
+ ) {
+ appWidgetIds.forEach { id ->
+ updateWidget(context, appWidgetManager, id)
+ }
+ }
+
+ override fun onReceive(context: Context, intent: Intent) {
+ if (intent.action == ACTION_TOGGLE) {
+ handleToggle()
+ return
+ }
+ super.onReceive(context, intent)
+ }
+
+ private fun handleToggle() {
+ val state = DataStore.serviceState
+ when {
+ state.canStop -> SagerNet.stopService()
+ state == BaseService.State.Stopped || state == BaseService.State.Idle ->
+ SagerNet.startService()
+ }
+ }
+
+ private fun updateWidget(
+ context: Context,
+ manager: AppWidgetManager,
+ widgetId: Int
+ ) {
+ val views = RemoteViews(context.packageName, R.layout.widget_proxy_toggle)
+ val state = DataStore.serviceState
+
+ val bgRes = when {
+ state == BaseService.State.Connected -> R.drawable.widget_background_connected
+ state == BaseService.State.Connecting || state == BaseService.State.Stopping ->
+ R.drawable.widget_background_busy
+ else -> R.drawable.widget_background
+ }
+ views.setInt(R.id.widget_root, "setBackgroundResource", bgRes)
+
+ val pendingFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ } else {
+ PendingIntent.FLAG_UPDATE_CURRENT
+ }
+ val toggleIntent = Intent(context, ProxyToggleWidget::class.java)
+ .setAction(ACTION_TOGGLE)
+ val pendingIntent = PendingIntent.getBroadcast(context, 0, toggleIntent, pendingFlags)
+ views.setOnClickPendingIntent(R.id.widget_root, pendingIntent)
+
+ manager.updateAppWidget(widgetId, views)
+ }
+}
diff --git a/app/src/main/res/drawable/ic_widget_power.xml b/app/src/main/res/drawable/ic_widget_power.xml
new file mode 100644
index 000000000..c0fcc4b50
--- /dev/null
+++ b/app/src/main/res/drawable/ic_widget_power.xml
@@ -0,0 +1,10 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/widget_background.xml b/app/src/main/res/drawable/widget_background.xml
new file mode 100644
index 000000000..04251499a
--- /dev/null
+++ b/app/src/main/res/drawable/widget_background.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/widget_background_busy.xml b/app/src/main/res/drawable/widget_background_busy.xml
new file mode 100644
index 000000000..e75b5efc8
--- /dev/null
+++ b/app/src/main/res/drawable/widget_background_busy.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/widget_background_connected.xml b/app/src/main/res/drawable/widget_background_connected.xml
new file mode 100644
index 000000000..971be8452
--- /dev/null
+++ b/app/src/main/res/drawable/widget_background_connected.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
diff --git a/app/src/main/res/layout/widget_proxy_toggle.xml b/app/src/main/res/layout/widget_proxy_toggle.xml
new file mode 100644
index 000000000..6c4c7d6b1
--- /dev/null
+++ b/app/src/main/res/layout/widget_proxy_toggle.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index a9f0c1cba..211b21562 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -287,6 +287,14 @@
超时
不可达
开关
+
+ 代理开关
+ 一键切换代理开关
+ 切换代理
+ 已连接
+ 连接中…
+ 停止中…
+ 已断开
追加 HTTP 代理至 VPN
浏览器 / 一些支持的应用 将直接使用 HTTP 代理, 而不经过虚拟网卡设备 (Android 10+)
ICMPing 不可用
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index de521bcbd..8c6f5a77d 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -201,6 +201,14 @@
\n%s
差異(%s)
切換器
+
+ 代理開關
+ 一鍵切換代理開關
+ 切換代理
+ 已連接
+ 連接中…
+ 停止中…
+ 已斷開
未分組
版本 (%s)
額外標頭
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 1995ff051..cf3406827 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -20,6 +20,14 @@
Enable
Disable
Switcher
+
+ Proxy Toggle
+ One-click proxy toggle
+ Toggle proxy
+ Connected
+ Connecting…
+ Stopping…
+ Stopped
Traffic
sing-box Dashboard
diff --git a/app/src/main/res/xml/widget_proxy_toggle_info.xml b/app/src/main/res/xml/widget_proxy_toggle_info.xml
new file mode 100644
index 000000000..e93bb7980
--- /dev/null
+++ b/app/src/main/res/xml/widget_proxy_toggle_info.xml
@@ -0,0 +1,12 @@
+
+