From 1bf79edc7cc1445cd54af5d99d4cb232e15c09db Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sat, 20 Jun 2026 15:02:25 +0530 Subject: [PATCH 01/10] feat(system): add scenario-specific DND modes using AutomaticZenRule --- app/src/main/AndroidManifest.xml | 11 ++ .../system/DndConditionProviderService.kt | 18 +++ .../vflow/core/system/DndModeManager.kt | 141 ++++++++++++++++++ 3 files changed, 170 insertions(+) create mode 100644 app/src/main/java/com/chaomixian/vflow/core/system/DndConditionProviderService.kt create mode 100644 app/src/main/java/com/chaomixian/vflow/core/system/DndModeManager.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index dece5465..0c2132b4 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..e2758fb4 --- /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, providerComponent) + .setConditionId(conditionId) + .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() + } +} From 34f0a04c7debf2627a189b04862de887d0b83fc1 Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sat, 20 Jun 2026 15:20:40 +0530 Subject: [PATCH 02/10] fix(system): resolve type mismatch in DndModeManager.kt --- .../java/com/chaomixian/vflow/core/system/DndModeManager.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index e2758fb4..ecc0efde 100644 --- a/app/src/main/java/com/chaomixian/vflow/core/system/DndModeManager.kt +++ b/app/src/main/java/com/chaomixian/vflow/core/system/DndModeManager.kt @@ -67,8 +67,8 @@ class DndModeManager(private val context: Context) { policyBuilder.allowCalls(ZenPolicy.PEOPLE_TYPE_NONE) } - AutomaticZenRule.Builder(name, providerComponent) - .setConditionId(conditionId) + AutomaticZenRule.Builder(name, conditionId) + .setOwner(providerComponent) .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY) .setZenPolicy(policyBuilder.build()) .setEnabled(true) From f486b28a4d067a35592d20ead0bae6badba4373b Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sat, 20 Jun 2026 15:32:01 +0530 Subject: [PATCH 03/10] feat(system): add custom DND mode name support for separate ZenRules --- .../module/system/DoNotDisturbModule.kt | 52 ++++++++++++------- app/src/main/res/values-en/strings_module.xml | 1 + app/src/main/res/values/strings_module.xml | 1 + 3 files changed, 35 insertions(+), 19 deletions(-) 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..d53a03af 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 @@ -86,6 +86,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 +118,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 +134,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 @@ -140,7 +153,7 @@ class DoNotDisturbModule : BaseModule() { return try { val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - val ruleId = ensureRule(context.applicationContext, notificationManager) + 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) @@ -155,7 +168,7 @@ class DoNotDisturbModule : BaseModule() { notificationManager.setAutomaticZenRuleState( ruleId, Condition( - conditionId(context.applicationContext), + conditionId(context.applicationContext, modeName), getConditionSummary(context.applicationContext, enabled), targetState, Condition.SOURCE_USER_ACTION @@ -219,24 +232,24 @@ class DoNotDisturbModule : BaseModule() { } } - private fun ensureRule(context: Context, notificationManager: NotificationManager): 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 + private fun ensureRule(context: Context, notificationManager: NotificationManager, ruleName: String): String? { + val existingRuleEntry = notificationManager.automaticZenRules?.entries?.find { it.value.name == ruleName } + if (existingRuleEntry != null) { + return existingRuleEntry.key } - val rule = createAutomaticZenRule(context) - val ruleId = notificationManager.addAutomaticZenRule(rule) - if (ruleId != null) { - prefs.edit().putString(PREF_RULE_ID, ruleId).apply() + val rule = createAutomaticZenRule(context, ruleName) + return try { + notificationManager.addAutomaticZenRule(rule) + } 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 ) } @@ -246,7 +259,7 @@ class DoNotDisturbModule : BaseModule() { ruleName, null, createRuleConfigurationActivity(packageName), - conditionId(packageName), + conditionId(packageName, ruleName), null, NotificationManager.INTERRUPTION_FILTER_NONE, true @@ -271,15 +284,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/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 以下使用全局勿扰开关。 操作 + 模式名称 是否成功 免打扰已开启 切换 From a9f8c0b768444ac06e23f15a8ec825d4bce10eff Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sat, 20 Jun 2026 15:39:52 +0530 Subject: [PATCH 04/10] feat(system): bind DndConditionProviderService as owner for automatic DND rules --- .../vflow/core/workflow/module/system/DoNotDisturbModule.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 d53a03af..3dfdcf79 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 @@ -152,7 +152,7 @@ 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 enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val ruleId = ensureRule(context.applicationContext, notificationManager, modeName) ?: return ExecutionResult.Failure( appContext.getString(R.string.error_vflow_system_do_not_disturb_set_failed), @@ -255,9 +255,10 @@ class DoNotDisturbModule : BaseModule() { } internal fun createAutomaticZenRule(ruleName: String, packageName: String): AutomaticZenRule { + val ownerComponent = ComponentName(packageName, "com.chaomixian.vflow.core.system.DndConditionProviderService") return AutomaticZenRule( ruleName, - null, + ownerComponent, createRuleConfigurationActivity(packageName), conditionId(packageName, ruleName), null, From 9eeeee04e61ddee7fdee277e9b6d20c0a6a269b6 Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sat, 20 Jun 2026 15:43:20 +0530 Subject: [PATCH 05/10] fix(system): revert AutomaticZenRule owner to null for direct ZenRule state toggling --- .../vflow/core/workflow/module/system/DoNotDisturbModule.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 3dfdcf79..87e65efd 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 @@ -255,10 +255,9 @@ class DoNotDisturbModule : BaseModule() { } internal fun createAutomaticZenRule(ruleName: String, packageName: String): AutomaticZenRule { - val ownerComponent = ComponentName(packageName, "com.chaomixian.vflow.core.system.DndConditionProviderService") return AutomaticZenRule( ruleName, - ownerComponent, + null, createRuleConfigurationActivity(packageName), conditionId(packageName, ruleName), null, From 96d48ed1d8f7d0431d54b9b92e2e02fd11937acc Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sat, 20 Jun 2026 15:47:10 +0530 Subject: [PATCH 06/10] fix(system): read actual conditionId from existing rule to fix DND activation mismatch --- .../core/workflow/module/system/DoNotDisturbModule.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 87e65efd..64987210 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 @@ -152,12 +152,15 @@ 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.M) { + val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { 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) ) + // 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( @@ -168,7 +171,7 @@ class DoNotDisturbModule : BaseModule() { notificationManager.setAutomaticZenRuleState( ruleId, Condition( - conditionId(context.applicationContext, modeName), + actualConditionId, getConditionSummary(context.applicationContext, enabled), targetState, Condition.SOURCE_USER_ACTION From a98064fba36bbd9339e099f06104b6afe6322db1 Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sat, 20 Jun 2026 15:49:23 +0530 Subject: [PATCH 07/10] fix(system): remove mode_name input from DND module - use single fixed rule name --- .../module/system/DoNotDisturbModule.kt | 19 ++++--------------- app/src/main/res/values-en/strings_module.xml | 2 +- app/src/main/res/values/strings_module.xml | 2 +- 3 files changed, 6 insertions(+), 17 deletions(-) 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 64987210..050e88fb 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 @@ -86,14 +86,6 @@ 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 ) ) @@ -118,10 +110,9 @@ 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)} ($modeName): ", actionPill) + return PillUtil.buildSpannable(context, "${metadata.getLocalizedName(context)}: ", actionPill) } override suspend fun execute( @@ -134,9 +125,7 @@ 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 ruleName = appContext.getString(R.string.module_vflow_system_do_not_disturb_name) val notificationManager = context.applicationContext .getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager @@ -153,14 +142,14 @@ class DoNotDisturbModule : BaseModule() { return try { val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - val ruleId = ensureRule(context.applicationContext, notificationManager, modeName) + val ruleId = ensureRule(context.applicationContext, notificationManager, ruleName) ?: 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) ) // 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 actualConditionId = rule?.conditionId ?: conditionId(context.applicationContext, ruleName) val currentState = getRuleState(notificationManager, ruleId) val targetState = resolveTargetState(currentState, action) ?: return ExecutionResult.Failure( diff --git a/app/src/main/res/values-en/strings_module.xml b/app/src/main/res/values-en/strings_module.xml index 7b0c273b..8166ae86 100644 --- a/app/src/main/res/values-en/strings_module.xml +++ b/app/src/main/res/values-en/strings_module.xml @@ -2906,7 +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 21880d49..620101f9 100644 --- a/app/src/main/res/values/strings_module.xml +++ b/app/src/main/res/values/strings_module.xml @@ -2943,7 +2943,7 @@ 免打扰模式 开启、关闭或切换免打扰模式。Android 15+ 使用 vFlow 自己的勿扰规则,Android 15 以下使用全局勿扰开关。 操作 - 模式名称 + 是否成功 免打扰已开启 切换 From 96912032ab802089bdcbbae52229e3d15f2b8317 Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sat, 20 Jun 2026 16:41:15 +0530 Subject: [PATCH 08/10] feat(system): track DND mode by ruleId and sync renamed modes to system settings --- .../module/system/DoNotDisturbModule.kt | 44 ++++++++++++++++--- app/src/main/res/values-en/strings_module.xml | 2 +- app/src/main/res/values/strings_module.xml | 2 +- 3 files changed, 41 insertions(+), 7 deletions(-) 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 050e88fb..28bf9857 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 @@ -86,6 +86,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 +118,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,7 +134,9 @@ class DoNotDisturbModule : BaseModule() { ACTION_TOGGLE ) ?: ACTION_TOGGLE - val ruleName = appContext.getString(R.string.module_vflow_system_do_not_disturb_name) + 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 @@ -142,14 +153,14 @@ class DoNotDisturbModule : BaseModule() { return try { val enabled = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) { - val ruleId = ensureRule(context.applicationContext, notificationManager, ruleName) + 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) ) // Read the actual conditionId from the existing rule to avoid mismatch val rule = notificationManager.getAutomaticZenRule(ruleId) - val actualConditionId = rule?.conditionId ?: conditionId(context.applicationContext, ruleName) + val actualConditionId = rule?.conditionId ?: conditionId(context.applicationContext, modeName) val currentState = getRuleState(notificationManager, ruleId) val targetState = resolveTargetState(currentState, action) ?: return ExecutionResult.Failure( @@ -225,14 +236,37 @@ class DoNotDisturbModule : BaseModule() { } private fun ensureRule(context: Context, notificationManager: NotificationManager, ruleName: String): String? { + val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + val savedRuleId = prefs.getString(PREF_RULE_ID, null) + + if (savedRuleId != null) { + val existingRule = notificationManager.getAutomaticZenRule(savedRuleId) + if (existingRule != null) { + if (existingRule.name != ruleName) { + existingRule.name = ruleName + try { + notificationManager.updateAutomaticZenRule(savedRuleId, existingRule) + } catch (e: Exception) { + e.printStackTrace() + } + } + return savedRuleId + } + } + val existingRuleEntry = notificationManager.automaticZenRules?.entries?.find { it.value.name == ruleName } if (existingRuleEntry != null) { + prefs.edit().putString(PREF_RULE_ID, existingRuleEntry.key).apply() return existingRuleEntry.key } val rule = createAutomaticZenRule(context, ruleName) return try { - notificationManager.addAutomaticZenRule(rule) + val ruleId = notificationManager.addAutomaticZenRule(rule) + if (ruleId != null) { + prefs.edit().putString(PREF_RULE_ID, ruleId).apply() + } + ruleId } catch (e: Exception) { e.printStackTrace() null diff --git a/app/src/main/res/values-en/strings_module.xml b/app/src/main/res/values-en/strings_module.xml index 8166ae86..7b0c273b 100644 --- a/app/src/main/res/values-en/strings_module.xml +++ b/app/src/main/res/values-en/strings_module.xml @@ -2906,7 +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 620101f9..21880d49 100644 --- a/app/src/main/res/values/strings_module.xml +++ b/app/src/main/res/values/strings_module.xml @@ -2943,7 +2943,7 @@ 免打扰模式 开启、关闭或切换免打扰模式。Android 15+ 使用 vFlow 自己的勿扰规则,Android 15 以下使用全局勿扰开关。 操作 - + 模式名称 是否成功 免打扰已开启 切换 From 3a06215405ed573a865bf3ffb2bc6c202db52a98 Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sat, 20 Jun 2026 16:45:27 +0530 Subject: [PATCH 09/10] fix(system): fix background triggers by adding ZenPolicy and using SOURCE_CONTEXT --- .../core/workflow/module/system/DoNotDisturbModule.kt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 28bf9857..787be9f3 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 @@ -174,7 +175,7 @@ class DoNotDisturbModule : BaseModule() { actualConditionId, getConditionSummary(context.applicationContext, enabled), targetState, - Condition.SOURCE_USER_ACTION + Condition.SOURCE_CONTEXT ) ) enabled @@ -281,12 +282,16 @@ class DoNotDisturbModule : BaseModule() { } internal fun createAutomaticZenRule(ruleName: String, packageName: String): AutomaticZenRule { + val zenPolicy = ZenPolicy.Builder() + .disallowAllSounds() + .hideAllVisualEffects() + .build() return AutomaticZenRule( ruleName, null, createRuleConfigurationActivity(packageName), conditionId(packageName, ruleName), - null, + zenPolicy, NotificationManager.INTERRUPTION_FILTER_NONE, true ) From 7d558e31ea587f24e72d353f90e9ff6b5a9f6613 Mon Sep 17 00:00:00 2001 From: blindman81 Date: Sun, 21 Jun 2026 16:05:04 +0530 Subject: [PATCH 10/10] fix(system): resolve DND rule toggling and workflow notification persistence bugs --- app/src/main/AndroidManifest.xml | 2 +- .../module/system/DoNotDisturbModule.kt | 112 +++++++++++------- .../services/ExecutionNotificationManager.kt | 4 + 3 files changed, 72 insertions(+), 46 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0c2132b4..f4e1b29d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -278,7 +278,7 @@ android:permission="android.permission.BIND_CONDITION_PROVIDER_SERVICE" android:exported="true"> - + 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 787be9f3..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 @@ -153,43 +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, 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) - ) - // 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) - ) - val enabled = targetState == Condition.STATE_TRUE - notificationManager.setAutomaticZenRuleState( - ruleId, - Condition( - actualConditionId, - getConditionSummary(context.applicationContext, enabled), - targetState, - Condition.SOURCE_CONTEXT - ) + 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( @@ -225,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 } @@ -238,34 +232,62 @@ class DoNotDisturbModule : BaseModule() { private fun ensureRule(context: Context, notificationManager: NotificationManager, ruleName: String): String? { val prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - val savedRuleId = prefs.getString(PREF_RULE_ID, null) + 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) { - if (existingRule.name != ruleName) { - existingRule.name = ruleName + if (existingRule != null && existingRule.name == ruleName) { + if (existingRule.owner != null) { try { - notificationManager.updateAutomaticZenRule(savedRuleId, existingRule) + 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 } - return savedRuleId } } val existingRuleEntry = notificationManager.automaticZenRules?.entries?.find { it.value.name == ruleName } if (existingRuleEntry != null) { - prefs.edit().putString(PREF_RULE_ID, existingRuleEntry.key).apply() - return existingRuleEntry.key + 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(PREF_RULE_ID, ruleId).apply() + prefs.edit().putString(prefKey, ruleId).apply() } ruleId } catch (e: Exception) { @@ -288,11 +310,11 @@ class DoNotDisturbModule : BaseModule() { .build() return AutomaticZenRule( ruleName, - null, + android.content.ComponentName(packageName, com.chaomixian.vflow.core.system.DndConditionProviderService::class.java.name), createRuleConfigurationActivity(packageName), conditionId(packageName, ruleName), zenPolicy, - NotificationManager.INTERRUPTION_FILTER_NONE, + NotificationManager.INTERRUPTION_FILTER_PRIORITY, true ) } 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())