Skip to content

Commit

Permalink
Feature -> Add volume enforcement (#277)
Browse files Browse the repository at this point in the history
* Add volume enforcement for Android

* Add volume enforcement for iOS

* Remove warnings
  • Loading branch information
gdelataillade authored Nov 3, 2024
1 parent 2cae6cf commit 51fbcd7
Show file tree
Hide file tree
Showing 9 changed files with 86 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ class AlarmPlugin : FlutterPlugin, MethodChannel.MethodCallHandler {
intent.putExtra("loopAudio", call.argument<Boolean>("loopAudio") ?: true)
intent.putExtra("vibrate", call.argument<Boolean>("vibrate") ?: true)
intent.putExtra("volume", call.argument<Double>("volume"))
intent.putExtra("volumeEnforced", call.argument<Boolean>("volumeEnforced") ?: false)
intent.putExtra("fadeDuration", call.argument<Double>("fadeDuration") ?: 0.0)
intent.putExtra("fullScreenIntent", call.argument<Boolean>("fullScreenIntent") ?: true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,11 @@ class AlarmService : Service() {
val loopAudio = intent.getBooleanExtra("loopAudio", true)
val vibrate = intent.getBooleanExtra("vibrate", true)
val volume = intent.getDoubleExtra("volume", -1.0)
val volumeEnforced = intent.getBooleanExtra("volumeEnforced", false)
val fadeDuration = intent.getDoubleExtra("fadeDuration", 0.0)

Log.d("AlarmService", "volume enforced: $volumeEnforced")

// Notify the plugin about the alarm ringing
AlarmPlugin.eventSink?.success(
mapOf(
Expand All @@ -121,7 +124,7 @@ class AlarmService : Service() {

// Set the volume if specified
if (volume >= 0.0 && volume <= 1.0) {
volumeService?.setVolume(volume, showSystemUI)
volumeService?.setVolume(volume, volumeEnforced, showSystemUI)
}

// Request audio focus
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ data class AlarmSettings(
val loopAudio: Boolean,
val vibrate: Boolean,
val volume: Double?,
val volumeEnforced: Boolean = false,
val fadeDuration: Double,
val warningNotificationOnKill: Boolean,
val androidFullScreenIntent: Boolean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,55 @@ import android.media.AudioManager
import android.media.AudioAttributes
import android.media.AudioFocusRequest
import android.os.Build
import android.os.Handler
import android.os.Looper
import kotlin.math.round
import io.flutter.Log

class VolumeService(private val context: Context) {
private var previousVolume: Int? = null
private val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
private var focusRequest: AudioFocusRequest? = null
private val handler = Handler(Looper.getMainLooper())
private var targetVolume: Int = 0
private var volumeCheckRunnable: Runnable? = null

fun setVolume(volume: Double, showSystemUI: Boolean) {
fun setVolume(volume: Double, volumeEnforced: Boolean, showSystemUI: Boolean) {
val maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
previousVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
val _volume = (round(volume * maxVolume)).toInt()
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, _volume, if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0)
targetVolume = (round(volume * maxVolume)).toInt()
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, targetVolume, if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0)

if (volumeEnforced) {
startVolumeEnforcement(showSystemUI)
}
}

private fun startVolumeEnforcement(showSystemUI: Boolean) {
// Define the Runnable that checks and enforces the volume level
volumeCheckRunnable = Runnable {
val currentVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
if (currentVolume != targetVolume) {
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, targetVolume, if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0)
}
// Schedule the next check after 1000ms
handler.postDelayed(volumeCheckRunnable!!, 1000)
}
// Start the first run
handler.post(volumeCheckRunnable!!)
}

private fun stopVolumeEnforcement() {
// Remove callbacks to stop enforcing volume
volumeCheckRunnable?.let { handler.removeCallbacks(it) }
volumeCheckRunnable = null
}

fun restorePreviousVolume(showSystemUI: Boolean) {
// Stop the volume enforcement if it's active
stopVolumeEnforcement()

// Restore the previous volume
previousVolume?.let { prevVolume ->
audioManager.setStreamVolume(AudioManager.STREAM_MUSIC, prevVolume, if (showSystemUI) AudioManager.FLAG_SHOW_UI else 0)
previousVolume = null
Expand Down
36 changes: 4 additions & 32 deletions example/lib/screens/ring.dart
Original file line number Diff line number Diff line change
@@ -1,39 +1,11 @@
import 'dart:async';

import 'package:alarm/alarm.dart';
import 'package:flutter/material.dart';

class ExampleAlarmRingScreen extends StatefulWidget {
class ExampleAlarmRingScreen extends StatelessWidget {
const ExampleAlarmRingScreen({required this.alarmSettings, super.key});

final AlarmSettings alarmSettings;

@override
State<ExampleAlarmRingScreen> createState() => _ExampleAlarmRingScreenState();
}

class _ExampleAlarmRingScreenState extends State<ExampleAlarmRingScreen> {
@override
void initState() {
super.initState();
Timer.periodic(const Duration(seconds: 1), (timer) async {
if (!mounted) {
timer.cancel();
return;
}

final isRinging = await Alarm.isRinging(widget.alarmSettings.id);
if (isRinging) {
alarmPrint('Alarm ${widget.alarmSettings.id} is still ringing...');
return;
}

alarmPrint('Alarm ${widget.alarmSettings.id} stopped ringing.');
timer.cancel();
if (mounted) Navigator.pop(context);
});
}

@override
Widget build(BuildContext context) {
return Scaffold(
Expand All @@ -42,7 +14,7 @@ class _ExampleAlarmRingScreenState extends State<ExampleAlarmRingScreen> {
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Text(
'You alarm (${widget.alarmSettings.id}) is ringing...',
'You alarm (${alarmSettings.id}) is ringing...',
style: Theme.of(context).textTheme.titleLarge,
),
const Text('🔔', style: TextStyle(fontSize: 50)),
Expand All @@ -53,7 +25,7 @@ class _ExampleAlarmRingScreenState extends State<ExampleAlarmRingScreen> {
onPressed: () {
final now = DateTime.now();
Alarm.set(
alarmSettings: widget.alarmSettings.copyWith(
alarmSettings: alarmSettings.copyWith(
dateTime: DateTime(
now.year,
now.month,
Expand All @@ -73,7 +45,7 @@ class _ExampleAlarmRingScreenState extends State<ExampleAlarmRingScreen> {
),
RawMaterialButton(
onPressed: () {
Alarm.stop(widget.alarmSettings.id).then((_) {
Alarm.stop(alarmSettings.id).then((_) {
if (context.mounted) Navigator.pop(context);
});
},
Expand Down
5 changes: 4 additions & 1 deletion ios/Classes/AlarmConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,20 @@ class AlarmConfiguration {
let loopAudio: Bool
let fadeDuration: Double
let volume: Float?
var volumeEnforced: Bool
var volumeEnforcementTimer: Timer?
var triggerTime: Date?
var audioPlayer: AVAudioPlayer?
var timer: Timer?
var task: DispatchWorkItem?

init(id: Int, assetAudio: String, vibrationsEnabled: Bool, loopAudio: Bool, fadeDuration: Double, volume: Float?) {
init(id: Int, assetAudio: String, vibrationsEnabled: Bool, loopAudio: Bool, fadeDuration: Double, volume: Float?, volumeEnforced: Bool) {
self.id = id
self.assetAudio = assetAudio
self.vibrationsEnabled = vibrationsEnabled
self.loopAudio = loopAudio
self.fadeDuration = fadeDuration
self.volume = volume
self.volumeEnforced = volumeEnforced
}
}
14 changes: 13 additions & 1 deletion ios/Classes/SwiftAlarmPlugin.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,8 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
vibrationsEnabled: alarmSettings.vibrate,
loopAudio: alarmSettings.loopAudio,
fadeDuration: alarmSettings.fadeDuration,
volume: volumeFloat
volume: volumeFloat,
volumeEnforced: alarmSettings.volumeEnforced
)

self.alarms[id] = alarmConfig
Expand Down Expand Up @@ -290,6 +291,16 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
} else {
audioPlayer.volume = 1.0
}

if alarm.volumeEnforced {
alarm.volumeEnforcementTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] timer in
guard let self = self else { return }
let currentSystemVolume = self.getSystemVolume()
if abs(currentSystemVolume - targetSystemVolume) > 0.01 {
self.setVolume(volume: targetSystemVolume, enable: false)
}
}
}
}

private func getSystemVolume() -> Float {
Expand Down Expand Up @@ -341,6 +352,7 @@ public class SwiftAlarmPlugin: NSObject, FlutterPlugin {
alarm.timer?.invalidate()
alarm.task?.cancel()
alarm.audioPlayer?.stop()
alarm.volumeEnforcementTimer?.invalidate()
self.alarms.removeValue(forKey: id)
}

Expand Down
14 changes: 9 additions & 5 deletions ios/Classes/models/AlarmSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct AlarmSettings: Codable {
let warningNotificationOnKill: Bool
let androidFullScreenIntent: Bool
let notificationSettings: NotificationSettings
let volumeEnforced: Bool

static func fromJson(json: [String: Any]) -> AlarmSettings? {
guard let id = json["id"] as? Int,
Expand All @@ -29,10 +30,11 @@ struct AlarmSettings: Codable {
let maxValidMicroseconds: Int64 = 9223372036854775 // Corresponding to year 2262
let safeDateTimeMicros = min(dateTimeMicros, maxValidMicroseconds)

let dateTime = Date(timeIntervalSince1970: TimeInterval(safeDateTimeMicros) / 1_000_000)
let volume = json["volume"] as? Double
let dateTime: Date = Date(timeIntervalSince1970: TimeInterval(safeDateTimeMicros) / 1_000_000)
let volume: Double? = json["volume"] as? Double
let notificationSettings = NotificationSettings.fromJson(json: notificationSettingsDict)

let volumeEnforced: Bool = json["volumeEnforced"] as? Bool ?? false

return AlarmSettings(
id: id,
dateTime: dateTime,
Expand All @@ -43,7 +45,8 @@ struct AlarmSettings: Codable {
fadeDuration: fadeDuration,
warningNotificationOnKill: warningNotificationOnKill,
androidFullScreenIntent: androidFullScreenIntent,
notificationSettings: notificationSettings
notificationSettings: notificationSettings,
volumeEnforced: volumeEnforced
)
}

Expand All @@ -63,10 +66,11 @@ struct AlarmSettings: Codable {
"loopAudio": alarmSettings.loopAudio,
"vibrate": alarmSettings.vibrate,
"volume": alarmSettings.volume,
"volumeEnforced": alarmSettings.volumeEnforced,
"fadeDuration": alarmSettings.fadeDuration,
"warningNotificationOnKill": alarmSettings.warningNotificationOnKill,
"androidFullScreenIntent": alarmSettings.androidFullScreenIntent,
"notificationSettings": NotificationSettings.toJson(notificationSettings: alarmSettings.notificationSettings)
]
}
}
}
14 changes: 14 additions & 0 deletions lib/model/alarm_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class AlarmSettings {
this.loopAudio = true,
this.vibrate = true,
this.volume,
this.volumeEnforced = false,
this.fadeDuration = 0.0,
this.warningNotificationOnKill = true,
this.androidFullScreenIntent = true,
Expand Down Expand Up @@ -53,6 +54,7 @@ class AlarmSettings {
loopAudio: json['loopAudio'] as bool? ?? true,
vibrate: json['vibrate'] as bool? ?? true,
volume: json['volume'] as double?,
volumeEnforced: json['volumeEnforced'] as bool? ?? false,
fadeDuration: json['fadeDuration'] as double? ?? 0.0,
warningNotificationOnKill: warningNotificationOnKill,
androidFullScreenIntent: json['androidFullScreenIntent'] as bool? ?? true,
Expand Down Expand Up @@ -112,6 +114,14 @@ class AlarmSettings {
/// Defaults to `null`.
final double? volume;

/// If true, the alarm volume is enforced, automatically resetting to the
/// original alarm [volume] if the user attempts to adjust it.
/// This prevents the user from lowering the alarm volume.
/// Won't work if app is killed.
///
/// Defaults to false.
final bool volumeEnforced;

/// Duration, in seconds, over which to fade the alarm ringtone.
/// Set to 0.0 by default, which means no fade.
final double fadeDuration;
Expand Down Expand Up @@ -148,6 +158,7 @@ class AlarmSettings {
hash = hash ^ loopAudio.hashCode;
hash = hash ^ vibrate.hashCode;
hash = hash ^ volume.hashCode;
hash = hash ^ volumeEnforced.hashCode;
hash = hash ^ fadeDuration.hashCode;
hash = hash ^ warningNotificationOnKill.hashCode;
hash = hash ^ androidFullScreenIntent.hashCode;
Expand All @@ -166,6 +177,7 @@ class AlarmSettings {
bool? loopAudio,
bool? vibrate,
double? volume,
bool? volumeEnforced,
double? fadeDuration,
String? notificationTitle,
String? notificationBody,
Expand All @@ -180,6 +192,7 @@ class AlarmSettings {
loopAudio: loopAudio ?? this.loopAudio,
vibrate: vibrate ?? this.vibrate,
volume: volume ?? this.volume,
volumeEnforced: volumeEnforced ?? this.volumeEnforced,
fadeDuration: fadeDuration ?? this.fadeDuration,
warningNotificationOnKill:
warningNotificationOnKill ?? this.warningNotificationOnKill,
Expand All @@ -197,6 +210,7 @@ class AlarmSettings {
'loopAudio': loopAudio,
'vibrate': vibrate,
'volume': volume,
'volumeEnforced': volumeEnforced,
'fadeDuration': fadeDuration,
'warningNotificationOnKill': warningNotificationOnKill,
'androidFullScreenIntent': androidFullScreenIntent,
Expand Down

0 comments on commit 51fbcd7

Please sign in to comment.