From baa874aab45601358bf2746d7fa4b765e4e0dcb2 Mon Sep 17 00:00:00 2001 From: rakshitjain23 Date: Sat, 29 Mar 2025 12:21:45 +0530 Subject: [PATCH] Add system ringtones feature to alarm clock --- .../ultimate_alarm_clock/MainActivity.kt | 114 ++++++++- .../add_or_update_alarm_controller.dart | 94 +++++-- .../views/choose_ringtone_tile.dart | 239 ++++++++++++++++-- lib/app/utils/audio_utils.dart | 84 +++++- 4 files changed, 477 insertions(+), 54 deletions(-) diff --git a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/MainActivity.kt b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/MainActivity.kt index 4aa789b3..632b9740 100644 --- a/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/MainActivity.kt +++ b/android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/MainActivity.kt @@ -42,6 +42,105 @@ class MainActivity : FlutterActivity() { context.registerReceiver(TimerNotification(), intentFilter, Context.RECEIVER_EXPORTED) } + private fun getSystemRingtones(): List> { + val ringtoneList = mutableListOf>() + + try { + // Get alarm sounds + var manager = RingtoneManager(this) + manager.setType(RingtoneManager.TYPE_ALARM) + + var cursor = manager.cursor + Log.d("Ringtones", "Found ${cursor.count} alarm sounds") + + while (cursor.moveToNext()) { + try { + val title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX) + val uri = manager.getRingtoneUri(cursor.position).toString() + + ringtoneList.add(mapOf( + "title" to "⏰ $title", + "uri" to uri + )) + } catch (e: Exception) { + Log.e("Ringtones", "Error reading alarm sound", e) + } + } + + // Get notification sounds + manager = RingtoneManager(this) + manager.setType(RingtoneManager.TYPE_NOTIFICATION) + + cursor = manager.cursor + Log.d("Ringtones", "Found ${cursor.count} notification sounds") + + while (cursor.moveToNext()) { + try { + val title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX) + val uri = manager.getRingtoneUri(cursor.position).toString() + + ringtoneList.add(mapOf( + "title" to "🔔 $title", + "uri" to uri + )) + } catch (e: Exception) { + Log.e("Ringtones", "Error reading notification sound", e) + } + } + + // Get ringtones + manager = RingtoneManager(this) + manager.setType(RingtoneManager.TYPE_RINGTONE) + + cursor = manager.cursor + Log.d("Ringtones", "Found ${cursor.count} ringtones") + + while (cursor.moveToNext()) { + try { + val title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX) + val uri = manager.getRingtoneUri(cursor.position).toString() + + ringtoneList.add(mapOf( + "title" to "📱 $title", + "uri" to uri + )) + } catch (e: Exception) { + Log.e("Ringtones", "Error reading ringtone", e) + } + } + + // Also add default alarm sounds + try { + val defaultUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM) + val defaultRingtone = RingtoneManager.getRingtone(this, defaultUri) + val title = defaultRingtone.getTitle(this) + ringtoneList.add(mapOf( + "title" to "⏰ Default: $title", + "uri" to defaultUri.toString() + )) + } catch (e: Exception) { + Log.e("Ringtones", "Error getting default alarm", e) + } + + Log.d("Ringtones", "Total ringtones found: ${ringtoneList.size}") + } catch (e: Exception) { + Log.e("Ringtones", "Error retrieving system ringtones", e) + } + + return ringtoneList + } + + private fun playSystemRingtone(uriString: String) { + stopSystemRingtone() + val uri = Uri.parse(uriString) + ringtone = RingtoneManager.getRingtone(applicationContext, uri) + ringtone?.play() + } + + private fun stopSystemRingtone() { + ringtone?.stop() + ringtone = null + } override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) @@ -135,6 +234,20 @@ class MainActivity : FlutterActivity() { } else if (call.method == "stopDefaultAlarm") { stopDefaultAlarm() result.success(null) + } else if (call.method == "getSystemRingtones") { + val ringtones = getSystemRingtones() + result.success(ringtones) + } else if (call.method == "playSystemRingtone") { + val uri = call.argument("uri") + if (uri != null) { + playSystemRingtone(uri) + result.success(null) + } else { + result.error("INVALID_ARGUMENT", "URI cannot be null", null) + } + } else if (call.method == "stopSystemRingtone") { + stopSystemRingtone() + result.success(null) } else { result.notImplemented() } @@ -293,5 +406,4 @@ class MainActivity : FlutterActivity() { intent.data = Uri.parse("package:${packageName}") startActivity(intent) } - } \ No newline at end of file diff --git a/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart b/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart index 61473ead..767e375a 100644 --- a/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart +++ b/lib/app/modules/addOrUpdateAlarm/controllers/add_or_update_alarm_controller.dart @@ -113,6 +113,9 @@ class AddOrUpdateAlarmController extends GetxController { final RxInt hours = 0.obs, minutes = 0.obs, meridiemIndex = 0.obs; final List meridiem = ['AM'.obs, 'PM'.obs]; + RxList> systemRingtones = >[].obs; + RxBool isLoadingSystemRingtones = false.obs; + Future> fetchUserDetailsForSharedUsers() async { List userDetails = []; @@ -290,7 +293,7 @@ class AddOrUpdateAlarmController extends GetxController { Get.back(); }, style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(kprimaryColor), + backgroundColor: WidgetStateProperty.all(kprimaryColor), ), child: Text( 'Cancel'.tr, @@ -305,7 +308,7 @@ class AddOrUpdateAlarmController extends GetxController { Get.back(); }, style: OutlinedButton.styleFrom( - side: BorderSide( + side: const BorderSide( color: Colors.red, width: 1, ), @@ -465,7 +468,7 @@ class AddOrUpdateAlarmController extends GetxController { TextButton( style: ButtonStyle( backgroundColor: - MaterialStateProperty.all(kprimaryColor), + WidgetStateProperty.all(kprimaryColor), ), child: Text( 'Save', @@ -485,7 +488,7 @@ class AddOrUpdateAlarmController extends GetxController { TextButton( style: ButtonStyle( backgroundColor: - MaterialStateProperty.all(kprimaryColor), + WidgetStateProperty.all(kprimaryColor), ), child: Text( 'Retake', @@ -505,7 +508,7 @@ class AddOrUpdateAlarmController extends GetxController { TextButton( style: ButtonStyle( backgroundColor: - MaterialStateProperty.all(kprimaryColor), + WidgetStateProperty.all(kprimaryColor), ), child: Text( 'Disable', @@ -572,7 +575,7 @@ class AddOrUpdateAlarmController extends GetxController { }, confirm: TextButton( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all(kprimaryColor), + backgroundColor: WidgetStateProperty.all(kprimaryColor), ), child: Obx( () => Text( @@ -595,7 +598,7 @@ class AddOrUpdateAlarmController extends GetxController { cancel: Obx( () => TextButton( style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( themeController.primaryTextColor.value.withOpacity(0.5), ), ), @@ -669,7 +672,7 @@ class AddOrUpdateAlarmController extends GetxController { profileTextEditingController.text = homeController.isProfileUpdate.value ? homeController.selectedProfile.value - : ""; + : ''; emailTextEditingController.text = ''; if (Get.arguments != null) { @@ -1243,15 +1246,15 @@ class AddOrUpdateAlarmController extends GetxController { return Theme( data: themeController.currentTheme.value == ThemeMode.light ? ThemeData.light().copyWith( - colorScheme: const ColorScheme.light( - primary: kprimaryColor, - ), - ) + colorScheme: const ColorScheme.light( + primary: kprimaryColor, + ), + ) : ThemeData.dark().copyWith( - colorScheme: const ColorScheme.dark( - primary: kprimaryColor, - ), - ), + colorScheme: const ColorScheme.dark( + primary: kprimaryColor, + ), + ), child: child!, ); }, @@ -1357,12 +1360,59 @@ class AddOrUpdateAlarmController extends GetxController { storage.writeProfile(profileModel.profileName); homeController.writeProfileName(profileModel.profileName); } -} - int orderedCountryCode(Country countryA, Country countryB) { - // `??` for null safety of 'dialCode' - String dialCodeA = countryA.dialCode ?? '0'; - String dialCodeB = countryB.dialCode ?? '0'; + Future loadSystemRingtones() async { + isLoadingSystemRingtones.value = true; + + // No need for storage permission to access system ringtones + systemRingtones.value = await AudioUtils.getSystemRingtones(); - return int.parse(dialCodeA).compareTo(int.parse(dialCodeB)); + // Debug logging + debugPrint('Loaded ${systemRingtones.length} system ringtones'); + if (systemRingtones.isEmpty) { + debugPrint('No system ringtones found'); + } else { + debugPrint('First ringtone: ${systemRingtones[0]}'); + } + + isLoadingSystemRingtones.value = false; } + + Future saveSystemRingtone(String title, String uri) async { + try { + // Create a ringtone model for the system ringtone + RingtoneModel systemRingtone = RingtoneModel( + ringtoneName: title, + ringtonePath: uri, + currentCounterOfUsage: 1, + ); + + // Update previous ringtone counter + AudioUtils.updateRingtoneCounterOfUsage( + customRingtoneName: previousRingtone, + counterUpdate: CounterUpdate.decrement, + ); + + // Save the ringtone to the database + await IsarDb.addCustomRingtone(systemRingtone); + + // Update the current ringtone name + customRingtoneName.value = title; + + // Add to the list if not already present + if (!customRingtoneNames.contains(title)) { + customRingtoneNames.add(title); + } + } catch (e) { + debugPrint(e.toString()); + } + } +} + +int orderedCountryCode(Country countryA, Country countryB) { + // `??` for null safety of 'dialCode' + String dialCodeA = countryA.dialCode ?? '0'; + String dialCodeB = countryB.dialCode ?? '0'; + + return int.parse(dialCodeA).compareTo(int.parse(dialCodeB)); +} diff --git a/lib/app/modules/addOrUpdateAlarm/views/choose_ringtone_tile.dart b/lib/app/modules/addOrUpdateAlarm/views/choose_ringtone_tile.dart index b50a368e..fcdfefa8 100644 --- a/lib/app/modules/addOrUpdateAlarm/views/choose_ringtone_tile.dart +++ b/lib/app/modules/addOrUpdateAlarm/views/choose_ringtone_tile.dart @@ -63,7 +63,7 @@ class ChooseRingtoneTile extends StatelessWidget { children: [ Obx( () => Padding( - padding: EdgeInsets.all(4), + padding: const EdgeInsets.all(4), child: SizedBox( width: width * 0.8, height: height * 0.3, @@ -71,10 +71,10 @@ class ChooseRingtoneTile extends StatelessWidget { elevation: 0, color: themeController.secondaryBackgroundColor.value, child: Scrollbar( - radius: Radius.circular(5), + radius: const Radius.circular(5), thumbVisibility: true, child: Padding( - padding: EdgeInsets.only(right: 4), + padding: const EdgeInsets.only(right: 4), child: ListView.builder( itemCount: controller.customRingtoneNames.length, @@ -83,7 +83,8 @@ class ChooseRingtoneTile extends StatelessWidget { return Obx( () => ListTile( onTap: () async { - await AudioUtils.stopPreviewCustomSound(); + await AudioUtils + .stopPreviewCustomSound(); controller.isPlaying.value = false; controller.previousRingtone = controller.customRingtoneName.value; @@ -116,8 +117,10 @@ class ChooseRingtoneTile extends StatelessWidget { .customRingtoneName == controller .customRingtoneNames[index] - ? themeController.primaryBackgroundColor.value - : themeController.secondaryBackgroundColor.value, + ? themeController + .primaryBackgroundColor.value + : themeController + .secondaryBackgroundColor.value, title: Text( controller.customRingtoneNames[index], overflow: TextOverflow.ellipsis, @@ -131,9 +134,10 @@ class ChooseRingtoneTile extends StatelessWidget { .customRingtoneNames[index]) IconButton( onPressed: () => onTapPreview( - controller - .customRingtoneNames[ - index]), + controller + .customRingtoneNames[ + index], + ), icon: Icon( (controller.isPlaying.value && controller @@ -156,13 +160,15 @@ class ChooseRingtoneTile extends StatelessWidget { 255, 116, 111, - 110) // Change this color to red + 110, + ) // Change this color to red : kprimaryColor, ), ), if (!defaultRingtones.contains( - controller.customRingtoneNames[ - index])) + controller + .customRingtoneNames[index], + )) IconButton( onPressed: () async { await controller @@ -197,20 +203,200 @@ class ChooseRingtoneTile extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - OutlinedButton( - onPressed: () async { - Utils.hapticFeedback(); - await AudioUtils.stopPreviewCustomSound(); - controller.isPlaying.value = false; - controller.previousRingtone = - controller.customRingtoneName.value; - await controller.saveCustomRingtone(); + PopupMenuButton( + onSelected: (String value) async { + if (value == 'upload') { + Utils.hapticFeedback(); + await AudioUtils.stopPreviewCustomSound(); + controller.isPlaying.value = false; + controller.previousRingtone = + controller.customRingtoneName.value; + await controller.saveCustomRingtone(); + } else if (value == 'system') { + Utils.hapticFeedback(); + await AudioUtils.stopPreviewCustomSound(); + controller.isPlaying.value = false; + + // Load system ringtones + await controller.loadSystemRingtones(); + + // Show system ringtones dialog + Get.dialog( + Dialog( + backgroundColor: themeController + .secondaryBackgroundColor.value, + child: Container( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context) + .size + .height * + 0.7, // Limit to 70% of screen height + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'System Ringtones'.tr, + style: Theme.of(context) + .textTheme + .displaySmall, + ), + const SizedBox(height: 16), + Expanded( + child: Obx(() { + if (controller + .isLoadingSystemRingtones + .value) { + return const Center( + child: + CircularProgressIndicator(),); + } + + if (controller + .systemRingtones.isEmpty) { + return Center( + child: Text( + 'No system ringtones found' + .tr, + textAlign: TextAlign.center, + ), + ); + } + + return ListView.builder( + shrinkWrap: true, + itemCount: controller + .systemRingtones.length, + itemBuilder: (context, index) { + final ringtone = controller + .systemRingtones[index]; + final title = + ringtone['title'] as String; + final uri = + ringtone['uri'] as String; + + return ListTile( + title: Text( + title, + overflow: + TextOverflow.ellipsis, + ), + onTap: () async { + // Stop any playing preview + await AudioUtils + .stopPreviewCustomSound(); + await AudioUtils + .stopSystemRingtone(); + + // Save the selected system ringtone + await controller + .saveSystemRingtone( + title, uri,); + Get.back(); // Close system ringtones dialog + }, + trailing: IconButton( + icon: const Icon( + Icons.play_arrow,), + onPressed: () async { + // Preview the ringtone + await AudioUtils + .stopPreviewCustomSound(); + await AudioUtils + .stopSystemRingtone(); + await AudioUtils + .playSystemRingtone( + uri,); + }, + ), + ); + }, + ); + }), + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + AudioUtils.stopSystemRingtone(); + Get.back(); + }, + style: ElevatedButton.styleFrom( + backgroundColor: kprimaryColor, + ), + child: Text('Cancel'.tr), + ), + ], + ), + ), + ), + ), + ); + } }, - child: Text( - 'Upload Ringtone'.tr, - style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: kprimaryColor, + itemBuilder: (BuildContext context) => + >[ + PopupMenuItem( + value: 'upload', + child: Row( + children: [ + const Icon(Icons.upload_file, + size: 20, color: kprimaryColor,), + const SizedBox(width: 10), + Text( + 'Upload Ringtone'.tr, + style: TextStyle( + color: + themeController.primaryTextColor.value, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + PopupMenuItem( + value: 'system', + child: Row( + children: [ + const Icon(Icons.phone_android, + size: 20, color: kprimaryColor,), + const SizedBox(width: 10), + Text( + 'System Ringtones'.tr, + style: TextStyle( + color: + themeController.primaryTextColor.value, + fontWeight: FontWeight.w500, ), + ), + ], + ), + ), + ], + color: themeController.secondaryBackgroundColor.value, + elevation: 8, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: OutlinedButton( + onPressed: null, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.add, + size: 16, color: kprimaryColor,), + const SizedBox(width: 5), + Text( + 'Add Ringtone'.tr, + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith( + color: kprimaryColor, + ), + ), + ], + ), ), ), ElevatedButton( @@ -225,7 +411,10 @@ class ChooseRingtoneTile extends StatelessWidget { ), child: Text( 'Done'.tr, - style: Theme.of(context).textTheme.displaySmall!.copyWith( + style: Theme.of(context) + .textTheme + .displaySmall! + .copyWith( color: themeController.secondaryTextColor.value, ), ), diff --git a/lib/app/utils/audio_utils.dart b/lib/app/utils/audio_utils.dart index 86e62ca2..7807874d 100644 --- a/lib/app/utils/audio_utils.dart +++ b/lib/app/utils/audio_utils.dart @@ -48,7 +48,7 @@ class AudioUtils { ) async { try { var volume = await FlutterVolumeController.getVolume(); - await audioPlayer.setVolume(volume??1.0); + await audioPlayer.setVolume(volume ?? 1.0); await audioPlayer.setReleaseMode(audioplayer.ReleaseMode.loop); await audioPlayer.play(audioplayer.DeviceFileSource(customRingtonePath)); } catch (e) { @@ -61,7 +61,7 @@ class AudioUtils { ) async { try { var volume = await FlutterVolumeController.getVolume(); - await audioPlayer.setVolume(volume??1.0); + await audioPlayer.setVolume(volume ?? 1.0); await audioPlayer.setReleaseMode(audioplayer.ReleaseMode.loop); await audioPlayer.play(audioplayer.AssetSource(customRingtonePath)); } catch (e) { @@ -77,7 +77,7 @@ class AudioUtils { await initializeAudioSession(); } var volume = await FlutterVolumeController.getVolume(); - await audioPlayer.setVolume(volume??1.0); + await audioPlayer.setVolume(volume ?? 1.0); await audioSession!.setActive(true); String ringtoneName = alarmRecord.ringtoneName; @@ -91,6 +91,9 @@ class AudioUtils { String customRingtonePath = customRingtone.ringtonePath; if (defaultRingtones.contains(ringtoneName)) { await playAssetSound(customRingtonePath); + } else if (customRingtonePath.startsWith('content://')) { + await alarmChannel + .invokeMethod('playSystemRingtone', {'uri': customRingtonePath}); } else { await playCustomSound(customRingtonePath); } @@ -133,7 +136,14 @@ class AudioUtils { if (customRingtone != null) { String customRingtonePath = customRingtone.ringtonePath; - await playCustomSound(customRingtonePath); + if (customRingtonePath.startsWith('content://')) { + await alarmChannel.invokeMethod( + 'playSystemRingtone', + {'uri': customRingtonePath}, + ); + } else { + await playCustomSound(customRingtonePath); + } } else { await timerChannel.invokeMethod('playDefaultAlarm'); @@ -189,7 +199,14 @@ class AudioUtils { await alarmChannel.invokeMethod('stopDefaultAlarm'); await audioSession!.setActive(false); await audioSession!.setActive(true); - await playAssetSound(customRingtonePath); + if (customRingtonePath.startsWith('content://')) { + await alarmChannel.invokeMethod( + 'playSystemRingtone', + {'uri': customRingtonePath}, + ); + } else { + await playCustomSound(customRingtonePath); + } isPreviewing = true; } } else { @@ -203,7 +220,14 @@ class AudioUtils { await alarmChannel.invokeMethod('stopDefaultAlarm'); await audioSession!.setActive(false); await audioSession!.setActive(true); - await playCustomSound(customRingtonePath); + if (customRingtonePath.startsWith('content://')) { + await alarmChannel.invokeMethod( + 'playSystemRingtone', + {'uri': customRingtonePath}, + ); + } else { + await playCustomSound(customRingtonePath); + } isPreviewing = true; } } @@ -217,6 +241,7 @@ class AudioUtils { if (audioSession != null && isPreviewing) { await audioPlayer.stop(); await alarmChannel.invokeMethod('stopDefaultAlarm'); + await alarmChannel.invokeMethod('stopSystemRingtone'); await audioSession!.setActive(false); isPreviewing = false; } @@ -298,4 +323,51 @@ class AudioUtils { return hash; } + + static Future>> getSystemRingtones() async { + try { + final List ringtones = + await alarmChannel.invokeMethod('getSystemRingtones'); + + // Explicit conversion to avoid type errors + List> result = []; + for (var item in ringtones) { + // Safely convert each map to the expected type + if (item is Map) { + Map ringtone = { + 'title': item['title']?.toString() ?? '', + 'uri': item['uri']?.toString() ?? '', + }; + result.add(ringtone); + } + } + + debugPrint('Processed ${result.length} system ringtones'); + return result; + } on PlatformException catch (e) { + debugPrint('Failed to get system ringtones: ${e.message}'); + return []; + } catch (e) { + debugPrint('Error processing system ringtones: $e'); + return []; + } + } + + static Future playSystemRingtone(String uri) async { + try { + await alarmChannel.invokeMethod('playSystemRingtone', {'uri': uri}); + isPreviewing = true; + } on PlatformException catch (e) { + debugPrint('Failed to play system ringtone: ${e.message}'); + } + } + + static Future stopSystemRingtone() async { + try { + await alarmChannel.invokeMethod('stopSystemRingtone'); + isPreviewing = false; + } on PlatformException catch (e) { + debugPrint('Failed to stop system ringtone: ${e.message}'); + } + } }