jbh_ringtone
A Flutter plugin that provides easy access to device ringtones with advanced features. Currently in active development - this plugin allows you to retrieve the list of available ringtones with their IDs, titles, URIs, types, and even play them directly.
๐ง Current Status
This plugin is actively being developed and currently supports:
- โ Android: Full support for retrieving ringtones with different types, playing audio, and detailed information
- ๐ง iOS: Coming soon (in development)
- ๐ Future plans: Enhanced audio controls, selection features, and cross-platform functionality
Features
Currently Available
- ๐ฑ Android support: Works on Android devices (API level 21+)
- ๐ต Ringtone listing: Retrieves available ringtones with comprehensive information
- ๐ง Audio playback: Play and stop ringtones directly from the plugin
- ๐ฏ Type-specific methods: Get ringtones by type (ringtone, notification, alarm)
- ๐ Flexible filtering: Custom filter options for multiple ringtone types
- ๐ฆ Enhanced models: Rich
JbhRingtoneModelwith file size, duration, and metadata - โก Performance optimized: Fast loading with lazy loading for detailed information
- ๐ก๏ธ Error handling: Robust error handling with meaningful error messages
- ๐ฆ No dependencies: Lightweight with minimal external dependencies
- ๐ Real-time info: Get live information about currently playing ringtones
Coming Soon
- ๐ iOS support: Full cross-platform compatibility
- ๐ฏ Enhanced selection: Better ringtone management features
- ๐ Improved titles: Better ringtone name resolution
- ๐๏ธ Volume control: Adjust playback volume
- โฑ๏ธ Playback controls: Pause, resume, and seek functionality
Getting Started
Installation
Add jbh_ringtone to your pubspec.yaml file:
dependencies:
jbh_ringtone: ^1.0.3
Then run:
flutter pub get
Platform Setup
Android
No additional setup required! The plugin automatically handles all necessary permissions and configurations.
iOS
Note: iOS support is currently in development and will be available in future updates.
Usage
Basic Usage with Enhanced API
import 'package:jbh_ringtone/jbh_ringtone.dart';
void main() async {
// Create an instance of JbhRingtone
JbhRingtone ringtone = JbhRingtone();
try {
// Get all ringtones (enhanced typed API)
List<JbhRingtoneModel> allRingtones = await ringtone.getRingtones();
print('Found ${allRingtones.length} ringtones');
// Display each ringtone with enhanced information
for (var ringtone in allRingtones) {
print('Title: ${ringtone.title}');
print('Display Title: ${ringtone.displayTitle}');
print('File Name: ${ringtone.fileName}');
print('ID: ${ringtone.id}');
print('URI: ${ringtone.uri}');
print('Type: ${ringtone.type.displayName}');
print('Duration: ${ringtone.formattedDuration}');
print('File Size: ${ringtone.formattedFileSize}');
print('Is Default: ${ringtone.isDefault}');
print('---');
}
} catch (e) {
print('Error: $e');
}
}
Audio Playback
// Play a ringtone
await ringtone.playRingtone(ringtoneModel.uri);
// Stop currently playing ringtone
await ringtone.stopRingtone();
// Get information about a specific ringtone
Map<String, dynamic> info = await ringtone.getRingtoneInfo(ringtoneModel.uri);
print('Is Playing: ${info['isPlaying']}');
print('Duration: ${info['duration']}ms');
Enhanced Ringtone Information
// Get detailed information about a specific ringtone
Map<String, dynamic> details = await ringtone.getRingtoneDetails(ringtoneModel.uri);
print('Enhanced Title: ${details['title']}');
print('Display Title: ${details['displayTitle']}');
print('File Path: ${details['filePath']}');
print('File Size: ${details['fileSize']} bytes');
print('Duration: ${details['duration']}ms');
print('Is Default: ${details['isDefault']}');
Type-Specific Methods
// Get only phone ringtones
List<JbhRingtoneModel> phoneRingtones = await ringtone.getRingtoneOnly();
// Get notification sounds
List<JbhRingtoneModel> notificationSounds = await ringtone.getNotificationRingtones();
// Get alarm sounds
List<JbhRingtoneModel> alarmSounds = await ringtone.getAlarmRingtones();
// Get all types
List<JbhRingtoneModel> allSounds = await ringtone.getAllRingtones();
Advanced Filtering
// Get ringtones with custom filter
List<JbhRingtoneModel> customRingtones = await ringtone.getRingtonesWithFilter(
includeRingtone: true,
includeNotification: true,
includeAlarm: false,
);
// Get by specific type
List<JbhRingtoneModel> ringtones = await ringtone.getRingtonesByType(RingtoneType.ringtone);
// Get by multiple types
List<JbhRingtoneModel> mixedRingtones = await ringtone.getRingtonesByTypes([
RingtoneType.ringtone,
RingtoneType.notification,
]);
Advanced Usage with UI and Audio Controls
import 'package:flutter/material.dart';
import 'package:jbh_ringtone/jbh_ringtone.dart';
class RingtoneListPage extends StatefulWidget {
@override
_RingtoneListPageState createState() => _RingtoneListPageState();
}
class _RingtoneListPageState extends State<RingtoneListPage> {
List<JbhRingtoneModel> _ringtones = [];
bool _isLoading = false;
String? _error;
String? _currentlyPlayingUri;
bool _isPlaying = false;
RingtoneType _selectedType = RingtoneType.ringtone;
@override
void initState() {
super.initState();
_loadRingtones();
}
Future<void> _loadRingtones() async {
setState(() {
_isLoading = true;
_error = null;
});
try {
final JbhRingtone ringtone = JbhRingtone();
final ringtones = await ringtone.getRingtonesByType(_selectedType);
setState(() {
_ringtones = ringtones;
_isLoading = false;
});
} catch (e) {
setState(() {
_error = e.toString();
_isLoading = false;
});
}
}
Future<void> _playRingtone(JbhRingtoneModel ringtone) async {
try {
final JbhRingtone jbhRingtone = JbhRingtone();
// Stop previous ringtone if playing
if (_isPlaying) {
await jbhRingtone.stopRingtone();
}
// Play new ringtone
await jbhRingtone.playRingtone(ringtone.uri);
setState(() {
_currentlyPlayingUri = ringtone.uri;
_isPlaying = true;
});
// Auto-stop after 5 seconds
Future.delayed(Duration(seconds: 5), () async {
if (_currentlyPlayingUri == ringtone.uri) {
await jbhRingtone.stopRingtone();
setState(() {
_currentlyPlayingUri = null;
_isPlaying = false;
});
}
});
} catch (e) {
print('Error playing ringtone: $e');
}
}
Future<void> _stopRingtone() async {
try {
final JbhRingtone jbhRingtone = JbhRingtone();
await jbhRingtone.stopRingtone();
setState(() {
_currentlyPlayingUri = null;
_isPlaying = false;
});
} catch (e) {
print('Error stopping ringtone: $e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Available Ringtones'),
actions: [
if (_isPlaying)
IconButton(
icon: Icon(Icons.stop),
onPressed: _stopRingtone,
tooltip: 'Stop Playing',
),
PopupMenuButton<RingtoneType>(
onSelected: (RingtoneType type) {
setState(() {
_selectedType = type;
});
_loadRingtones();
},
itemBuilder: (BuildContext context) => RingtoneType.values.map((RingtoneType type) {
return PopupMenuItem<RingtoneType>(
value: type,
child: Text(type.displayName),
);
}).toList(),
),
],
),
body: _isLoading
? Center(child: CircularProgressIndicator())
: _error != null
? Center(child: Text('Error: $_error'))
: ListView.builder(
itemCount: _ringtones.length,
itemBuilder: (context, index) {
final ringtone = _ringtones[index];
final isCurrentlyPlaying = _currentlyPlayingUri == ringtone.uri;
return ListTile(
leading: CircleAvatar(
backgroundColor: isCurrentlyPlaying ? Colors.green : Colors.blue,
child: Icon(
isCurrentlyPlaying ? Icons.play_arrow : Icons.music_note,
color: Colors.white,
),
),
title: Text(
ringtone.displayTitle,
style: TextStyle(
fontWeight: isCurrentlyPlaying ? FontWeight.bold : FontWeight.normal,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Duration: ${ringtone.formattedDuration}'),
Text('Size: ${ringtone.formattedFileSize}'),
if (ringtone.isDefault)
Text('Default', style: TextStyle(color: Colors.amber)),
],
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: Icon(
isCurrentlyPlaying ? Icons.stop : Icons.play_arrow,
color: isCurrentlyPlaying ? Colors.red : Colors.green,
),
onPressed: () {
if (isCurrentlyPlaying) {
_stopRingtone();
} else {
_playRingtone(ringtone);
}
},
),
],
),
);
},
),
);
}
}
API Reference
JbhRingtone Class
The main class for interacting with device ringtones.
Methods
getRingtones() (Legacy)
Retrieves all available ringtones from the device (legacy method for backward compatibility).
Returns: Future<List<JbhRingtoneModel>>
getRingtonesByType(RingtoneType type)
Retrieves ringtones of a specific type.
Parameters:
type: The type of ringtones to retrieve (RingtoneType.ringtone, RingtoneType.notification, RingtoneType.alarm, RingtoneType.all)
Returns: Future<List<JbhRingtoneModel>>
getRingtonesByTypes(List<RingtoneType> types)
Retrieves ringtones of multiple types.
Parameters:
types: List of ringtone types to retrieve
Returns: Future<List<JbhRingtoneModel>>
getRingtonesWithFilter({bool includeRingtone, bool includeNotification, bool includeAlarm})
Retrieves ringtones with custom filter options.
Parameters:
includeRingtone: Whether to include phone ringtones (default: false)includeNotification: Whether to include notification sounds (default: false)includeAlarm: Whether to include alarm sounds (default: false)
Returns: Future<List<JbhRingtoneModel>>
playRingtone(String uri)
Plays a ringtone by its URI.
Parameters:
uri: The URI of the ringtone to play
Returns: Future<String> - Success message
stopRingtone()
Stops the currently playing ringtone.
Returns: Future<String> - Success message
getRingtoneInfo(String uri)
Gets basic information about a specific ringtone.
Parameters:
uri: The URI of the ringtone
Returns: Future<Map<String, dynamic>> - Basic ringtone information
getRingtoneDetails(String uri)
Gets detailed information about a specific ringtone (enhanced version).
Parameters:
uri: The URI of the ringtone
Returns: Future<Map<String, dynamic>> - Detailed ringtone information
Convenience Methods
getRingtoneOnly()
Gets only phone ringtones.
Returns: Future<List<JbhRingtoneModel>>
getNotificationRingtones()
Gets notification sounds.
Returns: Future<List<JbhRingtoneModel>>
getAlarmRingtones()
Gets alarm sounds.
Returns: Future<List<JbhRingtoneModel>>
getAllRingtones()
Gets all types of sounds.
Returns: Future<List<JbhRingtoneModel>>
getPlatformVersion()
Gets the current platform version.
Returns: Future<String?>
JbhRingtoneModel Class
Represents a single ringtone with comprehensive properties.
Properties
| Property | Type | Description |
|---|---|---|
id |
int |
Unique identifier for the ringtone |
title |
String |
Human-readable name of the ringtone |
displayTitle |
String |
User-friendly display title (shortened if needed) |
fileName |
String |
Original file name of the ringtone |
uri |
String |
Content URI for accessing the ringtone file |
uriType |
String |
Type of URI (content, file, unknown) |
filePath |
String? |
File path on device (null for lazy loading) |
fileSize |
int |
File size in bytes (0 for lazy loading) |
duration |
int |
Duration in milliseconds (0 for lazy loading) |
type |
RingtoneType |
Type of the ringtone (ringtone, notification, alarm, all) |
isDefault |
bool |
Whether this is a default ringtone (false for lazy loading) |
Computed Properties
| Property | Type | Description |
|---|---|---|
formattedDuration |
String |
Duration formatted as "M:SS" |
formattedFileSize |
String |
File size formatted as "X.X MB/KB" |
Methods
fromMap(Map<String, dynamic> map)
Creates a JbhRingtoneModel from a map.
toMap()
Converts the model to a map.
toString()
Returns a string representation with key information.
operator ==(Object other)
Compares two JbhRingtoneModel instances for equality.
hashCode
Returns the hash code for the model.
RingtoneType Enum
Enum representing different types of ringtones.
Values
| Value | Display Name | Description |
|---|---|---|
RingtoneType.ringtone |
"Ringtone" | Phone ringtone sounds |
RingtoneType.notification |
"Notification" | Notification sounds |
RingtoneType.alarm |
"Alarm" | Alarm sounds |
RingtoneType.all |
"All" | All types of sounds |
Properties
| Property | Type | Description |
|---|---|---|
value |
int |
Integer value used by Android RingtoneManager |
displayName |
String |
Human-readable name |
Error Handling
The plugin provides comprehensive error handling:
try {
List<JbhRingtoneModel> ringtones = await ringtone.getRingtonesByType(RingtoneType.ringtone);
// Handle success
} catch (e) {
if (e.toString().contains('RINGTONE_ERROR')) {
// Handle ringtone-specific errors
print('Failed to access ringtones: $e');
} else if (e.toString().contains('PLAY_ERROR')) {
// Handle playback errors
print('Failed to play ringtone: $e');
} else if (e.toString().contains('STOP_ERROR')) {
// Handle stop errors
print('Failed to stop ringtone: $e');
} else {
// Handle general errors
print('Unexpected error: $e');
}
}
Performance Optimizations
The plugin includes several performance optimizations:
Lazy Loading
- Fast initial loading: Basic information loads quickly
- Detailed info on demand: File size, duration, and metadata loaded only when needed
- Efficient memory usage: Minimal memory footprint
Caching
- Ringtone caching: Frequently accessed ringtones are cached
- Title optimization: Smart title resolution for better performance
Batch Operations
- Efficient queries: Optimized database queries for faster results
- Limited results: Configurable limits to prevent memory issues
Platform Support
| Platform | Support | Notes |
|---|---|---|
| Android | โ Full | Requires Android API level 21+, includes audio playback |
| iOS | ๐ง In Development | Coming in future updates |
| Web | โ Not supported | Platform-specific functionality |
| Desktop | โ Not supported | Platform-specific functionality |
Permissions
Android
The plugin automatically requests the following permissions:
READ_EXTERNAL_STORAGE- Required to access ringtone files- Audio playback permissions are handled automatically
iOS
Note: iOS permissions will be documented when iOS support is released.
Roadmap
Version 1.2 (Next Release)
- ๐ iOS support
- ๐๏ธ Volume control
- โฑ๏ธ Playback controls (pause, resume, seek)
- ๐ฑ Better UI components
Version 1.3
- ๐ฏ Enhanced selection features
- ๐ Ringtone management (set as default, etc.)
- ๐ Improved ringtone title resolution
- ๐งช Comprehensive testing suite
Future Versions
- ๐จ Custom UI widgets
- ๐ง Advanced configuration options
- ๐ Usage analytics
- ๐ Cross-platform synchronization
Example App
Check out the example app for a complete implementation showing how to:
- Display a list of available ringtones with enhanced information
- Handle loading states and error handling
- Implement audio playback controls
- Create a user-friendly interface with play/stop functionality
- Use type-specific filtering
- Show detailed ringtone information
- Handle real-time playback status
Contributing
Contributions are welcome! This is an actively developed plugin, and we'd love your help. Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
Development Setup
- Fork the repository
- Create a feature branch:
git checkout -b feature/amazing-feature - Commit your changes:
git commit -m 'Add amazing feature' - Push to the branch:
git push origin feature/amazing-feature - Open a Pull Request
Testing
Run the tests using:
flutter test
License
This project is licensed under the MIT License - see the LICENSE file for details.
Changelog
See CHANGELOG.md for a list of changes and version history.
Support
If you encounter any issues or have questions, please:
- Check the example app for usage examples
- Search existing issues
- Create a new issue with detailed information about your problem
Acknowledgments
- Flutter team for the excellent plugin development framework
- Android and iOS communities for platform-specific guidance
- Contributors and users for feedback and suggestions
Made with โค๏ธ for the Flutter community
This plugin is actively maintained and developed. Stay tuned for regular updates and new features!