Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,17 @@
android:exported="false"
android:foregroundServiceType="specialUse" />

<!-- DND rule owner service -->
<service
android:name=".core.system.DndConditionProviderService"
android:label="vFlow DND Manager"
android:permission="android.permission.BIND_CONDITION_PROVIDER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.ConditionProviderService" />
</intent-filter>
</service>

<receiver
android:name=".services.BootReceiver"
android:exported="true">
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
141 changes: 141 additions & 0 deletions app/src/main/java/com/chaomixian/vflow/core/system/DndModeManager.kt
Original file line number Diff line number Diff line change
@@ -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<String, AutomaticZenRule> {
if (!hasDndAccess()) return emptyMap()
return notificationManager.automaticZenRules ?: emptyMap()
}
}
Loading