Skip to content

Commit 25bf4d1

Browse files
authored
Merge pull request #843 from mahendra-918/implement-systemRingtones
Implement system ringtones
2 parents 8660a1f + 8d54171 commit 25bf4d1

File tree

11 files changed

+1578
-248
lines changed

11 files changed

+1578
-248
lines changed

android/app/src/main/kotlin/com/ccextractor/ultimate_alarm_clock/ultimate_alarm_clock/MainActivity.kt

Lines changed: 263 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ import android.app.PendingIntent
88
import android.content.Context
99
import android.content.Intent
1010
import android.content.IntentFilter
11+
import android.database.Cursor
12+
import android.media.AudioAttributes
13+
import android.media.AudioFocusRequest
14+
import android.media.AudioManager
1115
import android.media.Ringtone
1216
import android.media.RingtoneManager
1317
import android.net.Uri
18+
import android.os.Build
1419
import android.os.Bundle
1520
import android.os.SystemClock
1621
import android.provider.Settings
@@ -26,12 +31,16 @@ class MainActivity : FlutterActivity() {
2631
companion object {
2732
const val CHANNEL1 = "ulticlock"
2833
const val CHANNEL2 = "timer"
34+
const val CHANNEL3 = "system_ringtones"
2935
const val ACTION_START_FLUTTER_APP = "com.ccextractor.ultimate_alarm_clock"
3036
const val EXTRA_KEY = "alarmRing"
3137
const val ALARM_TYPE = "isAlarm"
3238
private var isAlarm: String? = "true"
3339
val alarmConfig = hashMapOf("shouldAlarmRing" to false, "alarmIgnore" to false)
3440
private var ringtone: Ringtone? = null
41+
private var systemRingtone: Ringtone? = null
42+
private var audioManager: AudioManager? = null
43+
private var audioFocusRequest: AudioFocusRequest? = null
3544
}
3645

3746
override fun onCreate(savedInstanceState: Bundle?) {
@@ -40,14 +49,16 @@ class MainActivity : FlutterActivity() {
4049
intentFilter.addAction("com.ccextractor.ultimate_alarm_clock.START_TIMERNOTIF")
4150
intentFilter.addAction("com.ccextractor.ultimate_alarm_clock.STOP_TIMERNOTIF")
4251
context.registerReceiver(TimerNotification(), intentFilter, Context.RECEIVER_EXPORTED)
52+
53+
audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager
4354
}
4455

45-
4656
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
4757
super.configureFlutterEngine(flutterEngine)
4858
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
4959
var methodChannel1 = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL1)
5060
var methodChannel2 = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL2)
61+
var methodChannel3 = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL3)
5162

5263
val intent = intent
5364

@@ -71,6 +82,35 @@ class MainActivity : FlutterActivity() {
7182
methodChannel1.invokeMethod("appStartup", alarmConfig)
7283
alarmConfig["shouldAlarmRing"] = false
7384
}
85+
86+
methodChannel3.setMethodCallHandler { call, result ->
87+
when (call.method) {
88+
"getSystemRingtones" -> {
89+
val category = call.argument<String>("category") ?: "alarm"
90+
val ringtones = getSystemRingtones(category)
91+
result.success(ringtones)
92+
}
93+
"playSystemRingtone" -> {
94+
val ringtoneUri = call.argument<String>("ringtoneUri")
95+
if (ringtoneUri != null) {
96+
playSystemRingtone(ringtoneUri)
97+
result.success(null)
98+
} else {
99+
result.error("INVALID_ARGUMENT", "Ringtone URI is required", null)
100+
}
101+
}
102+
"stopSystemRingtone" -> {
103+
stopSystemRingtone()
104+
result.success(null)
105+
}
106+
"testAudio" -> {
107+
testAudioSetup()
108+
result.success(null)
109+
}
110+
else -> result.notImplemented()
111+
}
112+
}
113+
74114
methodChannel2.setMethodCallHandler { call, result ->
75115
if (call.method == "playDefaultAlarm") {
76116
playDefaultAlarm(this)
@@ -294,4 +334,226 @@ class MainActivity : FlutterActivity() {
294334
startActivity(intent)
295335
}
296336

337+
private fun getSystemRingtones(category: String): List<Map<String, String>> {
338+
val ringtonesList = mutableListOf<Map<String, String>>()
339+
340+
val ringtoneType = when (category.lowercase()) {
341+
"alarm" -> RingtoneManager.TYPE_ALARM
342+
"notification" -> RingtoneManager.TYPE_NOTIFICATION
343+
"ringtone" -> RingtoneManager.TYPE_RINGTONE
344+
else -> RingtoneManager.TYPE_ALARM
345+
}
346+
347+
val ringtoneManager = RingtoneManager(this)
348+
ringtoneManager.setType(ringtoneType)
349+
350+
val cursor: Cursor = ringtoneManager.cursor
351+
352+
while (cursor.moveToNext()) {
353+
val title = cursor.getString(RingtoneManager.TITLE_COLUMN_INDEX)
354+
val uri = ringtoneManager.getRingtoneUri(cursor.position)
355+
val id = cursor.getString(RingtoneManager.ID_COLUMN_INDEX)
356+
357+
if (title != null && uri != null) {
358+
val ringtoneMap = mapOf(
359+
"title" to title,
360+
"uri" to uri.toString(),
361+
"id" to (id ?: ""),
362+
"category" to category
363+
)
364+
ringtonesList.add(ringtoneMap)
365+
}
366+
}
367+
368+
cursor.close()
369+
return ringtonesList
370+
}
371+
372+
private fun requestAudioFocus(): Boolean {
373+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
374+
val audioAttributes = AudioAttributes.Builder()
375+
.setUsage(AudioAttributes.USAGE_ALARM)
376+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
377+
.build()
378+
379+
audioFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
380+
.setAudioAttributes(audioAttributes)
381+
.setAcceptsDelayedFocusGain(false)
382+
.setWillPauseWhenDucked(false)
383+
.build()
384+
385+
audioManager?.requestAudioFocus(audioFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
386+
} else {
387+
@Suppress("DEPRECATION")
388+
audioManager?.requestAudioFocus(
389+
null,
390+
AudioManager.STREAM_ALARM,
391+
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
392+
) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
393+
}
394+
}
395+
396+
private fun abandonAudioFocus() {
397+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
398+
audioFocusRequest?.let { audioManager?.abandonAudioFocusRequest(it) }
399+
} else {
400+
@Suppress("DEPRECATION")
401+
audioManager?.abandonAudioFocus(null)
402+
}
403+
}
404+
405+
private fun playSystemRingtone(ringtoneUri: String) {
406+
try {
407+
stopSystemRingtone()
408+
409+
410+
if (!requestAudioFocus()) {
411+
Log.w("SystemRingtone", "Could not gain audio focus")
412+
}
413+
414+
val uri = Uri.parse(ringtoneUri)
415+
systemRingtone = RingtoneManager.getRingtone(this, uri)
416+
417+
if (systemRingtone == null) {
418+
Log.e("SystemRingtone", "Failed to create ringtone from URI: $ringtoneUri")
419+
playFallbackAlarm()
420+
return
421+
}
422+
423+
424+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
425+
val audioAttributes = AudioAttributes.Builder()
426+
.setUsage(AudioAttributes.USAGE_ALARM)
427+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
428+
.build()
429+
systemRingtone?.audioAttributes = audioAttributes
430+
systemRingtone?.isLooping = true
431+
}
432+
433+
434+
val alarmVolume = audioManager?.getStreamVolume(AudioManager.STREAM_ALARM) ?: 0
435+
val maxAlarmVolume = audioManager?.getStreamMaxVolume(AudioManager.STREAM_ALARM) ?: 1
436+
437+
if (alarmVolume == 0) {
438+
// Set to 50% of max volume if currently muted
439+
audioManager?.setStreamVolume(
440+
AudioManager.STREAM_ALARM,
441+
maxAlarmVolume / 2,
442+
0
443+
)
444+
Log.i("SystemRingtone", "Alarm volume was muted, set to 50%")
445+
}
446+
447+
Log.i("SystemRingtone", "Playing system ringtone: $ringtoneUri, Volume: $alarmVolume/$maxAlarmVolume")
448+
systemRingtone?.play()
449+
450+
} catch (e: Exception) {
451+
Log.e("SystemRingtone", "Error playing system ringtone: ${e.message}")
452+
playFallbackAlarm()
453+
}
454+
}
455+
456+
private fun playFallbackAlarm() {
457+
try {
458+
val defaultAlarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
459+
systemRingtone = RingtoneManager.getRingtone(this, defaultAlarmUri)
460+
461+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
462+
val audioAttributes = AudioAttributes.Builder()
463+
.setUsage(AudioAttributes.USAGE_ALARM)
464+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
465+
.build()
466+
systemRingtone?.audioAttributes = audioAttributes
467+
systemRingtone?.isLooping = true
468+
}
469+
470+
systemRingtone?.play()
471+
Log.i("SystemRingtone", "Playing fallback default alarm")
472+
} catch (fallbackException: Exception) {
473+
Log.e("SystemRingtone", "Fallback alarm also failed: ${fallbackException.message}")
474+
}
475+
}
476+
477+
private fun stopSystemRingtone() {
478+
try {
479+
systemRingtone?.stop()
480+
systemRingtone = null
481+
abandonAudioFocus()
482+
Log.i("SystemRingtone", "Stopped system ringtone and abandoned audio focus")
483+
} catch (e: Exception) {
484+
Log.e("SystemRingtone", "Error stopping system ringtone: ${e.message}")
485+
}
486+
}
487+
488+
private fun testAudioSetup() {
489+
Log.i("SystemRingtone", "=== AUDIO DIAGNOSTICS ===")
490+
491+
492+
val am = audioManager
493+
if (am == null) {
494+
Log.e("SystemRingtone", "AudioManager is null!")
495+
return
496+
}
497+
498+
// Check volumes
499+
val alarmVolume = am.getStreamVolume(AudioManager.STREAM_ALARM)
500+
val maxAlarmVolume = am.getStreamMaxVolume(AudioManager.STREAM_ALARM)
501+
val musicVolume = am.getStreamVolume(AudioManager.STREAM_MUSIC)
502+
val maxMusicVolume = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
503+
val ringVolume = am.getStreamVolume(AudioManager.STREAM_RING)
504+
val maxRingVolume = am.getStreamMaxVolume(AudioManager.STREAM_RING)
505+
506+
Log.i("SystemRingtone", "Alarm Volume: $alarmVolume/$maxAlarmVolume")
507+
Log.i("SystemRingtone", "Music Volume: $musicVolume/$maxMusicVolume")
508+
Log.i("SystemRingtone", "Ring Volume: $ringVolume/$maxRingVolume")
509+
510+
511+
val ringerMode = am.ringerMode
512+
val ringerModeText = when (ringerMode) {
513+
AudioManager.RINGER_MODE_SILENT -> "SILENT"
514+
AudioManager.RINGER_MODE_VIBRATE -> "VIBRATE"
515+
AudioManager.RINGER_MODE_NORMAL -> "NORMAL"
516+
else -> "UNKNOWN($ringerMode)"
517+
}
518+
Log.i("SystemRingtone", "Ringer Mode: $ringerModeText")
519+
520+
521+
try {
522+
val defaultAlarmUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_ALARM)
523+
Log.i("SystemRingtone", "Default alarm URI: $defaultAlarmUri")
524+
525+
val testRingtone = RingtoneManager.getRingtone(this, defaultAlarmUri)
526+
if (testRingtone != null) {
527+
Log.i("SystemRingtone", "Default alarm ringtone created successfully")
528+
529+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
530+
val audioAttributes = AudioAttributes.Builder()
531+
.setUsage(AudioAttributes.USAGE_ALARM)
532+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
533+
.build()
534+
testRingtone.audioAttributes = audioAttributes
535+
Log.i("SystemRingtone", "Audio attributes set")
536+
}
537+
538+
requestAudioFocus()
539+
testRingtone.play()
540+
Log.i("SystemRingtone", "Test alarm started")
541+
542+
// Stop after 3 seconds
543+
android.os.Handler(mainLooper).postDelayed({
544+
testRingtone.stop()
545+
abandonAudioFocus()
546+
Log.i("SystemRingtone", "Test alarm stopped")
547+
}, 3000)
548+
549+
} else {
550+
Log.e("SystemRingtone", "Failed to create test ringtone")
551+
}
552+
} catch (e: Exception) {
553+
Log.e("SystemRingtone", "Test audio failed: ${e.message}")
554+
}
555+
556+
Log.i("SystemRingtone", "=== END DIAGNOSTICS ===")
557+
}
558+
297559
}

android/gradle.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ org.gradle.jvmargs=-Xmx4608m
22
android.useAndroidX=true
33
android.enableJetifier=true
44
android.enableR8=true
5-
org.gradle.wrapper.verification=true
5+
org.gradle.wrapper.verification=true
6+
android.aapt2FromMavenOverride=/Users/roronoazoro/Library/Android/sdk/build-tools/34.0.0/aapt2

lib/app/data/models/ringtone_model.dart

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,18 @@ class RingtoneModel {
88
late String ringtoneName;
99
late String ringtonePath;
1010
late int currentCounterOfUsage;
11+
late bool isSystemRingtone;
12+
late String ringtoneUri;
13+
late String category;
1114

1215
Id get isarId => AudioUtils.fastHash(ringtoneName);
1316

1417
RingtoneModel({
1518
required this.ringtoneName,
1619
required this.ringtonePath,
1720
required this.currentCounterOfUsage,
21+
this.isSystemRingtone = false,
22+
this.ringtoneUri = '',
23+
this.category = '',
1824
});
1925
}

0 commit comments

Comments
 (0)