Skip to content

Commit

Permalink
Merge branch 'main' into fix/alarm-rings-NOW-in-DateTime.now
Browse files Browse the repository at this point in the history
  • Loading branch information
dtkdt100 committed Oct 2, 2023
2 parents bcd8577 + 85fa68b commit 8538cda
Show file tree
Hide file tree
Showing 29 changed files with 968 additions and 571 deletions.
13 changes: 13 additions & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# These are supported funding model platforms

github: gdelataillade
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
otechie: # Replace with a single Otechie username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ migrate_working_dir/
**/doc/api/
.dart_tool/
.packages
build/
build/
3 changes: 3 additions & 0 deletions .pubignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Debug assets
assets/blank.mp3
assets/not_blank.mp3
46 changes: 46 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,49 @@
## 2.0.1
* Update README.
* Fix example app's ring now button.
* Refactor set alarm methods.

## 2.0.0
**Breaking Changes**
* Installation steps were updated in the README. Please make sure to follow them.
* [iOS] Add Background Fetch to periodically make sure alarms are still active in the background.

## 2.0.0-release-candidate-1
* Add minor improvements.

## 2.0.0-dev.5
* [iOS] Move background fetch to native.
* [Android] Fix build errors.

## 2.0.0-dev.4
* [iOS] Improve background fetch & audio interruptions

## 2.0.0-dev.3
* [iOS] Improve alarm reliability.

## 2.0.0-dev.2
* [iOS] Improve silent audio interruption handling.
* Add a shortcut button in example app.

## 2.0.0-dev.1
* [iOS] Play silent audio until alarm rings to keep app active.
* [iOS] Add Background Fetch to periodically check if app is still active.
* [iOS] Add new installation steps.

## 1.2.2
* Upgrade plugin's dependencies.
* Prove plugin ownership for winning OnePub competition: 961eace7-3bbb-11ee-ade6-42010ab60008

## 1.2.1
* Fix fromJson error on plugin init.

## 1.2.0
* Add [volumeMax] parameter.
* [iOS] Keep app active in background by playing silent sound in a loop.

## 1.1.8
* [Android] Add missing isRinging method.

## 1.1.7
* [iOS] Fix alarm stop.

Expand Down
143 changes: 49 additions & 94 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,86 +5,20 @@

[![alarm](https://github.com/gdelataillade/alarm/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/gdelataillade/alarm/actions/workflows/main.yml)

Support our mission to improve this plugin. [Vote for it on OnePub](https://onepub.dev/packages/alarm/).
🏆 Winner of the [2023 OnePub Community Choice Awards](https://onepub.dev/Competition).

# Alarm plugin for iOS and Android

This Flutter plugin provides a simple and easy-to-use interface for setting and canceling alarms on iOS and Android devices. It utilizes the `android_alarm_manager_plus` plugin for Android and the native iOS `AVAudioPlayer` class.

## Why this plugin ?
## 🔧 Installation steps

As a Flutter developer at [Evolum](https://evolum.co), my CTO and I needed to develop an alarm feature for the new version of our app.
Please carefully follow these installation steps. They have been updated for plugin version `2.0.0`.

An alarm feature is a great way to increase users engagement.
### [iOS Setup](https://github.com/gdelataillade/alarm/blob/feat/ios-background-fetch/help/INSTALL-IOS.md)
### [Android Setup](https://github.com/gdelataillade/alarm/blob/feat/ios-background-fetch/help/INSTALL-ANDROID.md)

For the Android part, we used `android_alarm_manager_plus` plugin, but to be honest it was not very intuitive and incomplete.

Then, for the iOS part, we couldn't find any plugin or tutorial to add this feature.

Another issue we found is that when a user kills the app, all processes are terminated so the alarm may not ring. The workaround we thought about was to show a notification when the user kills the app to warn him that the alarm may not ring. Then, he just has to reopen the app to reschedule the alarm.

Therefore, we decided to write our own plugin to wrap everything and make it easy for everybody.

## Under the hood

### Android
Uses `oneShotAt` from the `android_alarm_manager_plus` plugin with a two-way communication isolated callback to start/stop the alarm.

### iOS
Implements `invokeMethod` to play the alarm audio using `AVAudioPlayer`. Due to the suspension of asynchronous native code when the app is in the background, we listen for app state changes and check if the player is playing when the app returns to the foreground. If it's the case, it means the alarm is ringing, and it's time to trigger your `onRing` callback.

## Getting Started

### iOS installation steps

In order to play audio in background, open your project in Xcode, select your Runner and select the Capabilities tab. Under the Capabilities tab, set the Background Modes switch to ON and select the “Audio, AirPlay, and Picture in Picture” option under the list of available modes.

### Android installation steps

In your `android/app/build.gradle`, make sure you have the following config:
```Gradle
android {
compileSdkVersion 33
[...]
defaultConfig {
[...]
multiDexEnabled true
}
}
```

After that, add the following to your `AndroidManifest.xml` within the `<manifest></manifest>` tags:

```xml
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- For apps with targetSDK=31 (Android 12) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
```

Next, within the `<application></application>` tags, add:

```xml
<service
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmService"
android:permission="android.permission.BIND_JOB_SERVICE"
android:exported="false"/>
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.AlarmBroadcastReceiver"
android:exported="false"/>
<receiver
android:name="dev.fluttercommunity.plus.androidalarmmanager.RebootBroadcastReceiver"
android:enabled="false"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
```

Finally, add your audio asset(s) to your project like usual.

## How to use
## 📖 How to use

Add to your pubspec.yaml:
```Bash
Expand All @@ -104,6 +38,7 @@ final alarmSettings = AlarmSettings(
assetAudioPath: 'assets/alarm.mp3',
loopAudio: true,
vibrate: true,
volumeMax: true,
fadeDuration: 3.0,
notificationTitle: 'This is the title',
notificationBody: 'This is the body',
Expand All @@ -113,7 +48,7 @@ final alarmSettings = AlarmSettings(

And finally set the alarm:
```Dart
await Alarm.set(settings: alarmSettings)
await Alarm.set(alarmSettings: alarmSettings)
```

Property | Type | Description
Expand All @@ -123,6 +58,7 @@ alarmDateTime | `DateTime` | The date and time you want your alarm to ring
assetAudio | `String` | The path to you audio asset you want to use as ringtone. Can be a path in your assets folder or a downloaded local file path.
loopAudio | `bool` | If true, audio will repeat indefinitely until alarm is stopped.
vibrate | `bool` | If true, device will vibrate indefinitely until alarm is stopped. If [loopAudio] is set to false, vibrations will stop when audio ends.
volumeMax | `bool` | If true, set system volume to maximum when [dateTime] is reached. Set back to previous volume when alarm is stopped.
fadeDuration | `double` | Duration, in seconds, over which to fade the alarm volume. Set to 0 by default, which means no fade.
notificationTitle | `String` | The title of the notification triggered when alarm rings if app is on background.
notificationBody | `String` | The body of the notification.
Expand All @@ -148,42 +84,61 @@ Alarm.ringStream.stream.listen((_) => yourOnRingCallback());

To avoid unexpected behaviors, if you set an alarm for the same time as an existing one, the new alarm will replace the existing one.

## Example app
## 📱 Example app

Don't hesitate to check out the example's code, and take a look at the app:

![alarm_example_1](https://user-images.githubusercontent.com/32983806/230773695-915860d5-fb3d-47ee-b990-805ff33ed0c3.png)
![alarm_example_2](https://user-images.githubusercontent.com/32983806/230773701-f77a042d-a493-4b9c-a9d0-41509fe227fd.png)
![home](https://github.com/gdelataillade/alarm/assets/32983806/695736aa-b55f-4050-8b0d-274b0d46714a)
![edit](https://github.com/gdelataillade/alarm/assets/32983806/05329836-9fbe-462c-aa1e-dce0fa70f455)

## ⏰ Alarm behaviour

| | Sound | Vibrate | Volume max | Notification
| ------------------------ | ----- | ------- | ---------- | -------
| Locked screen | ✅ | ✅ | ✅ | ✅
| Silent / Mute | ✅ | ✅ | ✅ | ✅
| Do not disturb | ✅ | ✅ | ✅ | Silenced
| Sleep mode | ✅ | ✅ | ✅ | Silenced
| While playing other media| ✅ | ✅ | ✅ | ✅
| App killed | ❌ | ❌ | ❌ | ✅

## Alarm behaviour
*Silenced: Means that the notification is not shown directly on the top of the screen. You have to go in your notification center to see it.*

| | Sound | Vibrate | Notification
| ------------------------ | ----- | ------- | -------
| Locked screen | ✅ | ✅ | ✅
| Silent / Mute | ✅ | ✅ | ✅
| Do not disturb | ✅ | ✅ | Silenced
| Sleep mode | ✅ | ✅ | Silenced
| While playing other media| ✅ | ✅ | ✅
| App killed | ❌ | ❌ | ✅
## ❓ FAQ

*Silenced: Means that the notification is not shown directly on the top of the screen. You have to go to your notification center to see it.*
### Why didn't my alarm fire on iOS?

## FAQ
Several factors could prevent your alarm from ringing:
- Your iPhone was restarted (either from a manual reboot or due to an iOS update).
- The app was either manually terminated or was closed because of memory constraints.

### My alarm is not firing on a specific Android device

Some Android manufacturers prefer battery life over proper functionality of your apps. Check out [dontkillmyapp.com](https://dontkillmyapp.com) to find out about more about optimizations done by different vendors, and potential workarounds.
*Source: [https://pub.dev/packages/android_alarm_manager_plus#faq](https://pub.dev/packages/android_alarm_manager_plus#faq)*
Some Android manufacturers prefer battery life over proper functionality of your apps. Check out [dontkillmyapp.com](https://dontkillmyapp.com) to find out about more about optimizations done by different vendors, and potential workarounds.
Most common solution is to educate users to disable **battery optimization** settings.
*Source: [android_alarm_manager_plus FAQ](https://pub.dev/packages/android_alarm_manager_plus#faq)*

### Why isn't my alarm ringing when the device volume is off?
### How can I increase the reliability of the alarm ringing?

iOS prevents third-party apps from modifying volumes. If media volume is off, then the alarm is muted. To ensure the alarm rings, users should turn on media volume before setting the alarm. For consistency, we don't add volume checks for Android, maintaining a uniform user experience on both platforms. Feel free to add one in your app is desired.
The more time the app spends in the background, the higher the chance the OS might stop it from running due to memory or battery optimizations. Here's how you can optimize:

- **Regular App Usage**: Encourage users to open the app at least once a day.
- **Leverage Background Modes**: Engage in activities like weather API calls that keep the app active in the background.
- **User Settings**: Educate users to refrain from using 'Do Not Disturb' (DnD) and 'Low Power Mode' when they're expecting the alarm to ring.

## ⚙️ Under the hood

### Android
Uses `oneShotAt` from the `android_alarm_manager_plus` plugin with a two-way communication isolated callback to start/stop the alarm.

### iOS
Keeps the app awake using a silent `AVAudioPlayer` until alarm rings. When in the background, it also uses `Background App Refresh` to periodically ensure the app is still active.

## Feature request
## ✉️ Feature request

If you have a feature request, just open an issue explaining clearly what you want and if you convince me I will develop it for you.

## Contributing
## 💙 Contributing

We welcome contributions to this plugin! If you would like to make a change or add a new feature, please follow these steps:

Expand All @@ -196,7 +151,7 @@ These are some features that I have in mind that could be useful:
- Use `ffigen` and `jnigen` binding generators to call native code more efficiently instead of using method channels.
- [Notification actions](https://pub.dev/packages/flutter_local_notifications#notification-actions): stop and snooze.
- Stop alarm sound when notification is dismissed.
- Add macOS, Windows, Linux and web support.
- Make alarm ring even if app was terminated.

Thank you for considering contributing to this plugin. Your help is greatly appreciated!

Expand Down
1 change: 0 additions & 1 deletion android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.VIBRATE"/>
<!-- For apps with targetSDK=31 (Android 12) -->
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<application>
<service
Expand Down
Binary file added assets/blank.mp3
Binary file not shown.
Binary file added assets/long_blank.mp3
Binary file not shown.
Binary file added assets/not_blank.mp3
Binary file not shown.
2 changes: 1 addition & 1 deletion example/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
package="com.gdelataillade.alarm.alarm_example">
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- For apps with targetSDK=31 (Android 12) -->
<uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<application
android:label="alarm_example"
Expand Down
16 changes: 14 additions & 2 deletions example/ios/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ PODS:
- Flutter
- audio_session (0.0.1):
- Flutter
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_fgbg (0.0.1):
- Flutter
Expand All @@ -18,23 +20,29 @@ PODS:
- FlutterMacOS
- vibration (1.7.5):
- Flutter
- volume_controller (0.0.1):
- Flutter

DEPENDENCIES:
- alarm (from `.symlinks/plugins/alarm/ios`)
- audio_session (from `.symlinks/plugins/audio_session/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_fgbg (from `.symlinks/plugins/flutter_fgbg/ios`)
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
- just_audio (from `.symlinks/plugins/just_audio/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- vibration (from `.symlinks/plugins/vibration/ios`)
- volume_controller (from `.symlinks/plugins/volume_controller/ios`)

EXTERNAL SOURCES:
alarm:
:path: ".symlinks/plugins/alarm/ios"
audio_session:
:path: ".symlinks/plugins/audio_session/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_fgbg:
Expand All @@ -49,17 +57,21 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
vibration:
:path: ".symlinks/plugins/vibration/ios"
volume_controller:
:path: ".symlinks/plugins/volume_controller/ios"

SPEC CHECKSUMS:
alarm: 6c1f6a9688f94cd6bf8f104c67cc26e78c9d8d13
audio_session: 4f3e461722055d21515cf3261b64c973c062f345
device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
flutter_fgbg: 31c0d1140a131daea2d342121808f6aa0dcd879d
flutter_local_notifications: 0c0b1ae97e741e1521e4c1629a459d04b9aec743
just_audio: baa7252489dbcf47a4c7cc9ca663e9661c99aafa
path_provider_foundation: 37748e03f12783f9de2cb2c4eadfaa25fe6d4852
shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
vibration: 7d883d141656a1c1a6d8d238616b2042a51a1241
volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9

PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3

Expand Down
2 changes: 1 addition & 1 deletion example/ios/Runner.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
Expand Down
2 changes: 2 additions & 0 deletions example/ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import UIKit
import Flutter
import UserNotifications
import alarm

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
Expand All @@ -11,6 +12,7 @@ import UserNotifications
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
}
SwiftAlarmPlugin.registerBackgroundTasks()

GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
Expand Down
Loading

0 comments on commit 8538cda

Please sign in to comment.