diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index dece5465..f4e1b29d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -271,6 +271,17 @@
android:exported="false"
android:foregroundServiceType="specialUse" />
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/chaomixian/vflow/core/system/DndConditionProviderService.kt b/app/src/main/java/com/chaomixian/vflow/core/system/DndConditionProviderService.kt
new file mode 100644
index 00000000..fa950c9b
--- /dev/null
+++ b/app/src/main/java/com/chaomixian/vflow/core/system/DndConditionProviderService.kt
@@ -0,0 +1,18 @@
+package com.chaomixian.vflow.core.system
+
+import android.net.Uri
+import android.service.notification.ConditionProviderService
+
+class DndConditionProviderService : ConditionProviderService() {
+ override fun onConnected() {
+ // Called when the system binds to the provider
+ }
+
+ override fun onSubscribe(conditionId: Uri?) {
+ // System is listening for updates on this conditionId
+ }
+
+ override fun onUnsubscribe(conditionId: Uri?) {
+ // System stopped listening
+ }
+}
diff --git a/app/src/main/java/com/chaomixian/vflow/core/system/DndModeManager.kt b/app/src/main/java/com/chaomixian/vflow/core/system/DndModeManager.kt
new file mode 100644
index 00000000..ecc0efde
--- /dev/null
+++ b/app/src/main/java/com/chaomixian/vflow/core/system/DndModeManager.kt
@@ -0,0 +1,141 @@
+package com.chaomixian.vflow.core.system
+
+import android.app.AutomaticZenRule
+import android.app.NotificationManager
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.net.Uri
+import android.os.Build
+import android.provider.Settings
+import android.service.notification.Condition
+import android.service.notification.ZenPolicy
+
+class DndModeManager(private val context: Context) {
+
+ private val notificationManager =
+ context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+
+ private val providerComponent =
+ ComponentName(context, DndConditionProviderService::class.java)
+
+ /**
+ * Checks if the app has permission to modify Do Not Disturb policy.
+ */
+ fun hasDndAccess(): Boolean {
+ return notificationManager.isNotificationPolicyAccessGranted
+ }
+
+ /**
+ * Helper to launch DND access settings screen.
+ */
+ fun requestDndAccess() {
+ val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS).apply {
+ addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ }
+ context.startActivity(intent)
+ }
+
+ /**
+ * Creates a custom Do Not Disturb mode (AutomaticZenRule).
+ * @param name The name of the mode (e.g., "Gaming Mode", "Sleeping Mode").
+ * @param allowStarredCalls If true, priority calls from starred contacts will bypass DND.
+ * @param allowAlarms If true, alarms will bypass DND.
+ * @return The unique ruleId generated by Android, or null if creation failed/no permission.
+ */
+ fun createDndMode(
+ name: String,
+ allowStarredCalls: Boolean = true,
+ allowAlarms: Boolean = true
+ ): String? {
+ if (!hasDndAccess()) return null
+
+ // Unique condition identifier Uri for this mode
+ val conditionId = Uri.parse("vflow://dnd/rule/${System.currentTimeMillis()}")
+
+ val rule = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ // Android 10+ supports custom ZenPolicies
+ val policyBuilder = ZenPolicy.Builder()
+ .allowAlarms(allowAlarms)
+ .allowReminders(false)
+ .allowEvents(false)
+
+ if (allowStarredCalls) {
+ policyBuilder.allowCalls(ZenPolicy.PEOPLE_TYPE_STARRED)
+ policyBuilder.allowRepeatCallers(true)
+ } else {
+ policyBuilder.allowCalls(ZenPolicy.PEOPLE_TYPE_NONE)
+ }
+
+ AutomaticZenRule.Builder(name, conditionId)
+ .setOwner(providerComponent)
+ .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(policyBuilder.build())
+ .setEnabled(true)
+ .build()
+ } else {
+ // Fallback for API 23 to 28
+ @Suppress("DEPRECATION")
+ AutomaticZenRule(
+ name,
+ providerComponent,
+ conditionId,
+ NotificationManager.INTERRUPTION_FILTER_PRIORITY,
+ true
+ )
+ }
+
+ return try {
+ notificationManager.addAutomaticZenRule(rule)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
+ }
+ }
+
+ /**
+ * Toggles a registered DND mode.
+ * @param ruleId The unique ID of the rule returned by createDndMode.
+ * @param isActive True to activate the mode, false to deactivate.
+ */
+ fun setDndModeState(ruleId: String, isActive: Boolean): Boolean {
+ if (!hasDndAccess()) return false
+
+ val rule = notificationManager.getAutomaticZenRule(ruleId) ?: return false
+ val conditionId = rule.conditionId ?: return false
+
+ val state = if (isActive) Condition.STATE_TRUE else Condition.STATE_FALSE
+ val summary = if (isActive) "Active via vFlow Workflow" else "Inactive"
+ val condition = Condition(conditionId, summary, state)
+
+ return try {
+ notificationManager.setAutomaticZenRuleState(ruleId, condition)
+ true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+ }
+
+ /**
+ * Deletes a registered DND mode rule.
+ */
+ fun deleteDndMode(ruleId: String): Boolean {
+ if (!hasDndAccess()) return false
+ return try {
+ notificationManager.removeAutomaticZenRule(ruleId)
+ true
+ } catch (e: Exception) {
+ e.printStackTrace()
+ false
+ }
+ }
+
+ /**
+ * Lists all Zen rules owned by this app.
+ */
+ fun getRegisteredModes(): Map {
+ if (!hasDndAccess()) return emptyMap()
+ return notificationManager.automaticZenRules ?: emptyMap()
+ }
+}
diff --git a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/DoNotDisturbModule.kt b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/DoNotDisturbModule.kt
index 09a73212..67800c1d 100644
--- a/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/DoNotDisturbModule.kt
+++ b/app/src/main/java/com/chaomixian/vflow/core/workflow/module/system/DoNotDisturbModule.kt
@@ -7,6 +7,7 @@ import android.content.Context
import android.net.Uri
import android.os.Build
import android.service.notification.Condition
+import android.service.notification.ZenPolicy
import com.chaomixian.vflow.R
import com.chaomixian.vflow.core.execution.ExecutionContext
import com.chaomixian.vflow.core.module.ActionMetadata
@@ -86,6 +87,14 @@ class DoNotDisturbModule : BaseModule() {
),
legacyValueMap = ACTION_LEGACY_MAP,
inputStyle = InputStyle.CHIP_GROUP
+ ),
+ InputDefinition(
+ id = "mode_name",
+ name = "模式名称",
+ nameStringRes = R.string.param_vflow_system_do_not_disturb_mode_name,
+ staticType = ParameterType.STRING,
+ defaultValue = "vFlow",
+ inputStyle = InputStyle.DEFAULT
)
)
@@ -110,9 +119,10 @@ class DoNotDisturbModule : BaseModule() {
step.parameters["action"] as? String,
ACTION_TOGGLE
) ?: ACTION_TOGGLE
+ val modeName = step.parameters["mode_name"] as? String ?: "vFlow"
val displayText = getActionDisplayName(context, action)
val actionPill = PillUtil.Pill(displayText, "action", isModuleOption = true)
- return PillUtil.buildSpannable(context, "${metadata.getLocalizedName(context)}: ", actionPill)
+ return PillUtil.buildSpannable(context, "${metadata.getLocalizedName(context)} ($modeName): ", actionPill)
}
override suspend fun execute(
@@ -125,6 +135,10 @@ class DoNotDisturbModule : BaseModule() {
ACTION_TOGGLE
) ?: ACTION_TOGGLE
+ val modeName = context.getVariable("mode_name").asString()
+ .takeIf { it.isNotBlank() }
+ ?: appContext.getString(R.string.module_vflow_system_do_not_disturb_name)
+
val notificationManager = context.applicationContext
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -139,40 +153,37 @@ class DoNotDisturbModule : BaseModule() {
onProgress(ProgressUpdate(appContext.getString(R.string.msg_vflow_system_do_not_disturb_setting, actionName)))
return try {
- val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
- val ruleId = ensureRule(context.applicationContext, notificationManager)
- ?: return ExecutionResult.Failure(
- appContext.getString(R.string.error_vflow_system_do_not_disturb_set_failed),
- appContext.getString(R.string.error_vflow_system_do_not_disturb_rule_create_failed)
- )
- val currentState = getRuleState(notificationManager, ruleId)
- val targetState = resolveTargetState(currentState, action)
- ?: return ExecutionResult.Failure(
- appContext.getString(R.string.error_vflow_system_do_not_disturb_invalid_action),
- appContext.getString(R.string.error_vflow_system_do_not_disturb_invalid_action_detail, action)
- )
- val enabled = targetState == Condition.STATE_TRUE
- notificationManager.setAutomaticZenRuleState(
- ruleId,
- Condition(
- conditionId(context.applicationContext),
- getConditionSummary(context.applicationContext, enabled),
- targetState,
- Condition.SOURCE_USER_ACTION
- )
+ val ruleId = ensureRule(context.applicationContext, notificationManager, modeName)
+ ?: return ExecutionResult.Failure(
+ appContext.getString(R.string.error_vflow_system_do_not_disturb_set_failed),
+ appContext.getString(R.string.error_vflow_system_do_not_disturb_rule_create_failed)
)
- enabled
- } else {
- val targetFilter = resolveLegacyTargetFilter(
- notificationManager.currentInterruptionFilter,
- action
- ) ?: return ExecutionResult.Failure(
+ // Read the actual conditionId from the existing rule to avoid mismatch
+ val rule = notificationManager.getAutomaticZenRule(ruleId)
+ val actualConditionId = rule?.conditionId ?: conditionId(context.applicationContext, modeName)
+ val currentState = getRuleState(notificationManager, ruleId)
+ val targetState = resolveTargetState(currentState, action)
+ ?: return ExecutionResult.Failure(
appContext.getString(R.string.error_vflow_system_do_not_disturb_invalid_action),
appContext.getString(R.string.error_vflow_system_do_not_disturb_invalid_action_detail, action)
)
- notificationManager.setInterruptionFilter(targetFilter)
- targetFilter != NotificationManager.INTERRUPTION_FILTER_ALL
+ val enabled = targetState == Condition.STATE_TRUE
+
+ val source = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
+ Condition.SOURCE_CONTEXT
+ } else {
+ Condition.SOURCE_USER_ACTION
}
+
+ notificationManager.setAutomaticZenRuleState(
+ ruleId,
+ Condition(
+ actualConditionId,
+ getConditionSummary(context.applicationContext, enabled),
+ targetState,
+ source
+ )
+ )
onProgress(ProgressUpdate(appContext.getString(R.string.msg_vflow_system_do_not_disturb_completed)))
ExecutionResult.Success(
mapOf(
@@ -208,10 +219,10 @@ class DoNotDisturbModule : BaseModule() {
internal fun resolveLegacyTargetFilter(currentFilter: Int, action: String): Int? {
return when (action) {
- ACTION_ON -> NotificationManager.INTERRUPTION_FILTER_NONE
+ ACTION_ON -> NotificationManager.INTERRUPTION_FILTER_PRIORITY
ACTION_OFF -> NotificationManager.INTERRUPTION_FILTER_ALL
ACTION_TOGGLE -> if (currentFilter == NotificationManager.INTERRUPTION_FILTER_ALL) {
- NotificationManager.INTERRUPTION_FILTER_NONE
+ NotificationManager.INTERRUPTION_FILTER_PRIORITY
} else {
NotificationManager.INTERRUPTION_FILTER_ALL
}
@@ -219,36 +230,91 @@ class DoNotDisturbModule : BaseModule() {
}
}
- private fun ensureRule(context: Context, notificationManager: NotificationManager): String? {
+ private fun ensureRule(context: Context, notificationManager: NotificationManager, ruleName: String): String? {
val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
- val existingRuleId = prefs.getString(PREF_RULE_ID, null)
- if (existingRuleId != null && notificationManager.getAutomaticZenRule(existingRuleId) != null) {
- return existingRuleId
+ val prefKey = "${PREF_RULE_ID}_$ruleName"
+ val savedRuleId = prefs.getString(prefKey, prefs.getString(PREF_RULE_ID, null))
+
+ if (savedRuleId != null) {
+ val existingRule = notificationManager.getAutomaticZenRule(savedRuleId)
+ if (existingRule != null && existingRule.name == ruleName) {
+ if (existingRule.owner != null) {
+ try {
+ notificationManager.removeAutomaticZenRule(savedRuleId)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } else {
+ if (!existingRule.isEnabled) {
+ existingRule.isEnabled = true
+ try {
+ notificationManager.updateAutomaticZenRule(savedRuleId, existingRule)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ prefs.edit().putString(prefKey, savedRuleId).apply()
+ return savedRuleId
+ }
+ }
}
- val rule = createAutomaticZenRule(context)
- val ruleId = notificationManager.addAutomaticZenRule(rule)
- if (ruleId != null) {
- prefs.edit().putString(PREF_RULE_ID, ruleId).apply()
+ val existingRuleEntry = notificationManager.automaticZenRules?.entries?.find { it.value.name == ruleName }
+ if (existingRuleEntry != null) {
+ val existingRule = existingRuleEntry.value
+ if (existingRule.owner != null) {
+ try {
+ notificationManager.removeAutomaticZenRule(existingRuleEntry.key)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ } else {
+ if (!existingRule.isEnabled) {
+ existingRule.isEnabled = true
+ try {
+ notificationManager.updateAutomaticZenRule(existingRuleEntry.key, existingRule)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+ }
+ prefs.edit().putString(prefKey, existingRuleEntry.key)
+ .putString(PREF_RULE_ID, existingRuleEntry.key).apply()
+ return existingRuleEntry.key
+ }
+ }
+
+ val rule = createAutomaticZenRule(context, ruleName)
+ return try {
+ val ruleId = notificationManager.addAutomaticZenRule(rule)
+ if (ruleId != null) {
+ prefs.edit().putString(prefKey, ruleId).apply()
+ }
+ ruleId
+ } catch (e: Exception) {
+ e.printStackTrace()
+ null
}
- return ruleId
}
- internal fun createAutomaticZenRule(context: Context): AutomaticZenRule {
+ internal fun createAutomaticZenRule(context: Context, ruleName: String): AutomaticZenRule {
return createAutomaticZenRule(
- context.getString(R.string.module_vflow_system_do_not_disturb_name),
+ ruleName,
context.packageName
)
}
internal fun createAutomaticZenRule(ruleName: String, packageName: String): AutomaticZenRule {
+ val zenPolicy = ZenPolicy.Builder()
+ .disallowAllSounds()
+ .hideAllVisualEffects()
+ .build()
return AutomaticZenRule(
ruleName,
- null,
+ android.content.ComponentName(packageName, com.chaomixian.vflow.core.system.DndConditionProviderService::class.java.name),
createRuleConfigurationActivity(packageName),
- conditionId(packageName),
- null,
- NotificationManager.INTERRUPTION_FILTER_NONE,
+ conditionId(packageName, ruleName),
+ zenPolicy,
+ NotificationManager.INTERRUPTION_FILTER_PRIORITY,
true
)
}
@@ -271,15 +337,16 @@ class DoNotDisturbModule : BaseModule() {
}
}
- private fun conditionId(context: Context): Uri {
- return conditionId(context.packageName)
+ private fun conditionId(context: Context, ruleName: String): Uri {
+ return conditionId(context.packageName, ruleName)
}
- private fun conditionId(packageName: String): Uri {
+ private fun conditionId(packageName: String, ruleName: String): Uri {
return Uri.Builder()
.scheme(CONDITION_SCHEME)
.authority(CONDITION_HOST)
.appendPath(packageName)
+ .appendPath(ruleName)
.build()
}
diff --git a/app/src/main/java/com/chaomixian/vflow/services/ExecutionNotificationManager.kt b/app/src/main/java/com/chaomixian/vflow/services/ExecutionNotificationManager.kt
index 503cc6aa..6b431df5 100644
--- a/app/src/main/java/com/chaomixian/vflow/services/ExecutionNotificationManager.kt
+++ b/app/src/main/java/com/chaomixian/vflow/services/ExecutionNotificationManager.kt
@@ -127,6 +127,7 @@ object ExecutionNotificationManager {
.setOngoing(false)
.setRequestPromotedOngoing(false) // 取消提升请求
.setAutoCancel(true)
+ .setTimeoutAfter(3000) // 3秒后自动消失
// 通过 setProgress(0, 0, false) 来移除进度条
.setProgress(0, 0, false)
// (可选) 可以临时将小图标变为完成状态,增强视觉反馈
@@ -139,6 +140,7 @@ object ExecutionNotificationManager {
.setOngoing(false)
.setRequestPromotedOngoing(false) // 取消提升请求
.setAutoCancel(true)
+ .setTimeoutAfter(3000) // 3秒后自动消失
// 移除进度条
.setProgress(0, 0, false)
.setSmallIcon(R.drawable.rounded_close_small_24)
@@ -169,6 +171,7 @@ object ExecutionNotificationManager {
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
+ .setTimeoutAfter(3000) // 3秒后自动消失
}
is ExecutionNotificationState.Cancelled -> {
builder
@@ -176,6 +179,7 @@ object ExecutionNotificationManager {
.setProgress(0, 0, false)
.setOngoing(false)
.setAutoCancel(true)
+ .setTimeoutAfter(3000) // 3秒后自动消失
}
}
notificationManager.notify(NOTIFICATION_ID, builder.build())
diff --git a/app/src/main/res/values-en/strings_module.xml b/app/src/main/res/values-en/strings_module.xml
index d19bc403..7b0c273b 100644
--- a/app/src/main/res/values-en/strings_module.xml
+++ b/app/src/main/res/values-en/strings_module.xml
@@ -2906,6 +2906,7 @@
Do Not Disturb
Turn Do Not Disturb on, off, or toggle it. Android 15+ uses vFlow\'s own DND rule; earlier Android versions use the global DND switch.
Action
+ Mode Name
Success
Do Not Disturb Enabled
Toggle
diff --git a/app/src/main/res/values/strings_module.xml b/app/src/main/res/values/strings_module.xml
index 62f4ff4b..21880d49 100644
--- a/app/src/main/res/values/strings_module.xml
+++ b/app/src/main/res/values/strings_module.xml
@@ -2943,6 +2943,7 @@
免打扰模式
开启、关闭或切换免打扰模式。Android 15+ 使用 vFlow 自己的勿扰规则,Android 15 以下使用全局勿扰开关。
操作
+ 模式名称
是否成功
免打扰已开启
切换