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 以下使用全局勿扰开关。 操作 + 模式名称 是否成功 免打扰已开启 切换