Skip to main content

Mobile Development7 min read

Control Android Ringer Mode in Flutter

Control Android ringer mode from Flutter with a MethodChannel and AudioManager: silent, vibrate, and normal profiles plus the Do Not Disturb access step.

  • #flutter
  • #android
  • #method-channel
  • #ringer-mode
  • #mobile-development
Control Android Ringer Mode in Flutter

Every meeting has that moment: a phone rings at the worst possible time. If you have ever built an app that needs to prevent it — an auto-silent scheduler, a focus-mode companion, a prayer-time silencer — you have hit the same wall I did while building Smart Silent, my Android app that automatically switches phone sound profiles based on scheduled times and geographic location. Flutter has no built-in API for the ringer mode.

Android owns the ringer, and Flutter can only reach it through a platform channel. The implementation itself is short, but the permission story around Do Not Disturb access trips up almost everyone who attempts this. This guide walks through the complete, working setup: the Kotlin side with AudioManager, the Dart service, the DND access flow, and the production details that turn it from a demo button into a real feature.

Why Flutter Cannot Do This Out of the Box

Flutter's framework and plugin ecosystem cover cameras, sensors, and notifications, but nothing in Dart exposes the ringer mode. Android guards sound profiles behind AudioManager, a system service that Dart cannot touch directly. The standard bridge is a MethodChannel: one channel, a handful of methods (get the current mode, set a mode, check and request DND access), and roughly sixty lines of Kotlin.

There is one more wrinkle worth knowing up front. Since Android 6.0 (API 23), switching to silent or vibrate requires the user to grant your app notification policy access — the "Do Not Disturb access" special permission. Without it, your setRingerMode calls are silently ignored. Skip this step and you will spend an afternoon wondering why your code "works" but nothing changes on the device.

The Native Side: AudioManager Through a MethodChannel

Start with the manifest. The permission you need is ACCESS_NOTIFICATION_POLICY:

<uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />

This is a normal-level permission: you declare it, and there is no runtime dialog. The actual grant happens on a system settings screen, which we will open from Kotlin.

Next, wire the channel in MainActivity.kt:

package com.example.app

import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.media.AudioManager
import android.os.Build
import android.provider.Settings
import androidx.annotation.NonNull
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity : FlutterActivity() {
    private val channel = "app.example/ringer"

    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel)
            .setMethodCallHandler { call, result ->
                val audioManager =
                    getSystemService(Context.AUDIO_SERVICE) as AudioManager
                when (call.method) {
                    "getRingerMode" -> {
                        result.success(audioManager.ringerMode)
                    }
                    "setRingerMode" -> {
                        val mode = call.argument<Int>("mode")
                            ?: AudioManager.RINGER_MODE_NORMAL
                        audioManager.ringerMode = mode
                        result.success(null)
                    }
                    "hasDndAccess" -> {
                        val notificationManager = getSystemService(
                            Context.NOTIFICATION_SERVICE
                        ) as NotificationManager
                        val granted =
                            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                                notificationManager
                                    .isNotificationPolicyAccessGranted
                            } else {
                                true
                            }
                        result.success(granted)
                    }
                    "requestDndAccess" -> {
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                            startActivity(
                                Intent(
                                    Settings
                                        .ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS
                                )
                            )
                        }
                        result.success(null)
                    }
                    else -> result.notImplemented()
                }
            }
    }
}

The four methods cover the whole surface: read the current mode, write a mode, check whether DND access is granted, and deep-link the user to the exact settings screen where they grant it.

The Dart Side: A Small, Honest Service

On the Dart side, wrap the channel in a service that refuses to apply silent or vibrate profiles before the permission is granted. Failing loudly here is a feature — it is far better than a silent no-op that confuses your users.

import 'package:flutter/services.dart';

enum SoundProfile { normal, vibrate, silent }

class RingerModeService {
  static const _channel = MethodChannel('app.example/ringer');

  // Mirrors AudioManager.RINGER_MODE_* constants.
  static const _modeValues = {
    SoundProfile.normal: 2,
    SoundProfile.vibrate: 1,
    SoundProfile.silent: 0,
  };

  Future<int> getRingerMode() async {
    return await _channel.invokeMethod<int>('getRingerMode') ?? 2;
  }

  Future<bool> hasDndAccess() async {
    return await _channel.invokeMethod<bool>('hasDndAccess') ?? false;
  }

  Future<void> openDndSettings() async {
    await _channel.invokeMethod('requestDndAccess');
  }

  Future<void> applyProfile(SoundProfile profile) async {
    if (profile != SoundProfile.normal && !await hasDndAccess()) {
      throw StateError(
        'Do Not Disturb access is required for silent and vibrate profiles.',
      );
    }
    try {
      await _channel.invokeMethod('setRingerMode', {
        'mode': _modeValues[profile],
      });
    } on PlatformException catch (e) {
      throw StateError('Failed to set ringer mode: ${e.message}');
    }
  }
}

A settings screen in your app can now be genuinely simple: show the three profiles, and when the user picks silent or vibrate, first check hasDndAccess(). If it is false, show a short explanation ("Android requires Do Not Disturb access so the app can silence your phone") with a button that calls openDndSettings(). When the user returns, re-check before applying.

Do Not Disturb Access: The Step Most Tutorials Skip

This deserves its own section because it is where nearly every implementation breaks. The rules:

  • ACCESS_NOTIFICATION_POLICY is declared in the manifest and needs no runtime permission prompt.
  • The grant itself is a special app-access toggle under Settings, opened with Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS.
  • Always check isNotificationPolicyAccessGranted before applying a silent or vibrate profile, and re-check when the user returns from settings — they may have opened the screen and changed their mind.
  • Design for the denial: keep the app usable on the normal profile and let the user retry the grant later instead of blocking the whole screen.

From a Button to a Feature: Scheduling Profiles in a Real App

A button that toggles silent mode is a demo. A feature remembers the previous profile and restores it. That is the core of how Smart Silent works: it applies a silent profile when a scheduled period starts or when you enter a geofenced area, then reverts to whatever the phone was on before once the event ends or you leave. Getting the restore right matters more than the muting — nobody forgives an app that leaves their phone silent through the next morning's alarms.

In production this pairs the channel above with two more pieces: a scheduler for time-based profiles and geofencing for location-based ones. Both are deep topics on their own — see my guides on battery-friendly geofencing in Flutter and exact alarm scheduling on Android. The ringer channel is the actuator; scheduling and geofencing are the triggers. Keeping those layers separate made the Smart Silent codebase far easier to test: the profile logic never knows why it was asked to switch, only what to switch to and what to restore afterwards.

One more production lesson from that app: keep the battery cost of the triggers near zero. Smart Silent's location-based switching is built on battery-efficient geofencing rather than continuous location polling, and the app reports under 1.2% battery usage per 24 hours. A profile switcher that drains the battery to save you from one embarrassing ring is a net loss.

Testing Checklist

  • Test on a physical device. The emulator has no real audio routing, and ringer changes may appear to work or fail in ways that do not reproduce on hardware.
  • Test all three profiles, and test the denial path: with DND access revoked, applying silent must produce your clear error, not a crash.
  • Test the restore path end to end: apply silent, end the event, and confirm the original profile returns.
  • Test on the Android versions you support, especially the API 23 boundary where the DND requirement begins, and on at least one heavily skinned OEM device — some manufacturers add battery-optimization screens that can kill background triggers.

What Does Not Work

Honest limits, so you can scope the feature correctly:

  • iOS exposes no public API for changing the ringer mode, so this technique is Android-only. On iOS, the best you can do is schedule a local notification reminding the user to silence the phone themselves.
  • On Android 6.0 and above, setRingerMode to silent or vibrate without DND access is ignored by the system. There is no workaround; the user must grant access.
  • You cannot read or change another app's notification policy, only the device-wide ringer mode through AudioManager.

Wrapping Up

Controlling the ringer from Flutter is a small feature with an outsized "it just works" feeling when done right: a MethodChannel to AudioManager, the manifest permission, a DND access check with a clear explanation, and a restore step so the phone always returns to how the user left it. That combination is what powers Smart Silent's scheduled and location-based profiles in production.

Building something that needs to respect the user's context — silent during meetings, loud when it matters? Get in touch and let's talk about your project.

Frequently asked questions

Can Flutter change the ringer mode without a MethodChannel?
No. Flutter has no built-in API for ringer mode, so you must bridge to Android's AudioManager through a MethodChannel with get and set methods.
Why is my setRingerMode call ignored on the device?
Since Android 6.0, switching to silent or vibrate requires notification policy access. Open the Do Not Disturb access settings screen, confirm the grant, and re-check it before applying the profile.
Does this approach work on iOS?
No. iOS offers no public API for apps to change the ringer mode, so profile switching like this is Android-only.
How is this used in the Smart Silent app?
Smart Silent applies silent profiles on a schedule or by location and automatically restores the previous profile afterwards, using OS-level controls with a reported battery impact under 1.2 percent per 24 hours.

Written by Muhammad Uzair

Flutter Developer & Mobile Engineering Team Lead.

Work with me
All articles
  • Mobile Development

    9 min read

    Flutter Exact Alarm Scheduling on Android

    Build reliable alarms in Flutter on modern Android: exact alarm permissions, Doze-safe scheduling, reboot recovery, and fallbacks from a real alarm app.

  • Mobile Development

    8 min read

    Building an Ethiopian Calendar in Flutter

    Build an Ethiopian calendar in Flutter with bidirectional date conversion, Orthodox fasting data, and a fully offline design based on a real published app.

  • Comparisons

    6 min read

    Flutter vs SwiftUI: Cross-Platform vs Native iOS

    Flutter vs SwiftUI compared across performance, ecosystem, hiring, and native feel — the honest 2026 guide to picking the right framework for your iOS project.