Call State Handler
A Flutter plugin that detects active phone calls on Android and iOS, and VoIP/video calls (Google Meet, Zoom, WhatsApp, Teams, ...) on Android. Use it to pause or block parts of your app while the user is on a call.
Features
- Detects when a phone call starts and ends (Android and iOS)
- Detects VoIP/video calls on Android
- No CallKit on iOS, so apps using this plugin can be distributed in the China mainland App Store
- No permissions required
- Does not modify your app's audio session
- Stream-based API that emits the current state to every new listener
Platform support
| Phone calls | VoIP / video calls | |
|---|---|---|
| Android | ✅ | ✅ |
| iOS | ✅ | ❌ (see below) |
How It Works
Android
The plugin checks the system audio mode (AudioManager.getMode()) once per second:
MODE_IN_CALL/MODE_RINGTONE(and call screening/redirect modes) →CallType.phoneCallMODE_IN_COMMUNICATION→CallType.videoCall, reported only after the mode has been stable for about 3 seconds
Some non-call features also switch the device into MODE_IN_COMMUNICATION, for example voice-message recorders, voice chat in games and some Bluetooth headsets. If your app records audio itself, turn VoIP detection off while recording (see Recording audio in your app).
iOS
Phone calls are detected with CoreTelephony's CTCallCenter.
iOS offers no reliable way to detect VoIP/video calls from other apps without CallKit. Apps that use CallKit are not allowed in the China mainland App Store, so this plugin does not use it and reports only regular phone calls on iOS. setVoipDetectionEnabled does nothing on iOS.
Usage
Basic Implementation
import 'package:call_state_handler/call_state_handler.dart';
import 'package:flutter/material.dart';
class CallMonitorExample extends StatefulWidget {
const CallMonitorExample({super.key});
@override
State<CallMonitorExample> createState() => _CallMonitorExampleState();
}
class _CallMonitorExampleState extends State<CallMonitorExample> {
final CallStateHandler _callStateHandler = CallStateHandler();
@override
void initState() {
super.initState();
_callStateHandler.initialize();
}
@override
void dispose() {
_callStateHandler.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Call Monitor Example')),
body: StreamBuilder<CallState>(
stream: _callStateHandler.onCallStateChanged,
initialData: _callStateHandler.currentState,
builder: (context, snapshot) {
final callState = snapshot.data!;
return Center(
child: Text(
callState.isCallActive
? 'Call Active: ${callState.callType == CallType.phoneCall ? "Phone Call" : "Video Call"}'
: 'No Active Call',
style: Theme.of(context).textTheme.titleLarge,
),
);
},
),
);
}
}
With BLoC/Cubit
import 'dart:async';
import 'package:call_state_handler/call_state_handler.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CallMonitorCubit extends Cubit<CallState> {
final CallStateHandler _callStateHandler = CallStateHandler();
StreamSubscription<CallState>? _subscription;
CallMonitorCubit() : super(const CallState.initial()) {
_subscription = _callStateHandler.onCallStateChanged.listen(emit);
_callStateHandler.initialize();
}
@override
Future<void> close() async {
await _subscription?.cancel();
await _callStateHandler.dispose();
return super.close();
}
}
Recording audio in your app
If your app records voice messages or otherwise uses the microphone, disable VoIP detection while recording. Otherwise the recording can be reported as a video call on Android:
await CallStateHandler().setVoipDetectionEnabled(false);
try {
await recorder.start(...);
// ...
await recorder.stop();
} finally {
// Always re-enable, even if recording fails; the flag is global.
await CallStateHandler().setVoipDetectionEnabled(true);
}
Phone calls are still detected while VoIP detection is disabled.
API notes
CallStateHandler()is a singleton, andinitialize()/dispose()are reference counted. Several widgets can use it at the same time, as long as each one balances itsinitialize()with onedispose(). Monitoring stops only when the last one disposes.onCallStateChangedemits the current state immediately to each new listener, then every change.currentStatereturns the latest known state synchronously.- When the last
dispose()stops monitoring, an inactive state is emitted, so a call-blocking UI is never left stuck. The stream stays usable, and you can callinitialize()again later.
Migrating from 1.x
- iOS no longer uses CallKit and no longer reports
CallType.videoCall. - Android no longer inspects the foreground app. You can remove the
PACKAGE_USAGE_STATSandQUERY_ALL_PACKAGESpermissions if you added them only for this plugin. - The plugin no longer declares
READ_PHONE_STATE, andNSCallingCapabilityUsageDescriptionis no longer needed. CallStatenow has value equality and aconstconstructor.
Contributing
Contributions are welcome! If you find any issues or have suggestions for improvements:
Open an issue on GitHub Fork the repository Create a pull request with your changes
MIT License
Copyright (c) 2025 Abdusamad
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.