├── ios ├── live_activities │ ├── .build │ │ ├── .lock │ │ └── workspace-state.json │ ├── Sources │ │ └── live_activities │ │ │ ├── uuid.swift │ │ │ ├── PrivacyInfo.xcprivacy │ │ │ └── LiveActivitiesPlugin.swift │ └── Package.swift ├── .gitignore └── live_activities.podspec ├── android ├── settings.gradle ├── .gitignore ├── src │ ├── main │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── example │ │ │ │ └── live_activities │ │ │ │ ├── LiveActivityManagerHolder.kt │ │ │ │ ├── LiveActivityFirebaseMessagingService.kt │ │ │ │ ├── LiveActivitiesPlugin.kt │ │ │ │ └── LiveActivityManager.kt │ │ └── AndroidManifest.xml │ └── test │ │ └── kotlin │ │ └── com │ │ └── example │ │ └── live_activities │ │ └── LiveActivitiesPluginTest.kt └── build.gradle ├── example ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── Runner.entitlements │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── extension-example │ │ ├── Assets.xcassets │ │ │ ├── Contents.json │ │ │ ├── AccentColor.colorset │ │ │ │ └── Contents.json │ │ │ ├── WidgetBackground.colorset │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ └── Contents.json │ │ ├── Info.plist │ │ └── extension_example.swift │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── RunnerTests │ │ └── RunnerTests.swift │ ├── extension-exampleExtension.entitlements │ ├── Podfile.lock │ ├── .gitignore │ └── Podfile ├── assets │ ├── images │ │ ├── psg.png │ │ └── chelsea.png │ └── files │ │ └── rules.txt ├── android │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── values │ │ │ │ │ │ ├── colors.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── values-night │ │ │ │ │ │ ├── colors.xml │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable-hdpi │ │ │ │ │ │ └── ic_notification.png │ │ │ │ │ ├── drawable-mdpi │ │ │ │ │ │ └── ic_notification.png │ │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ │ └── ic_notification.png │ │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ │ └── ic_notification.png │ │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ │ └── ic_notification.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ └── layout │ │ │ │ │ │ └── live_activity.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── live_activities_example │ │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ │ └── CustomLiveActivityManager.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle │ └── settings.gradle ├── README.md ├── .gitignore ├── .metadata ├── analysis_options.yaml ├── lib │ ├── widgets │ │ └── score_widget.dart │ ├── models │ │ └── football_game_live_activity_model.dart │ └── main.dart └── pubspec.yaml ├── images ├── logo.webp ├── radion.webp ├── tutorial │ ├── app_group.webp │ ├── url_scheme.webp │ ├── push_capability.webp │ ├── enable_live_activities.webp │ └── create_widget_extension.webp └── showcase │ ├── animations │ ├── android-demo.gif │ ├── create_live_activity.webp │ └── update_live_activity.webp │ └── static │ ├── android_expanded.png │ ├── dynamic_island.webp │ ├── android_collapsed.png │ └── lockscreen_live_activity.webp ├── analysis_options.yaml ├── lib ├── models │ ├── alert_config.dart │ ├── live_activity_state.dart │ ├── url_scheme_data.dart │ ├── live_activity_file.dart │ └── activity_update.dart ├── services │ ├── image_file_service.dart │ └── app_groups_file_service.dart ├── live_activities_platform_interface.dart ├── live_activities_method_channel.dart └── live_activities.dart ├── .vscode └── launch.json ├── .gitignore ├── LICENSE ├── .metadata ├── pubspec.yaml ├── test ├── live_activities_method_channel_test.dart └── live_activities_test.dart └── CHANGELOG.md /ios/live_activities/.build/.lock: -------------------------------------------------------------------------------- 1 | 56498 -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'live_activities' 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /images/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/logo.webp -------------------------------------------------------------------------------- /images/radion.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/radion.webp -------------------------------------------------------------------------------- /ios/live_activities/Sources/live_activities/uuid.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import CryptoKit 3 | 4 | -------------------------------------------------------------------------------- /example/assets/images/psg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/assets/images/psg.png -------------------------------------------------------------------------------- /images/tutorial/app_group.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/tutorial/app_group.webp -------------------------------------------------------------------------------- /example/assets/images/chelsea.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/assets/images/chelsea.png -------------------------------------------------------------------------------- /images/tutorial/url_scheme.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/tutorial/url_scheme.webp -------------------------------------------------------------------------------- /images/tutorial/push_capability.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/tutorial/push_capability.webp -------------------------------------------------------------------------------- /example/ios/extension-example/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /images/showcase/animations/android-demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/showcase/animations/android-demo.gif -------------------------------------------------------------------------------- /images/showcase/static/android_expanded.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/showcase/static/android_expanded.png -------------------------------------------------------------------------------- /images/showcase/static/dynamic_island.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/showcase/static/dynamic_island.webp -------------------------------------------------------------------------------- /images/tutorial/enable_live_activities.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/tutorial/enable_live_activities.webp -------------------------------------------------------------------------------- /images/showcase/static/android_collapsed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/showcase/static/android_collapsed.png -------------------------------------------------------------------------------- /images/tutorial/create_widget_extension.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/tutorial/create_widget_extension.webp -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .cxx 10 | -------------------------------------------------------------------------------- /images/showcase/animations/create_live_activity.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/showcase/animations/create_live_activity.webp -------------------------------------------------------------------------------- /images/showcase/animations/update_live_activity.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/showcase/animations/update_live_activity.webp -------------------------------------------------------------------------------- /images/showcase/static/lockscreen_live_activity.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/images/showcase/static/lockscreen_live_activity.webp -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #000000 4 | 5 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFFFFF 4 | 5 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-hdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/drawable-hdpi/ic_notification.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-mdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/drawable-mdpi/ic_notification.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-xhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/drawable-xhdpi/ic_notification.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/drawable-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /android/src/main/kotlin/com/example/live_activities/LiveActivityManagerHolder.kt: -------------------------------------------------------------------------------- 1 | package com.example.live_activities 2 | 3 | object LiveActivityManagerHolder { 4 | var instance: LiveActivityManager? = null 5 | } 6 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/istornz/flutter_live_activities/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | 6 | formatter: 7 | page_width: 80 8 | -------------------------------------------------------------------------------- /example/assets/files/rules.txt: -------------------------------------------------------------------------------- 1 | Football (soccer) is a game where two teams of 11 players aim to score goals by kicking a ball into the opponent's net, following rules like no hand use (except for goalkeepers) and offside restrictions. -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/live_activities/.build/workspace-state.json: -------------------------------------------------------------------------------- 1 | { 2 | "object" : { 3 | "artifacts" : [ 4 | 5 | ], 6 | "dependencies" : [ 7 | 8 | ], 9 | "prebuilts" : [ 10 | 11 | ] 12 | }, 13 | "version" : 7 14 | } -------------------------------------------------------------------------------- /example/ios/extension-example/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/ios/extension-example/Assets.xcassets/WidgetBackground.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "idiom" : "universal" 5 | } 6 | ], 7 | "info" : { 8 | "author" : "xcode", 9 | "version" : 1 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 6 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | .kotlin -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/models/alert_config.dart: -------------------------------------------------------------------------------- 1 | class AlertConfig { 2 | final String title; 3 | final String body; 4 | final String? sound; 5 | 6 | AlertConfig({required this.title, required this.body, this.sound}); 7 | 8 | Map toMap() => { 9 | 'title': title, 10 | 'body': body, 11 | if (sound != null) 'sound': sound!, 12 | }; 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/ios/extension-exampleExtension.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.application-groups 6 | 7 | group.dimitridessus.liveactivities 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | aps-environment 6 | development 7 | com.apple.security.application-groups 8 | 9 | group.dimitridessus.liveactivities 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/extension-example/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSSupportsLiveActivities 6 | 7 | NSExtension 8 | 9 | NSExtensionPointIdentifier 10 | com.apple.widgetkit-extension 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Utilisez IntelliSense pour en savoir plus sur les attributs possibles. 3 | // Pointez pour afficher la description des attributs existants. 4 | // Pour plus d'informations, visitez : https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "example", 9 | "cwd": "example", 10 | "request": "launch", 11 | "type": "dart", 12 | }, 13 | ] 14 | } -------------------------------------------------------------------------------- /ios/live_activities/Sources/live_activities/PrivacyInfo.xcprivacy: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSPrivacyTrackingDomains 6 | 7 | NSPrivacyAccessedAPITypes 8 | 9 | NSPrivacyCollectedDataTypes 10 | 11 | NSPrivacyTracking 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /lib/models/live_activity_state.dart: -------------------------------------------------------------------------------- 1 | enum LiveActivityState { 2 | /// The Live Activity is active, visible to the user, and can receive content updates 3 | active, 4 | 5 | /// The Live Activity is visible, but the user, app, or system ended it, and it won't update its content 6 | /// anymore. 7 | ended, 8 | 9 | /// The Live Activity ended and is no longer visible because the user or the system removed it. 10 | dismissed, 11 | 12 | /// The Live Activity content is out of date and needs an update. 13 | stale, 14 | unknown, 15 | } 16 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | /Flutter/ephemeral/ 38 | /Flutter/flutter_export_environment.sh 39 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 11 | } 12 | 13 | func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { 14 | GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/live_activities_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.live_activities_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | import io.flutter.plugin.common.MethodChannel 5 | import io.flutter.embedding.engine.FlutterEngine 6 | 7 | import com.example.live_activities.LiveActivityManagerHolder 8 | 9 | class MainActivity : FlutterActivity() { 10 | override fun configureFlutterEngine(flutterEngine: FlutterEngine) { 11 | super.configureFlutterEngine(flutterEngine) 12 | 13 | LiveActivityManagerHolder.instance = CustomLiveActivityManager(this) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # live_activities_example 2 | 3 | Demonstrates how to use the live_activities plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - permission_handler_apple (9.3.0): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | permission_handler_apple: 14 | :path: ".symlinks/plugins/permission_handler_apple/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 18 | permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d 19 | 20 | PODFILE CHECKSUM: 251cb053df7158f337c0712f2ab29f4e0fa474ce 21 | 22 | COCOAPODS: 1.16.2 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/extension-example/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "platform" : "ios", 6 | "size" : "1024x1024" 7 | }, 8 | { 9 | "appearances" : [ 10 | { 11 | "appearance" : "luminosity", 12 | "value" : "dark" 13 | } 14 | ], 15 | "idiom" : "universal", 16 | "platform" : "ios", 17 | "size" : "1024x1024" 18 | }, 19 | { 20 | "appearances" : [ 21 | { 22 | "appearance" : "luminosity", 23 | "value" : "tinted" 24 | } 25 | ], 26 | "idiom" : "universal", 27 | "platform" : "ios", 28 | "size" : "1024x1024" 29 | } 30 | ], 31 | "info" : { 32 | "author" : "xcode", 33 | "version" : 1 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/models/url_scheme_data.dart: -------------------------------------------------------------------------------- 1 | class UrlSchemeData { 2 | final String scheme; 3 | final String? url; 4 | final String? host; 5 | final String? path; 6 | final List> queryParameters; 7 | 8 | UrlSchemeData({ 9 | required this.scheme, 10 | this.url, 11 | this.host, 12 | this.path, 13 | this.queryParameters = const [], 14 | }); 15 | 16 | factory UrlSchemeData.fromMap(Map map) { 17 | return UrlSchemeData( 18 | scheme: map['scheme'] ?? '', 19 | url: map['url'], 20 | host: map['host'], 21 | path: map['path'], 22 | queryParameters: map['queryItems'] != null 23 | ? (map['queryItems'] as List) 24 | .map((e) => Map.from(e)) 25 | .toList() 26 | : [], 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version '8.9.1' apply false 22 | id "org.jetbrains.kotlin.android" version "2.1.0" apply false 23 | } 24 | 25 | include ":app" 26 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 13.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .packages 33 | .pub-cache/ 34 | .pub/ 35 | /build/ 36 | 37 | # Symbolication related 38 | app.*.symbols 39 | 40 | # Obfuscation related 41 | app.*.map.json 42 | 43 | # Android Studio will place build artifacts here 44 | /android/app/debug 45 | /android/app/profile 46 | /android/app/release 47 | /android/app/.cxx 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © Dimitri Dessus 2 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 3 | 4 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 5 | 6 | The Software is provided “as is”, without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the software or the use or other dealings in the Software. -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 17 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 18 | - platform: android 19 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 20 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/src/test/kotlin/com/example/live_activities/LiveActivitiesPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.example.live_activities 2 | 3 | import io.flutter.plugin.common.MethodCall 4 | import io.flutter.plugin.common.MethodChannel 5 | import kotlin.test.Test 6 | import org.mockito.Mockito 7 | 8 | /* 9 | * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. 10 | * 11 | * Once you have built the plugin's example app, you can run these tests from the command 12 | * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or 13 | * you can run them directly from IDEs that support JUnit such as Android Studio. 14 | */ 15 | 16 | internal class LiveActivitiesPluginTest { 17 | @Test 18 | fun onMethodCall_getPlatformVersion_returnsExpectedValue() { 19 | val plugin = LiveActivitiesPlugin() 20 | 21 | val call = MethodCall("getPlatformVersion", null) 22 | val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) 23 | plugin.onMethodCall(call, mockResult) 24 | 25 | Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "17025dd88227cd9532c33fa78f5250d548d87e9a" 8 | channel: "stable" 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 17 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 18 | - platform: android 19 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 20 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 21 | - platform: ios 22 | create_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 23 | base_revision: 17025dd88227cd9532c33fa78f5250d548d87e9a 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /ios/live_activities/Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version: 5.9 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "live_activities", 8 | platforms: [ 9 | .iOS("13.0") 10 | ], 11 | products: [ 12 | .library(name: "live-activities", targets: ["live_activities"]) 13 | ], 14 | dependencies: [], 15 | targets: [ 16 | .target( 17 | name: "live_activities", 18 | dependencies: [], 19 | resources: [ 20 | // If your plugin requires a privacy manifest, for example if it uses any required 21 | // reason APIs, update the PrivacyInfo.xcprivacy file to describe your plugin's 22 | // privacy impact, and then uncomment these lines. For more information, see 23 | // https://developer.apple.com/documentation/bundleresources/privacy_manifest_files 24 | // .process("PrivacyInfo.xcprivacy"), 25 | 26 | // If you have other resources that need to be bundled with your plugin, refer to 27 | // the following instructions to add them: 28 | // https://developer.apple.com/documentation/xcode/bundling-resources-with-a-swift-package 29 | ] 30 | ) 31 | ] 32 | ) 33 | -------------------------------------------------------------------------------- /ios/live_activities.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. 3 | # Run `pod lib lint live_activities.podspec` to validate before publishing. 4 | # 5 | Pod::Spec.new do |s| 6 | s.name = 'live_activities' 7 | s.version = '0.0.1' 8 | s.summary = 'A new Flutter plugin project.' 9 | s.description = <<-DESC 10 | A new Flutter plugin project. 11 | DESC 12 | s.homepage = 'http://example.com' 13 | s.license = { :file => '../LICENSE' } 14 | s.author = { 'Your Company' => 'email@example.com' } 15 | s.source = { :path => '.' } 16 | s.source_files = 'live_activities/Sources/live_activities/**/*' 17 | s.dependency 'Flutter' 18 | s.platform = :ios, '13.0' 19 | 20 | # Flutter.framework does not contain a i386 slice. 21 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } 22 | s.swift_version = '5.0' 23 | 24 | # If your plugin requires a privacy manifest, for example if it uses any 25 | # required reason APIs, update the PrivacyInfo.xcprivacy file to describe your 26 | # plugin's privacy impact, and then uncomment this line. For more information, 27 | # see https://developer.apple.com/documentation/bundleresources/privacy_manifest_files 28 | # s.resource_bundles = {'live_activities_privacy' => ['live_activities/Sources/live_activities/PrivacyInfo.xcprivacy']} 29 | end 30 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: live_activities 2 | description: Support iOS Live Activities, Android RemoteViews and iPhone's Dynamic Island. 3 | version: 2.4.3 4 | homepage: https://dimitridessus.fr/ 5 | repository: https://github.com/istornz/live_activities 6 | 7 | environment: 8 | sdk: ^3.10.0 9 | flutter: ">=3.38.0" 10 | 11 | screenshots: 12 | - description: 'Display a live activity on the iPhone 14 Pro+ dynamic island.' 13 | path: images/showcase/static/dynamic_island.webp 14 | - description: 'Display a live activity on the lockscreen.' 15 | path: images/showcase/static/lockscreen_live_activity.webp 16 | - description: 'Create a new live activity with the live_activities plugin.' 17 | path: images/showcase/animations/create_live_activity.webp 18 | - description: 'Update a live activity with the live_activities plugin.' 19 | path: images/showcase/animations/update_live_activity.webp 20 | - description: 'live_activities plugin logo.' 21 | path: images/logo.webp 22 | 23 | dependencies: 24 | flutter: 25 | sdk: flutter 26 | plugin_platform_interface: ^2.1.8 27 | flutter_app_group_directory: ^1.1.0 28 | path_provider: ^2.1.5 29 | image: ^4.5.4 30 | 31 | dev_dependencies: 32 | flutter_test: 33 | sdk: flutter 34 | flutter_lints: ^6.0.0 35 | 36 | flutter: 37 | plugin: 38 | platforms: 39 | ios: 40 | pluginClass: LiveActivitiesPlugin 41 | android: 42 | package: com.example.live_activities 43 | pluginClass: LiveActivitiesPlugin 44 | -------------------------------------------------------------------------------- /lib/services/image_file_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:ui'; 3 | 4 | import 'package:image/image.dart' as img; 5 | 6 | /// Service to handle all processes to image files 7 | class ImageFileService { 8 | /// Resize the image to a specific factor 9 | Future resizeImage(File file, num resizeFactor) async { 10 | final bytes = file.readAsBytesSync(); 11 | final buffer = await ImmutableBuffer.fromUint8List(bytes); 12 | final descriptor = await ImageDescriptor.encoded(buffer); 13 | final imageWidth = descriptor.width; 14 | 15 | assert( 16 | imageWidth > 0, 17 | 'Please make sure you are using an image that is not corrupt or too small', 18 | ); 19 | 20 | final targetWidth = (imageWidth * resizeFactor).round(); 21 | return _compressImage(file, targetWidth); 22 | } 23 | 24 | Future _compressImage(File file, int targetWidth) async { 25 | final bytes = await file.readAsBytes(); 26 | final image = img.decodeImage(bytes); 27 | 28 | if (image == null) { 29 | throw Exception("Invalid image file"); 30 | } 31 | 32 | final imageWidth = image.width; 33 | final imageHeight = image.height; 34 | final targetHeight = (imageHeight * targetWidth / imageWidth).round(); 35 | 36 | final resizedImage = img.copyResize( 37 | image, 38 | width: targetWidth, 39 | height: targetHeight, 40 | ); 41 | 42 | final compressedBytes = img.encodeJpg(resizedImage, quality: 85); 43 | return File(file.path)..writeAsBytesSync(compressedBytes); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 5 | id "dev.flutter.flutter-gradle-plugin" 6 | } 7 | 8 | android { 9 | namespace = "com.example.live_activities_example" 10 | compileSdk = flutter.compileSdkVersion 11 | ndkVersion = flutter.ndkVersion 12 | 13 | compileOptions { 14 | sourceCompatibility = JavaVersion.VERSION_11 15 | targetCompatibility = JavaVersion.VERSION_11 16 | } 17 | 18 | kotlinOptions { 19 | jvmTarget = JavaVersion.VERSION_11 20 | } 21 | 22 | defaultConfig { 23 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 24 | applicationId = "com.example.live_activities_example" 25 | // You can update the following values to match your application needs. 26 | // For more information, see: https://flutter.dev/to/review-gradle-config. 27 | minSdk = 24 28 | targetSdk = flutter.targetSdkVersion 29 | versionCode = flutter.versionCode 30 | versionName = flutter.versionName 31 | } 32 | 33 | buildTypes { 34 | release { 35 | // TODO: Add your own signing config for the release build. 36 | // Signing with the debug keys for now, so `flutter run --release` works. 37 | signingConfig = signingConfigs.debug 38 | } 39 | } 40 | } 41 | 42 | flutter { 43 | source = "../.." 44 | } 45 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '13.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | 33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_ios_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | formatter: 13 | page_width: 80 14 | linter: 15 | # The lint rules applied to this project can be customized in the 16 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 17 | # included above or to enable additional rules. A list of all available lints 18 | # and their documentation is published at 19 | # https://dart-lang.github.io/linter/lints/index.html. 20 | # 21 | # Instead of disabling a lint rule for the entire project in the 22 | # section below, it can also be suppressed for a single line of code 23 | # or a specific dart file by using the `// ignore: name_of_lint` and 24 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 25 | # producing the lint. 26 | rules: 27 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 28 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 29 | 30 | # Additional information about this file can be found at 31 | # https://dart.dev/guides/language/analysis-options 32 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group = "com.example.live_activities" 2 | version = "1.0-SNAPSHOT" 3 | 4 | buildscript { 5 | ext.kotlin_version = "1.8.22" 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath("com.android.tools.build:gradle:8.1.0") 13 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") 14 | } 15 | } 16 | 17 | allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: "com.android.library" 25 | apply plugin: "kotlin-android" 26 | 27 | android { 28 | namespace = "com.example.live_activities" 29 | 30 | compileSdk = 35 31 | 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_11 34 | targetCompatibility = JavaVersion.VERSION_11 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = JavaVersion.VERSION_11 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += "src/main/kotlin" 43 | test.java.srcDirs += "src/test/kotlin" 44 | } 45 | 46 | defaultConfig { 47 | minSdk = flutter.minSdkVersion 48 | } 49 | 50 | dependencies { 51 | implementation 'com.google.firebase:firebase-messaging:24.0.0' 52 | testImplementation("org.jetbrains.kotlin:kotlin-test") 53 | testImplementation("org.mockito:mockito-core:5.0.0") 54 | } 55 | 56 | testOptions { 57 | unitTests.all { 58 | useJUnitPlatform() 59 | 60 | testLogging { 61 | events "passed", "skipped", "failed", "standardOut", "standardError" 62 | outputs.upToDateWhen {false} 63 | showStandardStreams = true 64 | } 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/live_activities_method_channel_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:live_activities/live_activities_method_channel.dart'; 4 | 5 | void main() { 6 | MethodChannelLiveActivities platform = MethodChannelLiveActivities(); 7 | const MethodChannel channel = MethodChannel('live_activities'); 8 | 9 | TestWidgetsFlutterBinding.ensureInitialized(); 10 | 11 | setUp(() { 12 | handler(MethodCall methodCall) async { 13 | switch (methodCall.method) { 14 | case 'createActivity': 15 | return 'ACTIVITY_ID'; 16 | case 'getAllActivitiesIds': 17 | return ['ACTIVITY_ID']; 18 | case 'getActivityState': 19 | return 'dismissed'; 20 | default: 21 | } 22 | return null; 23 | } 24 | 25 | TestWidgetsFlutterBinding.ensureInitialized(); 26 | 27 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 28 | .setMockMethodCallHandler(channel, handler); 29 | }); 30 | 31 | tearDown(() { 32 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 33 | .setMockMethodCallHandler(channel, null); 34 | }); 35 | 36 | test('createActivity', () async { 37 | expect(await platform.createActivity('ACTIVITY_ID', {}), 'ACTIVITY_ID'); 38 | }); 39 | 40 | test('updateActivity', () async { 41 | expect(await platform.updateActivity('ACTIVITY_ID', {}), null); 42 | }); 43 | 44 | test('endActivity', () async { 45 | expect(await platform.endActivity('ACTIVITY_ID'), null); 46 | }); 47 | 48 | test('endAllActivities', () async { 49 | expect(await platform.endAllActivities(), null); 50 | }); 51 | 52 | test('init', () async { 53 | expect(await platform.init('APP_GROUP_ID', urlScheme: 'URL_SCHEME'), null); 54 | }); 55 | 56 | test('getAllActivities', () async { 57 | expect(await platform.getAllActivitiesIds(), ['ACTIVITY_ID']); 58 | }); 59 | 60 | test('areActivitiesSupported', () async { 61 | expect(await platform.areActivitiesSupported(), false); 62 | }); 63 | 64 | test('areActivitiesEnabled', () async { 65 | expect(await platform.areActivitiesEnabled(), false); 66 | }); 67 | 68 | test('getActivityState', () async { 69 | expect(await platform.getActivityState('ACTIVITY_ID'), null); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/example/live_activities/LiveActivityFirebaseMessagingService.kt: -------------------------------------------------------------------------------- 1 | package com.example.live_activities 2 | 3 | import android.os.Build 4 | import android.util.Log 5 | import com.google.firebase.messaging.FirebaseMessagingService 6 | import com.google.firebase.messaging.RemoteMessage 7 | import kotlinx.coroutines.CoroutineScope 8 | import kotlinx.coroutines.Dispatchers 9 | import kotlinx.coroutines.launch 10 | import org.json.JSONObject 11 | 12 | class LiveActivityFirebaseMessagingService : FirebaseMessagingService() { 13 | 14 | private fun jsonDecode(json: String): Map { 15 | val jsonObject = JSONObject(json) 16 | val map = mutableMapOf() 17 | jsonObject.keys().forEach { key -> 18 | map[key] = jsonObject.get(key) 19 | } 20 | return map 21 | } 22 | 23 | override fun onMessageReceived(remoteMessage: RemoteMessage) { 24 | super.onMessageReceived(remoteMessage) 25 | 26 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return 27 | 28 | val liveActivityManager = LiveActivityManagerHolder.instance!! 29 | 30 | CoroutineScope(Dispatchers.IO).launch { 31 | try { 32 | val args = remoteMessage.data 33 | val event = args["event"] as String 34 | val data = jsonDecode(args["content-state"] ?: "{}") 35 | val id = args["activity-id"] as String 36 | val timestamp = 37 | (args["timestamp"] as? String)?.toLongOrNull() ?: 0L 38 | 39 | when (event) { 40 | "update" -> { 41 | liveActivityManager.updateActivity(id, timestamp, data) 42 | } 43 | 44 | "end" -> { 45 | liveActivityManager.endActivity(id, data) 46 | } 47 | 48 | else -> { 49 | throw IllegalArgumentException("Unknown event type: $event") 50 | } 51 | } 52 | 53 | } catch (e: Exception) { 54 | Log.e( 55 | "LiveActivityFirebaseMessagingService", 56 | "Error while parsing or processing FCM", 57 | e 58 | ) 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /example/lib/widgets/score_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class ScoreWidget extends StatelessWidget { 4 | final String teamName; 5 | final int score; 6 | final Function(int) onScoreChanged; 7 | const ScoreWidget({ 8 | super.key, 9 | required this.teamName, 10 | required this.score, 11 | required this.onScoreChanged, 12 | }); 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Column( 17 | mainAxisAlignment: MainAxisAlignment.center, 18 | children: [ 19 | Text( 20 | teamName, 21 | textAlign: TextAlign.center, 22 | style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 17), 23 | ), 24 | const SizedBox(height: 15), 25 | Row( 26 | mainAxisAlignment: MainAxisAlignment.center, 27 | children: [ 28 | Container( 29 | decoration: const BoxDecoration( 30 | color: Colors.red, 31 | shape: BoxShape.circle, 32 | ), 33 | width: 35, 34 | height: 35, 35 | child: IconButton( 36 | iconSize: 18, 37 | icon: const Icon( 38 | Icons.remove_rounded, 39 | color: Colors.white, 40 | ), 41 | onPressed: () => onScoreChanged(score - 1), 42 | ), 43 | ), 44 | const SizedBox(width: 10), 45 | Text( 46 | score.toString(), 47 | textAlign: TextAlign.center, 48 | style: const TextStyle( 49 | fontSize: 26, 50 | fontWeight: FontWeight.bold, 51 | ), 52 | ), 53 | const SizedBox(width: 10), 54 | Container( 55 | decoration: const BoxDecoration( 56 | color: Colors.green, 57 | shape: BoxShape.circle, 58 | ), 59 | width: 35, 60 | height: 35, 61 | child: IconButton( 62 | iconSize: 16, 63 | icon: const Icon( 64 | Icons.add_rounded, 65 | color: Colors.white, 66 | ), 67 | onPressed: () => onScoreChanged(score + 1), 68 | ), 69 | ), 70 | ], 71 | ), 72 | ], 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Live Activities Example 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | live_activities_example 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleVersion 26 | $(FLUTTER_BUILD_NUMBER) 27 | LSRequiresIPhoneOS 28 | 29 | NSSupportsLiveActivities 30 | 31 | UIApplicationSceneManifest 32 | 33 | UIApplicationSupportsMultipleScenes 34 | 35 | UISceneConfigurations 36 | 37 | UIWindowSceneSessionRoleApplication 38 | 39 | 40 | UISceneClassName 41 | UIWindowScene 42 | UISceneConfigurationName 43 | flutter 44 | UISceneDelegateClassName 45 | FlutterSceneDelegate 46 | UISceneStoryboardFile 47 | Main 48 | 49 | 50 | 51 | 52 | UIApplicationSupportsIndirectInputEvents 53 | 54 | UILaunchStoryboardName 55 | LaunchScreen 56 | UIMainStoryboardFile 57 | Main 58 | UISupportedInterfaceOrientations 59 | 60 | UIInterfaceOrientationPortrait 61 | UIInterfaceOrientationLandscapeLeft 62 | UIInterfaceOrientationLandscapeRight 63 | 64 | UISupportedInterfaceOrientations~ipad 65 | 66 | UIInterfaceOrientationPortrait 67 | UIInterfaceOrientationPortraitUpsideDown 68 | UIInterfaceOrientationLandscapeLeft 69 | UIInterfaceOrientationLandscapeRight 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /example/lib/models/football_game_live_activity_model.dart: -------------------------------------------------------------------------------- 1 | import 'package:live_activities/models/live_activity_file.dart'; 2 | 3 | class FootballGameLiveActivityModel { 4 | final DateTime? matchStartDate; 5 | final DateTime? matchEndDate; 6 | final String? matchName; 7 | final LiveActivityFileFromAsset? ruleFile; 8 | 9 | final String? teamAName; 10 | final String? teamAState; 11 | final int teamAScore; 12 | final LiveActivityFileFromAsset? teamALogo; 13 | 14 | final String? teamBName; 15 | final String? teamBState; 16 | final int teamBScore; 17 | final LiveActivityFileFromAsset? teamBLogo; 18 | 19 | const FootballGameLiveActivityModel({ 20 | this.teamAName, 21 | this.matchName, 22 | this.teamAState, 23 | this.ruleFile, 24 | this.teamAScore = 0, 25 | this.teamBScore = 0, 26 | this.teamALogo, 27 | this.teamBName, 28 | this.teamBState, 29 | this.teamBLogo, 30 | this.matchEndDate, 31 | this.matchStartDate, 32 | }); 33 | 34 | Map toMap() { 35 | final map = { 36 | 'matchName': matchName, 37 | 'ruleFile': ruleFile, 38 | 'teamAName': teamAName, 39 | 'teamAState': teamAState, 40 | 'teamALogo': teamALogo, 41 | 'teamAScore': teamAScore, 42 | 'teamBScore': teamBScore, 43 | 'teamBName': teamBName, 44 | 'teamBState': teamBState, 45 | 'teamBLogo': teamBLogo, 46 | 'matchStartDate': matchStartDate?.millisecondsSinceEpoch, 47 | 'matchEndDate': matchEndDate?.millisecondsSinceEpoch, 48 | }; 49 | 50 | return map; 51 | } 52 | 53 | FootballGameLiveActivityModel copyWith({ 54 | String? activityId, 55 | DateTime? matchStartDate, 56 | DateTime? matchEndDate, 57 | LiveActivityFileFromAsset? ruleFile, 58 | String? matchName, 59 | String? teamAName, 60 | String? teamAState, 61 | int? teamAScore, 62 | LiveActivityFileFromAsset? teamALogo, 63 | String? teamBName, 64 | String? teamBState, 65 | int? teamBScore, 66 | LiveActivityFileFromAsset? teamBLogo, 67 | }) { 68 | return FootballGameLiveActivityModel( 69 | ruleFile: ruleFile ?? this.ruleFile, 70 | matchStartDate: matchStartDate ?? this.matchStartDate, 71 | matchEndDate: matchEndDate ?? this.matchEndDate, 72 | matchName: matchName ?? this.matchName, 73 | teamAName: teamAName ?? this.teamAName, 74 | teamAState: teamAState ?? this.teamAState, 75 | teamAScore: teamAScore ?? this.teamAScore, 76 | teamALogo: teamALogo ?? this.teamALogo, 77 | teamBName: teamBName ?? this.teamBName, 78 | teamBState: teamBState ?? this.teamBState, 79 | teamBScore: teamBScore ?? this.teamBScore, 80 | teamBLogo: teamBLogo ?? this.teamBLogo, 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /lib/models/live_activity_file.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/services.dart'; 2 | 3 | class LiveActivityImageFileOptions { 4 | num? resizeFactor; 5 | 6 | LiveActivityImageFileOptions({this.resizeFactor}); 7 | } 8 | 9 | abstract class LiveActivityFile { 10 | final LiveActivityImageFileOptions? imageOptions; 11 | 12 | LiveActivityFile(this.imageOptions); 13 | 14 | /// Load the image. 15 | Future loadFile(); 16 | String get fileName; 17 | } 18 | 19 | class LiveActivityFileFromUrl extends LiveActivityFile { 20 | final String url; 21 | 22 | LiveActivityFileFromUrl._( 23 | this.url, 24 | LiveActivityImageFileOptions? imageOptions, 25 | ) : super(imageOptions); 26 | 27 | factory LiveActivityFileFromUrl(String url) { 28 | return LiveActivityFileFromUrl._(url, null); 29 | } 30 | 31 | factory LiveActivityFileFromUrl.image( 32 | String url, { 33 | LiveActivityImageFileOptions? imageOptions, 34 | }) { 35 | return LiveActivityFileFromUrl._(url, imageOptions); 36 | } 37 | 38 | @override 39 | Future loadFile() async { 40 | final ByteData byteData = await NetworkAssetBundle(Uri.parse(url)).load(''); 41 | return byteData.buffer.asUint8List(); 42 | } 43 | 44 | @override 45 | String get fileName => url.split('/').last; 46 | } 47 | 48 | class LiveActivityFileFromAsset extends LiveActivityFile { 49 | final String path; 50 | 51 | LiveActivityFileFromAsset._(this.path, LiveActivityImageFileOptions? options) 52 | : super(options); 53 | 54 | factory LiveActivityFileFromAsset(String path) { 55 | return LiveActivityFileFromAsset._(path, null); 56 | } 57 | 58 | factory LiveActivityFileFromAsset.image( 59 | String path, { 60 | LiveActivityImageFileOptions? imageOptions, 61 | }) { 62 | return LiveActivityFileFromAsset._(path, imageOptions); 63 | } 64 | 65 | @override 66 | Future loadFile() async { 67 | final byteData = await rootBundle.load(path); 68 | return byteData.buffer.asUint8List(); 69 | } 70 | 71 | @override 72 | String get fileName => path.split('/').last; 73 | } 74 | 75 | class LiveActivityFileFromMemory extends LiveActivityFile { 76 | final Uint8List data; 77 | final String imageName; 78 | 79 | LiveActivityFileFromMemory._( 80 | this.data, 81 | this.imageName, 82 | LiveActivityImageFileOptions? imageOptions, 83 | ) : super(imageOptions); 84 | 85 | factory LiveActivityFileFromMemory(Uint8List data, String imageName) { 86 | return LiveActivityFileFromMemory._(data, imageName, null); 87 | } 88 | 89 | factory LiveActivityFileFromMemory.image( 90 | Uint8List data, 91 | String imageName, { 92 | LiveActivityImageFileOptions? imageOptions, 93 | }) { 94 | return LiveActivityFileFromMemory._(data, imageName, imageOptions); 95 | } 96 | 97 | @override 98 | Future loadFile() { 99 | return Future.value(data); 100 | } 101 | 102 | @override 103 | String get fileName => imageName; 104 | } 105 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /lib/services/app_groups_file_service.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:flutter_app_group_directory/flutter_app_group_directory.dart'; 4 | import 'package:live_activities/models/live_activity_file.dart'; 5 | import 'package:live_activities/services/image_file_service.dart'; 6 | import 'package:path_provider/path_provider.dart'; 7 | 8 | /// Folder name to store files in the app groups directory 9 | const kFileFolderName = 'LiveActivitiesFiles'; 10 | 11 | /// Service to handle all processes to files that are sent to the app groups 12 | class AppGroupsFileService { 13 | final _imageService = ImageFileService(); 14 | 15 | String? _appGroupId; 16 | final List _assetsCopiedInAppGroups = []; 17 | 18 | /// Initialize the service with the app group id 19 | void init({required String appGroupId}) { 20 | _appGroupId = appGroupId; 21 | } 22 | 23 | /// Send files to the app groups directory 24 | Future sendFilesToAppGroups(Map data) async { 25 | if (_appGroupId == null) { 26 | throw Exception('appGroupId is null. Please call init() first.'); 27 | } 28 | 29 | for (String key in data.keys) { 30 | final value = data[key]; 31 | 32 | if (value is! LiveActivityFile) { 33 | continue; 34 | } 35 | 36 | Directory appGroupFiles = await _liveActivitiesFilesDirectory(); 37 | Directory tempDir = await getTemporaryDirectory(); 38 | 39 | // create directory if not exists 40 | appGroupFiles.createSync(); 41 | 42 | final bytes = await value.loadFile(); 43 | File file = await File('${tempDir.path}/${value.fileName}').create() 44 | ..writeAsBytesSync(bytes); 45 | 46 | if (value.imageOptions != null) { 47 | await _processImageFileOperations(file, value.imageOptions!); 48 | } 49 | 50 | final finalDestination = '${appGroupFiles.path}/${value.fileName}'; 51 | file.copySync(finalDestination); 52 | 53 | data[key] = finalDestination; 54 | _assetsCopiedInAppGroups.add(finalDestination); 55 | 56 | // remove file from temp directory 57 | file.deleteSync(); 58 | } 59 | } 60 | 61 | /// Remove all files from the app group directory 62 | Future removeAllFiles() async { 63 | final laFilesDir = await _liveActivitiesFilesDirectory(); 64 | laFilesDir.deleteSync(recursive: true); 65 | } 66 | 67 | /// Remove all files that were copied to the app groups in this session 68 | Future removeFilesSession() async { 69 | for (String filePath in _assetsCopiedInAppGroups) { 70 | final file = File(filePath); 71 | await file.delete(); 72 | } 73 | } 74 | 75 | Future _processImageFileOperations( 76 | File file, 77 | LiveActivityImageFileOptions imageOptions, 78 | ) async { 79 | if (imageOptions.resizeFactor != null && imageOptions.resizeFactor != 1) { 80 | file = await _imageService.resizeImage(file, imageOptions.resizeFactor!); 81 | } 82 | } 83 | 84 | Future _liveActivitiesFilesDirectory() async { 85 | final appGroupDirectory = 86 | await FlutterAppGroupDirectory.getAppGroupDirectory(_appGroupId!); 87 | return Directory('${appGroupDirectory!.path}/$kFileFolderName'); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: live_activities_example 2 | description: Demonstrates how to use the live_activities plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: '>=3.0.6 <4.0.0' 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | live_activities: 22 | # When depending on this package from a real application you should use: 23 | # live_activities: ^x.y.z 24 | # See https://dart.dev/tools/pub/dependencies#version-constraints 25 | # The example app is bundled with the plugin so we use a path dependency on 26 | # the parent directory to use the current plugin's version. 27 | path: ../ 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.8 32 | permission_handler: ^12.0.0+1 33 | 34 | dev_dependencies: 35 | flutter_test: 36 | sdk: flutter 37 | 38 | # The "flutter_lints" package below contains a set of recommended lints to 39 | # encourage good coding practices. The lint set provided by the package is 40 | # activated in the `analysis_options.yaml` file located at the root of your 41 | # package. See that file for information about deactivating specific lint 42 | # rules and activating additional ones. 43 | flutter_lints: ^5.0.0 44 | 45 | # For information on the generic Dart part of this file, see the 46 | # following page: https://dart.dev/tools/pub/pubspec 47 | 48 | # The following section is specific to Flutter packages. 49 | flutter: 50 | 51 | # The following line ensures that the Material Icons font is 52 | # included with your application, so that you can use the icons in 53 | # the material Icons class. 54 | uses-material-design: true 55 | 56 | # To add assets to your application, add an assets section, like this: 57 | assets: 58 | - assets/images/psg.png 59 | - assets/images/chelsea.png 60 | - assets/files/rules.txt 61 | 62 | # An image asset can refer to one or more resolution-specific "variants", see 63 | # https://flutter.dev/assets-and-images/#resolution-aware 64 | 65 | # For details regarding adding assets from package dependencies, see 66 | # https://flutter.dev/assets-and-images/#from-packages 67 | 68 | # To add custom fonts to your application, add a fonts section here, 69 | # in this "flutter" section. Each entry in this list should have a 70 | # "family" key with the font family name, and a "fonts" key with a 71 | # list giving the asset and other descriptors for the font. For 72 | # example: 73 | # fonts: 74 | # - family: Schyler 75 | # fonts: 76 | # - asset: fonts/Schyler-Regular.ttf 77 | # - asset: fonts/Schyler-Italic.ttf 78 | # style: italic 79 | # - family: Trajan Pro 80 | # fonts: 81 | # - asset: fonts/TrajanPro.ttf 82 | # - asset: fonts/TrajanPro_Bold.ttf 83 | # weight: 700 84 | # 85 | # For details regarding fonts from package dependencies, 86 | # see https://flutter.dev/custom-fonts/#from-packages 87 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/example/live_activities/LiveActivitiesPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.example.live_activities 2 | 3 | import android.content.Context 4 | import io.flutter.embedding.engine.plugins.FlutterPlugin 5 | import io.flutter.plugin.common.MethodCall 6 | import io.flutter.plugin.common.MethodChannel 7 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 8 | import io.flutter.plugin.common.MethodChannel.Result 9 | import kotlinx.coroutines.CoroutineScope 10 | import kotlinx.coroutines.Dispatchers 11 | import kotlinx.coroutines.SupervisorJob 12 | import kotlinx.coroutines.launch 13 | 14 | class LiveActivitiesPlugin : FlutterPlugin, MethodCallHandler { 15 | 16 | private lateinit var channel: MethodChannel 17 | 18 | private val pluginScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) 19 | 20 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 21 | channel = MethodChannel( 22 | flutterPluginBinding.binaryMessenger, "live_activities" 23 | ) 24 | channel.setMethodCallHandler(this) 25 | } 26 | 27 | override fun onMethodCall(call: MethodCall, result: Result) { 28 | try { 29 | val args = call.arguments as? Map ?: emptyMap() 30 | val data = args["data"] as? Map ?: emptyMap() 31 | val liveActivityManager = LiveActivityManagerHolder.instance!! 32 | 33 | when (call.method) { 34 | "init" -> { 35 | liveActivityManager.initialize(data) 36 | result.success("initialized") 37 | } 38 | 39 | "createActivity" -> { 40 | val timestamp = System.currentTimeMillis() 41 | val id = args["activityId"] as String 42 | 43 | pluginScope.launch { 44 | val notificationId = liveActivityManager.createActivity( 45 | id, 46 | timestamp, 47 | data 48 | ) 49 | result.success(notificationId) 50 | } 51 | } 52 | 53 | "updateActivity" -> { 54 | val timestamp = System.currentTimeMillis() 55 | val id = args["activityId"] as String 56 | 57 | pluginScope.launch { 58 | liveActivityManager.updateActivity(id, timestamp, data) 59 | result.success("activity/updated") 60 | } 61 | } 62 | 63 | "endActivity" -> { 64 | val id = args["activityId"] as String 65 | 66 | liveActivityManager.endActivity(id, data) 67 | result.success("activity/ended") 68 | } 69 | 70 | "endAllActivities" -> { 71 | liveActivityManager.endAllActivities(data) 72 | result.success("activities/ended") 73 | } 74 | 75 | "getAllActivitiesIds" -> { 76 | val ids = liveActivityManager.getAllActivitiesIds(data) 77 | result.success(ids) 78 | } 79 | 80 | "areActivitiesSupported" -> { 81 | val supported = liveActivityManager.areActivitiesSupported(data) 82 | result.success(supported) 83 | } 84 | 85 | "areActivitiesEnabled" -> { 86 | val enabled = liveActivityManager.areActivitiesEnabled(data) 87 | result.success(enabled) 88 | } 89 | 90 | else -> { 91 | result.notImplemented() 92 | } 93 | } 94 | } catch (e: Exception) { 95 | result.error("ERROR", e.message ?: "Unknown error", null) 96 | } 97 | } 98 | 99 | 100 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 101 | channel.setMethodCallHandler(null) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/layout/live_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 13 | 14 | 15 | 21 | 22 | 28 | 29 | 39 | 40 | 41 | 42 | 49 | 50 | 58 | 59 | 67 | 68 | 69 | 70 | 76 | 77 | 83 | 84 | 94 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /lib/live_activities_platform_interface.dart: -------------------------------------------------------------------------------- 1 | import 'package:live_activities/live_activities_method_channel.dart'; 2 | import 'package:live_activities/models/activity_update.dart'; 3 | import 'package:live_activities/models/alert_config.dart'; 4 | import 'package:live_activities/models/live_activity_state.dart'; 5 | import 'package:live_activities/models/url_scheme_data.dart'; 6 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 7 | 8 | abstract class LiveActivitiesPlatform extends PlatformInterface { 9 | /// Constructs a LiveActivitiesPlatform. 10 | LiveActivitiesPlatform() : super(token: _token); 11 | 12 | static final Object _token = Object(); 13 | 14 | static LiveActivitiesPlatform _instance = MethodChannelLiveActivities(); 15 | 16 | /// The default instance of [LiveActivitiesPlatform] to use. 17 | /// 18 | /// Defaults to [MethodChannelLiveActivities]. 19 | static LiveActivitiesPlatform get instance => _instance; 20 | 21 | /// Platform-specific implementations should set this with their own 22 | /// platform-specific class that extends [LiveActivitiesPlatform] when 23 | /// they register themselves. 24 | static set instance(LiveActivitiesPlatform instance) { 25 | PlatformInterface.verifyToken(instance, _token); 26 | _instance = instance; 27 | } 28 | 29 | Future init( 30 | String appGroupId, { 31 | String? urlScheme, 32 | bool requireNotificationPermission = true, 33 | }) { 34 | throw UnimplementedError('init() has not been implemented.'); 35 | } 36 | 37 | Future createActivity( 38 | String activityId, 39 | Map data, { 40 | bool removeWhenAppIsKilled = false, 41 | Duration? staleIn, 42 | }) { 43 | throw UnimplementedError('createActivity() has not been implemented.'); 44 | } 45 | 46 | Future updateActivity( 47 | String activityId, 48 | Map data, [ 49 | AlertConfig? alertConfig, 50 | ]) { 51 | throw UnimplementedError('updateActivity() has not been implemented.'); 52 | } 53 | 54 | Future createOrUpdateActivity( 55 | String activityId, 56 | Map data, { 57 | bool removeWhenAppIsKilled = false, 58 | Duration? staleIn, 59 | }) { 60 | throw UnimplementedError( 61 | 'createOrUpdateActivity() has not been implemented.', 62 | ); 63 | } 64 | 65 | Future endActivity(String activityId) { 66 | throw UnimplementedError('endActivity() has not been implemented.'); 67 | } 68 | 69 | Future> getAllActivitiesIds() { 70 | throw UnimplementedError('getAllActivitiesIds() has not been implemented.'); 71 | } 72 | 73 | Future endAllActivities() { 74 | throw UnimplementedError('endAllActivities() has not been implemented.'); 75 | } 76 | 77 | Future> getAllActivities() { 78 | throw UnimplementedError('getAllActivities() has not been implemented.'); 79 | } 80 | 81 | /// Check if live activities are supported on this platform/OS version. 82 | Future areActivitiesSupported() { 83 | throw UnimplementedError( 84 | 'areActivitiesSupported() has not been implemented.', 85 | ); 86 | } 87 | 88 | /// Check if live activities are enabled by the user in their device settings. 89 | Future areActivitiesEnabled() { 90 | throw UnimplementedError( 91 | 'areActivitiesEnabled() has not been implemented.', 92 | ); 93 | } 94 | 95 | Future allowsPushStart() { 96 | throw UnimplementedError( 97 | 'supportsStartActivities() has not been implemented.', 98 | ); 99 | } 100 | 101 | Stream urlSchemeStream() { 102 | throw UnimplementedError('urlSchemeStream() has not been implemented.'); 103 | } 104 | 105 | Future getActivityState(String activityId) { 106 | throw UnimplementedError('getActivityState() has not been implemented.'); 107 | } 108 | 109 | Future getPushToken(String activityId) { 110 | throw UnimplementedError('getPushToken() has not been implemented.'); 111 | } 112 | 113 | Stream get activityUpdateStream => 114 | throw UnimplementedError('pushTokenUpdates has not been implemented'); 115 | 116 | Stream get pushToStartTokenUpdateStream => throw UnimplementedError( 117 | 'pushToStartTokenUpdateStream has not been implemented', 118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 11 | 14 | 15 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 39 | 40 | 41 | 42 | 43 | 49 | 50 | 56 | 57 | 58 | 59 | 62 | 68 | 69 | 70 | 71 | 72 | 84 | 86 | 92 | 93 | 94 | 95 | 101 | 103 | 109 | 110 | 111 | 112 | 114 | 115 | 118 | 119 | 120 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/live_activities_example/CustomLiveActivityManager.kt: -------------------------------------------------------------------------------- 1 | package com.example.live_activities_example 2 | 3 | 4 | import android.app.Notification 5 | import android.app.PendingIntent 6 | import android.content.Context 7 | import android.content.Intent 8 | import android.graphics.Bitmap 9 | import android.graphics.BitmapFactory 10 | import android.widget.RemoteViews 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.withContext 13 | import java.net.HttpURLConnection 14 | import java.net.URL 15 | import com.example.live_activities.LiveActivityManager 16 | 17 | class CustomLiveActivityManager(context: Context) : 18 | LiveActivityManager(context) { 19 | private val context: Context = context.applicationContext 20 | private val pendingIntent = PendingIntent.getActivity( 21 | context, 200, Intent(context, MainActivity::class.java).apply { 22 | flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT 23 | }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE 24 | ) 25 | 26 | private val remoteViews = RemoteViews( 27 | context.packageName, R.layout.live_activity 28 | ) 29 | 30 | suspend fun loadImageBitmap(imageUrl: String?): Bitmap? { 31 | val dp = context.resources.displayMetrics.density.toInt() 32 | return withContext(Dispatchers.IO) { 33 | if (imageUrl.isNullOrEmpty()) return@withContext null 34 | try { 35 | val url = URL(imageUrl) 36 | val connection = url.openConnection() as HttpURLConnection 37 | connection.doInput = true 38 | connection.connectTimeout = 3000 39 | connection.readTimeout = 3000 40 | connection.connect() 41 | connection.inputStream.use { inputStream -> 42 | val originalBitmap = BitmapFactory.decodeStream(inputStream) 43 | originalBitmap?.let { 44 | val targetSize = 64 * dp 45 | val aspectRatio = 46 | it.width.toFloat() / it.height.toFloat() 47 | val (targetWidth, targetHeight) = if (aspectRatio > 1) { 48 | targetSize to (targetSize / aspectRatio).toInt() 49 | } else { 50 | (targetSize * aspectRatio).toInt() to targetSize 51 | } 52 | Bitmap.createScaledBitmap( 53 | it, 54 | targetWidth, 55 | targetHeight, 56 | true 57 | ) 58 | } 59 | } 60 | } catch (e: Exception) { 61 | e.printStackTrace() 62 | null 63 | } 64 | } 65 | } 66 | 67 | private suspend fun updateRemoteViews( 68 | team1Name: String, 69 | team1Score: Int, 70 | team2Name: String, 71 | team2Score: Int, 72 | timestamp: Long, 73 | team1ImageUrl: String?, 74 | team2ImageUrl: String?, 75 | ) { 76 | remoteViews.setTextViewText(R.id.team1_name, team1Name) 77 | remoteViews.setTextViewText(R.id.team2_name, team2Name) 78 | remoteViews.setTextViewText(R.id.score, "$team1Score : $team2Score") 79 | 80 | val elapsedRealtime = android.os.SystemClock.elapsedRealtime() 81 | val currentTimeMillis = System.currentTimeMillis() 82 | val base = elapsedRealtime - (currentTimeMillis - timestamp) 83 | 84 | remoteViews.setChronometer(R.id.match_time, base, null, true) 85 | 86 | val team1Image = 87 | if (!team1ImageUrl.isNullOrEmpty()) loadImageBitmap( 88 | team1ImageUrl 89 | ) else null 90 | val team2Image = 91 | if (!team2ImageUrl.isNullOrEmpty()) loadImageBitmap( 92 | team2ImageUrl 93 | ) else null 94 | 95 | team1Image?.let { image -> 96 | remoteViews.setImageViewBitmap( 97 | R.id.team1_image_placeholder, 98 | image, 99 | ) 100 | } 101 | 102 | team2Image?.let { image -> 103 | remoteViews.setImageViewBitmap( 104 | R.id.team2_image_placeholder, 105 | image, 106 | ) 107 | } 108 | } 109 | 110 | 111 | override suspend fun buildNotification( 112 | notification: Notification.Builder, 113 | event: String, 114 | data: Map 115 | ): Notification { 116 | val matchName = data["matchName"] as String 117 | val timestamp = data["matchStartDate"] as Long 118 | val team1Name = data["teamAName"] as String 119 | val team1Score = data["teamAScore"] as Int 120 | val team2Name = data["teamBName"] as String 121 | val team2Score = data["teamBScore"] as Int 122 | 123 | /// If the event is "update", skip images as previous notification already downloaded them 124 | val team1ImageUrl = 125 | if (event == "update") null else data["teamAImageUrl"] as String? 126 | val team2ImageUrl = 127 | if (event == "update") null else data["teamBImageUrl"] as String? 128 | 129 | updateRemoteViews( 130 | team1Name, 131 | team1Score, 132 | team2Name, 133 | team2Score, 134 | timestamp, 135 | team1ImageUrl, 136 | team2ImageUrl, 137 | ) 138 | 139 | return notification 140 | .setSmallIcon(R.drawable.ic_notification) 141 | .setOngoing(true) 142 | .setContentTitle("$team1Name vs $team2Name") 143 | .setContentIntent(pendingIntent) 144 | .setContentText("$team1Score : $team2Score") 145 | .setStyle(Notification.DecoratedCustomViewStyle()) 146 | .setCustomContentView(remoteViews) 147 | .setCustomBigContentView(remoteViews) 148 | .setPriority(Notification.PRIORITY_LOW) 149 | .setCategory(Notification.CATEGORY_EVENT) 150 | .setVisibility(Notification.VISIBILITY_PUBLIC) 151 | .build() 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /lib/models/activity_update.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/cupertino.dart'; 2 | import 'package:live_activities/models/live_activity_state.dart'; 3 | 4 | /// Abstract class to represent an activity update 5 | abstract class ActivityUpdate { 6 | /// The id of the activity 7 | ActivityUpdate({required this.activityId}); 8 | 9 | /// The id of the activity 10 | final String activityId; 11 | 12 | /// Factory to create an activity update from a map 13 | factory ActivityUpdate.fromMap(Map map) { 14 | final status = LiveActivityState.values.byName(map['status']); 15 | final activityId = map['activityId'] as String; 16 | switch (status) { 17 | case LiveActivityState.active: 18 | return ActiveActivityUpdate( 19 | activityId: activityId, 20 | activityToken: map['token'] as String, 21 | ); 22 | case LiveActivityState.ended: 23 | case LiveActivityState.dismissed: 24 | return EndedActivityUpdate(activityId: activityId); 25 | case LiveActivityState.stale: 26 | return StaleActivityUpdate(activityId: activityId); 27 | case LiveActivityState.unknown: 28 | return UnknownActivityUpdate(activityId: activityId); 29 | } 30 | } 31 | 32 | /// Map the activity update to a specific type 33 | TResult map({ 34 | required TResult Function(ActiveActivityUpdate value) active, 35 | required TResult Function(EndedActivityUpdate value) ended, 36 | required TResult Function(StaleActivityUpdate value) stale, 37 | required TResult Function(UnknownActivityUpdate value) unknown, 38 | }); 39 | 40 | /// Map the activity update to a specific type or return null 41 | TResult? mapOrNull({ 42 | TResult Function(ActiveActivityUpdate value)? active, 43 | TResult Function(EndedActivityUpdate value)? ended, 44 | TResult Function(StaleActivityUpdate value)? stale, 45 | TResult Function(UnknownActivityUpdate value)? unknown, 46 | }); 47 | 48 | @override 49 | String toString() { 50 | return '$runtimeType(activityId: $activityId)'; 51 | } 52 | } 53 | 54 | /// Class to represent an active activity update 55 | class ActiveActivityUpdate extends ActivityUpdate { 56 | /// Constructor for an active activity update 57 | @visibleForTesting 58 | ActiveActivityUpdate({ 59 | required super.activityId, 60 | required this.activityToken, 61 | }); 62 | 63 | /// The token of the activity 64 | final String activityToken; 65 | 66 | @override 67 | map({ 68 | required TResult Function(ActiveActivityUpdate value) active, 69 | required TResult Function(EndedActivityUpdate value) ended, 70 | required TResult Function(StaleActivityUpdate value) stale, 71 | required TResult Function(UnknownActivityUpdate value) unknown, 72 | }) { 73 | return active(this); 74 | } 75 | 76 | @override 77 | String toString() { 78 | return '$runtimeType(activityId: $activityId, activityToken: $activityToken)'; 79 | } 80 | 81 | @override 82 | TResult? mapOrNull({ 83 | TResult Function(ActiveActivityUpdate value)? active, 84 | TResult Function(EndedActivityUpdate value)? ended, 85 | TResult Function(StaleActivityUpdate value)? stale, 86 | TResult Function(UnknownActivityUpdate value)? unknown, 87 | }) { 88 | return active?.call(this); 89 | } 90 | } 91 | 92 | /// Class to represent an ended activity update 93 | class EndedActivityUpdate extends ActivityUpdate { 94 | /// Constructor for an ended activity update 95 | @visibleForTesting 96 | EndedActivityUpdate({required super.activityId}); 97 | 98 | @override 99 | map({ 100 | required TResult Function(ActiveActivityUpdate value) active, 101 | required TResult Function(EndedActivityUpdate value) ended, 102 | required TResult Function(StaleActivityUpdate value) stale, 103 | required TResult Function(UnknownActivityUpdate value) unknown, 104 | }) { 105 | return ended(this); 106 | } 107 | 108 | @override 109 | TResult? mapOrNull({ 110 | TResult Function(ActiveActivityUpdate value)? active, 111 | TResult Function(EndedActivityUpdate value)? ended, 112 | TResult Function(StaleActivityUpdate value)? stale, 113 | TResult Function(UnknownActivityUpdate value)? unknown, 114 | }) { 115 | return ended?.call(this); 116 | } 117 | } 118 | 119 | /// Class to represent a stale activity update 120 | class StaleActivityUpdate extends ActivityUpdate { 121 | /// Constructor for a stale activity update 122 | @visibleForTesting 123 | StaleActivityUpdate({required super.activityId}); 124 | 125 | @override 126 | map({ 127 | required TResult Function(ActiveActivityUpdate value) active, 128 | required TResult Function(EndedActivityUpdate value) ended, 129 | required TResult Function(StaleActivityUpdate value) stale, 130 | required TResult Function(UnknownActivityUpdate value) unknown, 131 | }) { 132 | return stale(this); 133 | } 134 | 135 | @override 136 | TResult? mapOrNull({ 137 | TResult Function(ActiveActivityUpdate value)? active, 138 | TResult Function(EndedActivityUpdate value)? ended, 139 | TResult Function(StaleActivityUpdate value)? stale, 140 | TResult Function(UnknownActivityUpdate value)? unknown, 141 | }) { 142 | return stale?.call(this); 143 | } 144 | } 145 | 146 | /// Class to represent an unknown activity update 147 | class UnknownActivityUpdate extends ActivityUpdate { 148 | /// Constructor for an unknown activity update 149 | @visibleForTesting 150 | UnknownActivityUpdate({required super.activityId}); 151 | 152 | @override 153 | map({ 154 | required TResult Function(ActiveActivityUpdate value) active, 155 | required TResult Function(EndedActivityUpdate value) ended, 156 | required TResult Function(StaleActivityUpdate value) stale, 157 | required TResult Function(UnknownActivityUpdate value) unknown, 158 | }) { 159 | return unknown(this); 160 | } 161 | 162 | @override 163 | TResult? mapOrNull({ 164 | TResult Function(ActiveActivityUpdate value)? active, 165 | TResult Function(EndedActivityUpdate value)? ended, 166 | TResult Function(StaleActivityUpdate value)? stale, 167 | TResult Function(UnknownActivityUpdate value)? unknown, 168 | }) { 169 | return unknown?.call(this); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /lib/live_activities_method_channel.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:live_activities/live_activities_platform_interface.dart'; 6 | import 'package:live_activities/models/activity_update.dart'; 7 | import 'package:live_activities/models/alert_config.dart'; 8 | import 'package:live_activities/models/live_activity_state.dart'; 9 | import 'package:live_activities/models/url_scheme_data.dart'; 10 | 11 | /// An implementation of [LiveActivitiesPlatform] that uses method channels. 12 | class MethodChannelLiveActivities extends LiveActivitiesPlatform { 13 | /// The method channel used to interact with the native platform. 14 | @visibleForTesting 15 | final methodChannel = const MethodChannel('live_activities'); 16 | 17 | @visibleForTesting 18 | final activityStatusChannel = const EventChannel( 19 | 'live_activities/activity_status', 20 | ); 21 | 22 | @visibleForTesting 23 | final EventChannel urlSchemeChannel = const EventChannel( 24 | 'live_activities/url_scheme', 25 | ); 26 | 27 | @visibleForTesting 28 | final EventChannel pushToStartTokenUpdatesChannel = const EventChannel( 29 | 'live_activities/push_to_start_token_updates', 30 | ); 31 | 32 | @override 33 | Future init( 34 | String appGroupId, { 35 | String? urlScheme, 36 | bool requireNotificationPermission = true, 37 | }) async { 38 | await methodChannel.invokeMethod('init', { 39 | 'appGroupId': appGroupId, 40 | 'urlScheme': urlScheme, 41 | 'requireNotificationPermission': requireNotificationPermission, 42 | }); 43 | } 44 | 45 | @override 46 | Future createActivity( 47 | String activityId, 48 | Map data, { 49 | bool removeWhenAppIsKilled = false, 50 | Duration? staleIn, 51 | }) async { 52 | // If the duration is less than 1 minute then pass a null value instead of using 0 minutes 53 | final staleInMinutes = (staleIn?.inMinutes ?? 0) >= 1 54 | ? staleIn?.inMinutes 55 | : null; 56 | return methodChannel.invokeMethod('createActivity', { 57 | 'activityId': activityId, 58 | 'data': data, 59 | 'removeWhenAppIsKilled': removeWhenAppIsKilled, 60 | 'staleIn': staleInMinutes, 61 | }); 62 | } 63 | 64 | @override 65 | Future updateActivity( 66 | String activityId, 67 | Map data, [ 68 | AlertConfig? alertConfig, 69 | ]) async { 70 | return methodChannel.invokeMethod('updateActivity', { 71 | 'activityId': activityId, 72 | 'data': data, 73 | 'alertConfig': alertConfig?.toMap(), 74 | }); 75 | } 76 | 77 | @override 78 | Future createOrUpdateActivity( 79 | String activityId, 80 | Map data, { 81 | bool removeWhenAppIsKilled = false, 82 | Duration? staleIn, 83 | }) async { 84 | final staleInMinutes = (staleIn?.inMinutes ?? 0) >= 1 85 | ? staleIn?.inMinutes 86 | : null; 87 | return methodChannel.invokeMethod('createOrUpdateActivity', { 88 | 'activityId': activityId, 89 | 'data': data, 90 | 'removeWhenAppIsKilled': removeWhenAppIsKilled, 91 | 'staleIn': staleInMinutes, 92 | }); 93 | } 94 | 95 | @override 96 | Future endActivity(String activityId) async { 97 | return methodChannel.invokeMethod('endActivity', { 98 | 'activityId': activityId, 99 | }); 100 | } 101 | 102 | @override 103 | Future endAllActivities() async { 104 | return methodChannel.invokeMethod('endAllActivities'); 105 | } 106 | 107 | @override 108 | Future> getAllActivitiesIds() async { 109 | final result = await methodChannel.invokeListMethod( 110 | 'getAllActivitiesIds', 111 | ); 112 | return result ?? []; 113 | } 114 | 115 | @override 116 | Future> getAllActivities() async { 117 | final result = await methodChannel.invokeMapMethod( 118 | 'getAllActivities', 119 | ); 120 | 121 | return result?.map( 122 | (key, value) => MapEntry(key, LiveActivityState.values.byName(value)), 123 | ) ?? 124 | {}; 125 | } 126 | 127 | @override 128 | Future areActivitiesSupported() async { 129 | final result = await methodChannel.invokeMethod( 130 | 'areActivitiesSupported', 131 | ); 132 | return result ?? false; 133 | } 134 | 135 | @override 136 | Future areActivitiesEnabled() async { 137 | final result = await methodChannel.invokeMethod( 138 | 'areActivitiesEnabled', 139 | ); 140 | return result ?? false; 141 | } 142 | 143 | @override 144 | Future allowsPushStart() async { 145 | if (defaultTargetPlatform != TargetPlatform.iOS) { 146 | return false; 147 | } 148 | 149 | final result = await methodChannel.invokeMethod('allowsPushStart'); 150 | return result ?? false; 151 | } 152 | 153 | @override 154 | Stream urlSchemeStream() { 155 | if (defaultTargetPlatform != TargetPlatform.iOS) { 156 | return Stream.empty(); 157 | } 158 | 159 | return urlSchemeChannel 160 | .receiveBroadcastStream('urlSchemeStream') 161 | .map( 162 | (dynamic event) => 163 | UrlSchemeData.fromMap(Map.from(event)), 164 | ); 165 | } 166 | 167 | @override 168 | Future getActivityState(String activityId) async { 169 | if (defaultTargetPlatform != TargetPlatform.iOS) { 170 | return null; 171 | } 172 | 173 | final result = await methodChannel.invokeMethod( 174 | 'getActivityState', 175 | {'activityId': activityId}, 176 | ); 177 | return result != null ? LiveActivityState.values.byName(result) : null; 178 | } 179 | 180 | @override 181 | Future getPushToken(String activityId) async { 182 | if (defaultTargetPlatform != TargetPlatform.iOS) { 183 | return null; 184 | } 185 | 186 | final result = await methodChannel.invokeMethod('getPushToken', { 187 | 'activityId': activityId, 188 | }); 189 | return result; 190 | } 191 | 192 | @override 193 | Stream get activityUpdateStream { 194 | if (defaultTargetPlatform != TargetPlatform.iOS) { 195 | return Stream.empty(); 196 | } 197 | 198 | return activityStatusChannel 199 | .receiveBroadcastStream('activityUpdateStream') 200 | .distinct() 201 | .map( 202 | (event) => ActivityUpdate.fromMap(Map.from(event)), 203 | ); 204 | } 205 | 206 | @override 207 | Stream get pushToStartTokenUpdateStream { 208 | if (defaultTargetPlatform != TargetPlatform.iOS) { 209 | return Stream.empty(); 210 | } 211 | 212 | return pushToStartTokenUpdatesChannel 213 | .receiveBroadcastStream('pushToStartTokenUpdateStream') 214 | .distinct() 215 | .cast(); 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /test/live_activities_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:live_activities/live_activities.dart'; 3 | import 'package:live_activities/live_activities_platform_interface.dart'; 4 | import 'package:live_activities/live_activities_method_channel.dart'; 5 | import 'package:live_activities/models/activity_update.dart'; 6 | import 'package:live_activities/models/alert_config.dart'; 7 | import 'package:live_activities/models/live_activity_state.dart'; 8 | import 'package:live_activities/models/url_scheme_data.dart'; 9 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 10 | 11 | class MockLiveActivitiesPlatform 12 | with MockPlatformInterfaceMixin 13 | implements LiveActivitiesPlatform { 14 | @override 15 | Future init( 16 | String appGroupId, { 17 | String? urlScheme, 18 | bool requireNotificationPermission = true, 19 | }) { 20 | return Future.value(); 21 | } 22 | 23 | @override 24 | Future createActivity( 25 | String activityId, 26 | Map data, { 27 | bool removeWhenAppIsKilled = false, 28 | Duration? staleIn, 29 | }) { 30 | return Future.value('ACTIVITY_ID'); 31 | } 32 | 33 | @override 34 | Future endActivity(String activityId) { 35 | return Future.value(); 36 | } 37 | 38 | @override 39 | Future areActivitiesSupported() { 40 | return Future.value(true); 41 | } 42 | 43 | @override 44 | Future areActivitiesEnabled() { 45 | return Future.value(true); 46 | } 47 | 48 | @override 49 | Future endAllActivities() { 50 | return Future.value(); 51 | } 52 | 53 | @override 54 | Future> getAllActivitiesIds() { 55 | return Future.value(['ACTIVITY_ID']); 56 | } 57 | 58 | @override 59 | Stream urlSchemeStream() { 60 | return Stream.value( 61 | UrlSchemeData( 62 | url: 'URL', 63 | scheme: 'SCHEME', 64 | host: 'HOST', 65 | path: 'PATH', 66 | queryParameters: [ 67 | {'name': 'NAME', 'value': 'VALUE'}, 68 | ], 69 | ), 70 | ); 71 | } 72 | 73 | @override 74 | Future getActivityState(String activityId) { 75 | return Future.value(LiveActivityState.active); 76 | } 77 | 78 | @override 79 | Future getPushToken(String activityId) { 80 | return Future.value('PUSH_TOKEN'); 81 | } 82 | 83 | @override 84 | Stream get activityUpdateStream { 85 | final map = { 86 | 'status': 'active', 87 | 'activityId': 'ACTIVITY_ID', 88 | 'token': 'ACTIVITY_TOKEN', 89 | }; 90 | return Stream.value(ActivityUpdate.fromMap(map)); 91 | } 92 | 93 | @override 94 | Future updateActivity( 95 | String activityId, 96 | Map data, [ 97 | AlertConfig? alertConfig, 98 | ]) { 99 | return Future.value(); 100 | } 101 | 102 | @override 103 | Future> getAllActivities() { 104 | return Future.value({'ACTIVITY_ID': LiveActivityState.active}); 105 | } 106 | 107 | @override 108 | Future createOrUpdateActivity( 109 | String customId, 110 | Map data, { 111 | bool removeWhenAppIsKilled = false, 112 | Duration? staleIn, 113 | }) { 114 | return Future.value(); 115 | } 116 | 117 | @override 118 | Future allowsPushStart() { 119 | return Future.value(true); 120 | } 121 | 122 | @override 123 | Stream get pushToStartTokenUpdateStream { 124 | return Stream.value('PUSH_TO_START_TOKEN'); 125 | } 126 | } 127 | 128 | void main() { 129 | final LiveActivitiesPlatform initialPlatform = 130 | LiveActivitiesPlatform.instance; 131 | LiveActivities liveActivitiesPlugin = LiveActivities(); 132 | MockLiveActivitiesPlatform fakePlatform = MockLiveActivitiesPlatform(); 133 | LiveActivitiesPlatform.instance = fakePlatform; 134 | 135 | test('$MethodChannelLiveActivities is the default instance', () { 136 | expect(initialPlatform, isInstanceOf()); 137 | }); 138 | 139 | test('init', () async { 140 | expect(await liveActivitiesPlugin.init(appGroupId: 'APP_GROUP_ID'), null); 141 | }); 142 | 143 | test('endActivity', () async { 144 | expect(await liveActivitiesPlugin.endActivity('ACTIVITY_ID'), null); 145 | }); 146 | 147 | test('updateActivity', () async { 148 | expect(await liveActivitiesPlugin.updateActivity('ACTIVITY_ID', {}), null); 149 | }); 150 | 151 | test('endAllActivities', () async { 152 | expect(await liveActivitiesPlugin.endAllActivities(), null); 153 | }); 154 | 155 | test('getAllActivitiesIds', () async { 156 | expect(await liveActivitiesPlugin.getAllActivitiesIds(), ['ACTIVITY_ID']); 157 | }); 158 | 159 | test('getAllActivities', () async { 160 | expect(await liveActivitiesPlugin.getAllActivities(), { 161 | 'ACTIVITY_ID': LiveActivityState.active, 162 | }); 163 | }); 164 | 165 | test('areActivitiesSupported', () async { 166 | expect(await liveActivitiesPlugin.areActivitiesSupported(), true); 167 | }); 168 | 169 | test('areActivitiesEnabled', () async { 170 | expect(await liveActivitiesPlugin.areActivitiesEnabled(), true); 171 | }); 172 | 173 | test('urlSchemeStream', () async { 174 | final result = await liveActivitiesPlugin.urlSchemeStream().first; 175 | expect(result.host, 'HOST'); 176 | expect(result.path, 'PATH'); 177 | expect(result.scheme, 'SCHEME'); 178 | expect(result.url, 'URL'); 179 | expect(result.queryParameters.first['name'], 'NAME'); 180 | expect(result.queryParameters.first['value'], 'VALUE'); 181 | }); 182 | 183 | test('getActivityState', () async { 184 | expect( 185 | await liveActivitiesPlugin.getActivityState('ACTIVITY_ID'), 186 | LiveActivityState.active, 187 | ); 188 | }); 189 | 190 | test('getPushToken', () async { 191 | expect(await liveActivitiesPlugin.getPushToken('PUSH_TOKEN'), 'PUSH_TOKEN'); 192 | }); 193 | 194 | test('activityUpdateStream', () async { 195 | final result = await liveActivitiesPlugin.activityUpdateStream.first; 196 | expect(result.activityId, 'ACTIVITY_ID'); 197 | expect( 198 | result.map( 199 | active: (state) => state.activityToken, 200 | ended: (_) => 'WRONG_TOKEN', 201 | stale: (_) => 'WRONG_TOKEN', 202 | unknown: (_) => 'WRONG_TOKEN', 203 | ), 204 | 'ACTIVITY_TOKEN', 205 | ); 206 | }); 207 | 208 | test('activityUpdateStreamMapOrNullCorrectMapping', () async { 209 | final result = await liveActivitiesPlugin.activityUpdateStream.first; 210 | final wrongMappingIsNull = result.mapOrNull(ended: (_) => 'NOT_NULL'); 211 | 212 | expect(wrongMappingIsNull, null); 213 | 214 | final correctMappingNotNull = result.mapOrNull( 215 | active: (state) => state.activityToken, 216 | ); 217 | 218 | expect(correctMappingNotNull, 'ACTIVITY_TOKEN'); 219 | }); 220 | 221 | test('allowsPushStart', () async { 222 | expect(await liveActivitiesPlugin.allowsPushStart(), true); 223 | }); 224 | 225 | test('pushToStartTokenUpdateStream', () async { 226 | expect( 227 | await liveActivitiesPlugin.pushToStartTokenUpdateStream.first, 228 | 'PUSH_TO_START_TOKEN', 229 | ); 230 | }); 231 | } 232 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/example/live_activities/LiveActivityManager.kt: -------------------------------------------------------------------------------- 1 | package com.example.live_activities 2 | 3 | import android.util.Log 4 | import android.app.Notification 5 | import android.app.NotificationChannel 6 | import android.app.NotificationManager 7 | import android.content.Context 8 | import android.os.Build 9 | import androidx.core.app.NotificationManagerCompat 10 | import java.math.BigInteger 11 | import java.security.MessageDigest 12 | 13 | open class LiveActivityManager(private val context: Context) { 14 | private var channelName: String = "Live Activities" 15 | 16 | open suspend fun buildNotification( 17 | notification: Notification.Builder, 18 | event: String, 19 | data: Map 20 | ): Notification { 21 | throw NotImplementedError("You must implement buildNotification in your subclass") 22 | } 23 | 24 | private fun createNotificationChannel( 25 | channelName: String, 26 | channelDescription: String, 27 | channelImportance: Int = NotificationManager.IMPORTANCE_LOW, 28 | ) { 29 | this.channelName = channelName 30 | val existingChannel = 31 | (context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).getNotificationChannel( 32 | channelName 33 | ) 34 | if (existingChannel == null) { 35 | val channel = NotificationChannel( 36 | channelName, channelDescription, channelImportance 37 | ).apply { 38 | setSound(null, null) 39 | enableVibration(false) 40 | setShowBadge(false) 41 | lockscreenVisibility = Notification.VISIBILITY_PUBLIC 42 | } 43 | 44 | val notificationManager = 45 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 46 | notificationManager.createNotificationChannel(channel) 47 | } 48 | } 49 | 50 | // Converts a string to an int to use it as notification ID 51 | private fun getNotificationIdFromString(input: String): Int { 52 | val digest = 53 | MessageDigest.getInstance("SHA-256").digest(input.toByteArray()) 54 | return BigInteger(digest).abs().toInt() // Get positive Int 55 | } 56 | 57 | fun initialize(data: Map) { 58 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return 59 | 60 | val liveActivityChannelName = 61 | data["liveActivityChannelName"] as? String ?: "Live Activities" 62 | val liveActivityChannelDescription = 63 | data["liveActivityChannelDescription"] as? String 64 | ?: "Live Activities Notifications" 65 | 66 | 67 | createNotificationChannel( 68 | liveActivityChannelName, liveActivityChannelDescription 69 | ) 70 | } 71 | 72 | suspend fun createActivity( 73 | activityTag: String, 74 | timestamp: Long, data: Map 75 | ): String? { 76 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return null; 77 | 78 | val notificationId = getNotificationIdFromString(activityTag) 79 | 80 | val notificationManager = 81 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 82 | 83 | val areNotificationsEnabled = 84 | NotificationManagerCompat.from(context).areNotificationsEnabled() 85 | 86 | if (areNotificationsEnabled) { 87 | val builder = Notification.Builder(context, channelName) 88 | builder.extras.putLong("activity_timestamp", timestamp) 89 | 90 | notificationManager.notify( 91 | activityTag, 92 | notificationId, 93 | buildNotification(builder, "create", data) 94 | ) 95 | } else { 96 | Log.w( 97 | "LiveActivityManager", 98 | "Notification permission denied. Unable to show notification." 99 | ) 100 | return null 101 | } 102 | 103 | return activityTag 104 | } 105 | 106 | suspend fun updateActivity( 107 | activityTag: String, 108 | timestamp: Long, 109 | data: Map 110 | ) { 111 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return 112 | 113 | val notificationId = getNotificationIdFromString(activityTag) 114 | 115 | val notificationManager = 116 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 117 | 118 | // Check if existing notification has a newer or equal timestamp 119 | val existingNotification = notificationManager.getActiveNotifications() 120 | .firstOrNull { 121 | it.tag == activityTag && 122 | it.notification.channelId == channelName 123 | } 124 | 125 | val existingTimestamp = existingNotification?.notification?.extras?.getLong("activity_timestamp") ?: 0L 126 | if (existingTimestamp >= timestamp) { 127 | Log.w( 128 | "LiveActivityManager", 129 | "Attempted to update activity with ID $activityTag but the timestamp is not newer than the existing one." 130 | ) 131 | return 132 | } 133 | 134 | val areNotificationsEnabled = 135 | NotificationManagerCompat.from(context).areNotificationsEnabled() 136 | 137 | if (areNotificationsEnabled) { 138 | val builder = Notification.Builder(context, channelName) 139 | builder.extras.putLong("activity_timestamp", timestamp) 140 | 141 | notificationManager.notify( 142 | activityTag, 143 | notificationId, 144 | buildNotification(builder, "update", data) 145 | ) 146 | } else { 147 | Log.w( 148 | "LiveActivityManager", 149 | "Notification permission denied. Unable to show notification." 150 | ) 151 | return 152 | } 153 | } 154 | 155 | fun endActivity( 156 | activityTag: String, 157 | data: Map 158 | ) { 159 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return 160 | 161 | val notificationId = getNotificationIdFromString(activityTag) 162 | 163 | val notificationManager = 164 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 165 | 166 | notificationManager.cancel(activityTag, notificationId) 167 | } 168 | 169 | fun endAllActivities(data: Map) { 170 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return 171 | 172 | val notificationManager = 173 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 174 | 175 | notificationManager.getActiveNotifications() 176 | .filter { statusBarNotification -> 177 | statusBarNotification.notification.channelId == channelName 178 | } 179 | .forEach { statusBarNotification -> 180 | notificationManager.cancel(statusBarNotification.tag, statusBarNotification.id) 181 | } 182 | } 183 | 184 | fun getAllActivitiesIds(data: Map): List { 185 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return emptyList() 186 | 187 | val notificationManager = 188 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 189 | 190 | return notificationManager.getActiveNotifications() 191 | .filter { statusBarNotification -> 192 | statusBarNotification.notification.channelId == channelName 193 | } 194 | .mapNotNull { statusBarNotification -> 195 | statusBarNotification.tag 196 | } 197 | } 198 | 199 | fun areActivitiesSupported(data: Map): Boolean { 200 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false; 201 | return true 202 | } 203 | 204 | fun areActivitiesEnabled(data: Map): Boolean { 205 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return false; 206 | return NotificationManagerCompat.from(context).areNotificationsEnabled() 207 | } 208 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.4.3 2 | - 🏗️ Migrating to UISceneDelegate (Flutter 3.38.x iOS breaking change). 3 | - 🏗️ Default SDK environment is now 3.10.0 and Flutter SDK >= 3.38.0. 4 | - 🏗️ Fix compile error Android example project (thanks to @trunghieuvn 👍). 5 | - ✨🐛 (Android) Store notification IDs on app termination and return string IDs to Dart (thanks to @felixibel 👍). 6 | - ✨ Add support for `getActivityState()` to detect activity by custom activity id (thanks to @reynirf 👍). 7 | - ✨ Add option to control iOS notification permission request (thanks to @asmz 👍). 8 | 9 | ## 2.4.2 10 | 11 | - ✨ New method `areActivitiesSupported()` ➡️ Check if live activities are supported on the current platform/OS version. (thanks to @MortadhaFadhlaoui 👍). 12 | - 🏗️ Method `areActivitiesEnabled()` ➡️ Now only checks user settings without platform version validation. (thanks to @MortadhaFadhlaoui 👍). 13 | 14 | ## 2.4.1 15 | 16 | - 🐛 Check for customId in endActivitiesWithId (thanks to @dkobia 👍). 17 | - 🐛 Bugfix avoid remove all notifications (thanks to @EArminjon 👍). 18 | 19 | ## 2.4.0 - Live activity is now available for Android too! 20 | 21 | - ✨ Add support for Android Live Activities (thanks to @EArminjon 👍). 22 | - 🐛 Custom ID activities fail to end correctly (thanks to @charleyzhu 👍). 23 | - 📝 Update README.md for local and remote Live activities. 24 | 25 | **ℹ️ BREAKING CHANGES ℹ️** 26 | 27 | - On both platforms, activityID is now a required parameter for `createActivity` and `createOrUpdateActivity()`. 28 | - Bump **iOS** minimum version to **13**. 29 | 30 | ## 2.3.2 31 | 32 | - ✨ Ability to get the "pushToStartToken" (thanks to @Clon1998 👍). 33 | - ⬆️ Upgrade dependencies. 34 | 35 | ## 2.3.1 36 | 37 | - 🐛 `LiveActivityFileFromMemory` can't share image with AppGroup (thanks to @EArminjon 👍). 38 | - 📝 Added minor version check when not being able to see the Live Activity (thanks to @dasanten 👍). 39 | - ⬆️ Upgrade dependencies. 40 | 41 | ## 2.3.0 42 | 43 | - 🏗️ Move to Swift Package Manager. 44 | - 🏗️ Regenerate example app. 45 | - ⬆️ Upgrade dependencies. 46 | 47 | ## 2.2.0 48 | 49 | - ✨ Added a new method `createOrUpdateActivity()`, you can use it to create or update by passing an activity ID (thanks to @Clon1998 👍). 50 | - ⬆️ Upgrade dependencies. 51 | 52 | ## 2.1.0 53 | 54 | - ✨ You can now send generic files instead of just pictures. 55 | 56 | To send a file you can do the following on the Dart code: 57 | 58 | ```dart 59 | LiveActivityFileFromAsset('assets/files/rules.txt') 60 | ``` 61 | 62 | And in your Swift code: 63 | 64 | ```swift 65 | let ruleFile = sharedDefault.string(forKey: context.attributes.prefixedKey("yourFileKey"))! 66 | let rule = (try? String(contentsOfFile: ruleFile, encoding: .utf8)) ?? "" 67 | ``` 68 | 69 | > Check the example for a full demo. 70 | 71 | **BREAKING CHANGES** 72 | 73 | You must replace your images to: 74 | 75 | - `LiveActivityFileFromAsset.image()` 76 | - `LiveActivityFileFromMemory.image()` 77 | - `LiveActivityFileFromUrl.image()` 78 | 79 | In order to use `resizeFactor` you must do the following now: 80 | 81 | ```dart 82 | LiveActivityFileFromAsset.image( 83 | 'assets/images/chelsea.png', 84 | imageOptions: LiveActivityImageFileOptions( 85 | resizeFactor: 0.2 86 | ) 87 | ), 88 | ``` 89 | 90 | - 🐛 Example works again with scheme. 91 | 92 | ## 2.0.1 93 | 94 | - 🐛 Fix channel message sent from native to Flutter on a non-platform thread (thanks to @aswanath 👍) 95 | - 🗑️ Clean some code. 96 | 97 | ## 2.0.0 98 | 99 | - ✨ Use new custom iOS [App Group Directory dependency](https://pub.dev/packages/flutter_app_group_directory) (this will fix namespace for Android gradle builds). 100 | - ✨ Removed deprecated flutter_native_image dependency and replaced by image (thanks to @SnapDrive 👍) 101 | - ⬆️ Upgrade dependencies. 102 | 103 | ## 1.9.5 104 | 105 | - 🐛 Fix `areActivitiesEnabled()` on unsupported devices. 106 | 107 | ## 1.9.4 108 | 109 | - 🍱 Convert images to webp. 110 | - ⬆️ Upgrade dependencies. 111 | 112 | ## 1.9.3 113 | 114 | - 🐛 Force returning false for `areActivitiesEnabled()` when no iOS devices. 115 | 116 | ## 1.9.2 117 | 118 | - ✨ Simplified fetching of ActivityState of all created live activities (thanks to @Clon1998 👍). 119 | - 🐛 Fixes background thread invocation of event streams (thanks to @ggirotto 👍). 120 | - 🐛 Replaced getImageProperties with dart buffer and descriptor (thanks to @anumb 👍). 121 | - 🐛 Fix tests. 122 | - ⬆️ Upgrade dependencies. 123 | 124 | ## 1.9.1 125 | 126 | - ✨ Add update with alert config (thanks @charlesRmajor 👍). 127 | - ✨ Add an option to use preloaded images (thanks @Niklas-Sommer 👍). 128 | - ✨ Add Android support - currently only used to check if live activities is supported (thanks @ggirotto 👍). 129 | - ✨ Example app support Material 3. 130 | - 🐛 Fix tests. 131 | - 📝 Update README.md. 132 | - ⬆️ Upgrade dependencies. 133 | 134 | ## 1.9.0 135 | 136 | - ✨ **BREAKING CHANGE**: Add the ability to handle multiple live notification (thanks @Clon1998 👍). 137 | 138 | Please follow this tutorial to add implement it: 139 | 140 | - Add the following Swift extension at the end of your extension code: 141 | 142 | ```swift 143 | extension LiveActivitiesAppAttributes { 144 | func prefixedKey(_ key: String) -> String { 145 | return "\(id)_\(key)" 146 | } 147 | } 148 | ``` 149 | 150 | - For each keys on your native Swift code, please changes the following lines: 151 | 152 | ```swift 153 | let myVariableFromFlutter = sharedDefault.string(forKey: "myVariableFromFlutter") // repleace this by ... 154 | let myVariableFromFlutter = sharedDefault.string(forKey: context.attributes.prefixedKey("myVariableFromFlutter")) // <-- this 155 | ``` 156 | 157 | - 🐛 Fix stall state for unknown activityId (thanks @Clon1998 👍). 158 | - 🐛 Now return `null` value when activity is not found in `getActivityState()`. 159 | 160 | ## 1.8.0 161 | 162 | - ✨ Add url scheme optional argument. 163 | - ✨ Add sinks unregister on engine end (thanks @ggirotto 👍). 164 | - 🐛 Fix example images size. 165 | - ⬆️ Upgrade dependencies. 166 | 167 | ## 1.7.5 168 | 169 | - 🚨 Lint some code. 170 | - 🐛 Fix deprecated tests. 171 | - ⬆️ Upgrade dependencies. 172 | 173 | ## 1.7.4 174 | 175 | - 🐛 Method `areActivitiesEnabled()` are now callable on iOS < 16.1 176 | - ✨ Creating an activity can now use stale-date (thanks @arnar-steinthors 👍). 177 | 178 | ## 1.7.3 179 | 180 | - ✨ ActivityUpdate subclasses are now public along with a new MapOrNull method (thanks @arnar-steinthors 👍). 181 | 182 | ## 1.7.2 183 | 184 | - ✨ Add missing "stale" activity status. 185 | - 🐛 When value set to null in map, value is removed from live activity. 186 | 187 | ## 1.7.1 188 | 189 | - 🐛 Fix missing `activityUpdateStream` implementation channel on native part. 190 | 191 | ## 1.7.0 192 | 193 | - ✨🐛 Change method `getPushToken()` to be synchronous. 194 | - ⬆️ Upgrade dependencies. 195 | 196 | ## 1.6.0 197 | 198 | - ✨ Add a way to track push token and the activity status (thanks @arnar-steinthors 👍). 199 | - ♻️ Format code. 200 | 201 | ## 1.5.0 202 | 203 | - ✨ Add method to get push token (thanks to @jolamar 👍). 204 | - ♻️ Rework Swift code. 205 | 206 | ## 1.4.2+1 207 | 208 | - 📝 Add screenshots in pubspec.yaml 209 | 210 | ## 1.4.2 211 | 212 | - ✨ End live activity when the app is terminated (thanks to @JulianBissekkou 👍). 213 | 214 | ## 1.4.1 215 | 216 | - 🐛 Fix a bug where init never completes (thanks to @JulianBissekkou 👍). 217 | 218 | ## 1.4.0 219 | 220 | - ✨ Can now pass assets between Flutter & Native. 221 | - 📝 Update README.md. 222 | 223 | ## 1.3.0+1 224 | 225 | - 📝 Update README.md. 226 | 227 | ## 1.3.0 228 | 229 | - ✨ Now using [App Groups](https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups) to pass typed data across Flutter & Native ! 230 | - 🗑️ Remove unused code in example. 231 | - 📝 Improve README.md. 232 | 233 | ## 1.2.1 234 | 235 | - ✨ Add method to get the activity state (active, ended or dismissed). 236 | 237 | ## 1.2.0 238 | 239 | - ✨ Add stream to handle url scheme from live activities &/or dynamic island. 240 | - 📝 Improve README.md 241 | - ♻️ Rework example 242 | 243 | ## 1.1.0 244 | 245 | - ✨ Add method to check if live activities are enabled. 246 | - ✨ Add method to get all activities ids created. 247 | - ✨ Add method to cancel all activities. 248 | - 🐛 Fix add result to all methods. 249 | 250 | ## 1.0.0 251 | 252 | - 🎉 Initial release. 253 | -------------------------------------------------------------------------------- /lib/live_activities.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:live_activities/live_activities_platform_interface.dart'; 3 | import 'package:live_activities/models/activity_update.dart'; 4 | import 'package:live_activities/models/alert_config.dart'; 5 | import 'package:live_activities/models/live_activity_state.dart'; 6 | import 'package:live_activities/models/url_scheme_data.dart'; 7 | import 'package:live_activities/services/app_groups_file_service.dart'; 8 | 9 | class LiveActivities { 10 | final AppGroupsFileService _appGroupsFileService = AppGroupsFileService(); 11 | 12 | /// This is required to initialize the plugin. 13 | /// Create an App Group inside "Runner" target & "Extension" in Xcode. 14 | /// Be sure to set the *SAME* App Group in both targets. 15 | /// [urlScheme] is optional and is the scheme sub-component of the URL. 16 | /// [appGroupId] is the App Group identifier. 17 | /// If [requireNotificationPermission] is set to false, the plugin will not request notification permission for iOS. 18 | Future init({ 19 | required String appGroupId, 20 | String? urlScheme, 21 | bool requireNotificationPermission = true, 22 | }) { 23 | _appGroupsFileService.init(appGroupId: appGroupId); 24 | return LiveActivitiesPlatform.instance.init( 25 | appGroupId, 26 | urlScheme: urlScheme, 27 | requireNotificationPermission: requireNotificationPermission, 28 | ); 29 | } 30 | 31 | /// Create an iOS 16.1+ live activity. 32 | /// When the activity is created, an activity id is returned. 33 | /// Data is a map of key/value pairs that will be transmitted to your iOS extension widget. 34 | /// Files like images are limited by size, 35 | /// be sure to pass only small file size (you can use ```resizeFactor``` for images). 36 | /// 37 | /// [StaleIn] indicates if a StaleDate should be added to the activity. If the value is null or the Duration 38 | /// is less than 1 minute then no staleDate will be used. The parameter only affects the live activity on 39 | /// iOS 16.2+ and does nothing on on iOS 16.1 40 | Future createActivity( 41 | String activityId, 42 | Map data, { 43 | bool removeWhenAppIsKilled = false, 44 | Duration? staleIn, 45 | }) async { 46 | if (defaultTargetPlatform == TargetPlatform.iOS) { 47 | await _appGroupsFileService.sendFilesToAppGroups(data); 48 | } 49 | return LiveActivitiesPlatform.instance.createActivity( 50 | activityId, 51 | data, 52 | removeWhenAppIsKilled: removeWhenAppIsKilled, 53 | staleIn: staleIn, 54 | ); 55 | } 56 | 57 | /// Update an iOS 16.1+ live activity. 58 | /// You can get an activity id by calling [createActivity]. 59 | /// Data is a map of key/value pairs that will be transmitted to your iOS extension widget. 60 | /// Map is limited to String keys and values for now. 61 | Future updateActivity( 62 | String activityId, 63 | Map data, [ 64 | AlertConfig? alertConfig, 65 | ]) async { 66 | if (defaultTargetPlatform == TargetPlatform.iOS) { 67 | await _appGroupsFileService.sendFilesToAppGroups(data); 68 | } 69 | return LiveActivitiesPlatform.instance.updateActivity( 70 | activityId, 71 | data, 72 | alertConfig, 73 | ); 74 | } 75 | 76 | Future createOrUpdateActivity( 77 | String activityId, 78 | Map data, { 79 | bool removeWhenAppIsKilled = false, 80 | Duration? staleIn, 81 | }) async { 82 | if (defaultTargetPlatform == TargetPlatform.iOS) { 83 | await _appGroupsFileService.sendFilesToAppGroups(data); 84 | } 85 | return LiveActivitiesPlatform.instance.createOrUpdateActivity( 86 | activityId, 87 | data, 88 | removeWhenAppIsKilled: removeWhenAppIsKilled, 89 | staleIn: staleIn, 90 | ); 91 | } 92 | 93 | /// End an iOS 16.1+ live activity. 94 | /// You can get an activity id by calling [createActivity]. 95 | Future endActivity(String activityId) { 96 | return LiveActivitiesPlatform.instance.endActivity(activityId); 97 | } 98 | 99 | /// Get the activity state. 100 | /// If the activity is not found, `null` is returned. 101 | /// 102 | /// Only available on iOS. 103 | Future getActivityState(String activityId) { 104 | return LiveActivitiesPlatform.instance.getActivityState(activityId); 105 | } 106 | 107 | /// Get synchronously the push token. 108 | /// Prefer using the stream [activityUpdateStream] to keep push token up to date. 109 | Future getPushToken(String activityId) { 110 | return LiveActivitiesPlatform.instance.getPushToken(activityId); 111 | } 112 | 113 | /// Get all iOS 16.1+ live activity ids. 114 | /// You can get an activity id by calling [createActivity]. 115 | Future> getAllActivitiesIds() { 116 | return LiveActivitiesPlatform.instance.getAllActivitiesIds(); 117 | } 118 | 119 | /// End all iOS 16.1+ live activities. 120 | Future endAllActivities() { 121 | return LiveActivitiesPlatform.instance.endAllActivities(); 122 | } 123 | 124 | /// Get all iOS 16.1+ live activities and their state. 125 | Future> getAllActivities() { 126 | return LiveActivitiesPlatform.instance.getAllActivities(); 127 | } 128 | 129 | /// Check if live activities are supported on this platform/OS version. 130 | /// Returns true if the device/OS supports live activities, false otherwise. 131 | Future areActivitiesSupported() { 132 | return LiveActivitiesPlatform.instance.areActivitiesSupported(); 133 | } 134 | 135 | /// Check if live activities are enabled by the user in their device settings. 136 | /// On iOS: Checks if the user has enabled Live Activities in Settings > Face ID & Passcode > Live Activities 137 | /// On Android: Checks if notifications are enabled for the app 138 | /// Note: This method only checks user settings, not platform support. Use areActivitiesSupported() first. 139 | Future areActivitiesEnabled() { 140 | return LiveActivitiesPlatform.instance.areActivitiesEnabled(); 141 | } 142 | 143 | /// Checks if iOS 17.2+ which allows push start for live activities. 144 | Future allowsPushStart() { 145 | return LiveActivitiesPlatform.instance.allowsPushStart(); 146 | } 147 | 148 | /// Get a stream of url scheme data. 149 | /// Don't forget to add **CFBundleURLSchemes** to your Info.plist file. 150 | /// Return a Future of [scheme] [url] [host] [path] and [queryParameters]. 151 | Stream urlSchemeStream() { 152 | return LiveActivitiesPlatform.instance.urlSchemeStream(); 153 | } 154 | 155 | /// Remove all files copied in app group directory. 156 | /// This is recommended after you send files, files are stored but never deleted. 157 | /// You can set force param to remove **ALL** files in app group directory. 158 | Future dispose({bool force = false}) async { 159 | if (force) { 160 | return _appGroupsFileService.removeAllFiles(); 161 | } else { 162 | return _appGroupsFileService.removeFilesSession(); 163 | } 164 | } 165 | 166 | /// A stream of activity updates. 167 | /// An event is emitted onto this stream each time a pushTokenUpdate occurs. The operating system can decide 168 | /// to update a push token at any time. An update can also mean that the activity has ended or it became stale 169 | /// 170 | /// You can map out each type of update to respond to it 171 | /// 172 | /// ```dart 173 | /// activityUpdateStream.listen((event) => event.map( 174 | /// active: (state) { ... }, 175 | /// ended: (state) { ... }, 176 | /// stale: (state) { ... }, 177 | /// unknown: (state) { ... }, 178 | /// )) 179 | /// ``` 180 | /// 181 | /// or if you only want to react to limited updates you can use mapOrNull 182 | /// 183 | /// ```dart 184 | /// activityUpdateStream.listen((event) => event.mapOrNull( 185 | /// active: (state) { ... }, 186 | /// )) 187 | /// ``` 188 | Stream get activityUpdateStream => 189 | LiveActivitiesPlatform.instance.activityUpdateStream; 190 | 191 | /// A stream of push-to-start tokens for iOS 17.2+ Live Activities. 192 | /// This stream emits tokens that can be used to start a Live Activity remotely via push notifications. 193 | /// 194 | /// When iOS generates or updates a push-to-start token, it will be emitted through this stream. 195 | /// You should send this token to your push notification server to enable remote Live Activity creation. 196 | /// 197 | /// Example usage: 198 | /// ```dart 199 | /// liveActivities.pushToStartTokenUpdateStream.listen((token) { 200 | /// // Send token to your server 201 | /// print('Received push-to-start token: $token'); 202 | /// }); 203 | /// ``` 204 | /// 205 | /// This feature is only available on iOS 17.2 and later. Use [allowsPushStart] to check support. 206 | Stream get pushToStartTokenUpdateStream async* { 207 | final allowed = await allowsPushStart(); 208 | 209 | if (!allowed) { 210 | throw Exception('Push-to-start is not allowed on this device'); 211 | } 212 | 213 | yield* LiveActivitiesPlatform.instance.pushToStartTokenUpdateStream; 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:io'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:live_activities/live_activities.dart'; 6 | import 'package:live_activities/models/live_activity_file.dart'; 7 | import 'package:live_activities/models/url_scheme_data.dart'; 8 | import 'package:live_activities_example/models/football_game_live_activity_model.dart'; 9 | import 'package:live_activities_example/widgets/score_widget.dart'; 10 | import 'package:permission_handler/permission_handler.dart'; 11 | 12 | void main() { 13 | runApp(const MyApp()); 14 | } 15 | 16 | class MyApp extends StatefulWidget { 17 | const MyApp({super.key}); 18 | 19 | @override 20 | State createState() => _MyAppState(); 21 | } 22 | 23 | class _MyAppState extends State { 24 | @override 25 | Widget build(BuildContext context) { 26 | return const MaterialApp( 27 | home: Home(), 28 | ); 29 | } 30 | } 31 | 32 | class Home extends StatefulWidget { 33 | const Home({super.key}); 34 | 35 | @override 36 | State createState() => _HomeState(); 37 | } 38 | 39 | class _HomeState extends State { 40 | final _liveActivitiesPlugin = LiveActivities(); 41 | String? _latestActivityId; 42 | StreamSubscription? urlSchemeSubscription; 43 | FootballGameLiveActivityModel? _footballGameLiveActivityModel; 44 | 45 | int teamAScore = 0; 46 | int teamBScore = 0; 47 | 48 | String teamAName = 'PSG'; 49 | String teamBName = 'Chelsea'; 50 | 51 | @override 52 | void initState() { 53 | super.initState(); 54 | 55 | _liveActivitiesPlugin.init( 56 | appGroupId: 'group.dimitridessus.liveactivities', urlScheme: 'la'); 57 | 58 | if (Platform.isIOS) { 59 | _liveActivitiesPlugin.activityUpdateStream.listen((event) { 60 | print('Activity update: $event'); 61 | }); 62 | 63 | urlSchemeSubscription = 64 | _liveActivitiesPlugin.urlSchemeStream().listen((schemeData) { 65 | setState(() { 66 | if (schemeData.path == '/stats') { 67 | showDialog( 68 | context: context, 69 | builder: (BuildContext context) { 70 | return AlertDialog( 71 | title: const Text('Stats 📊'), 72 | content: Text( 73 | 'Now playing final world cup between $teamAName and $teamBName\n\n$teamAName score: $teamAScore\n$teamBName score: $teamBScore', 74 | ), 75 | actions: [ 76 | TextButton( 77 | onPressed: () => Navigator.of(context).pop(), 78 | child: const Text('Close'), 79 | ), 80 | ], 81 | ); 82 | }, 83 | ); 84 | } 85 | }); 86 | }); 87 | } 88 | } 89 | 90 | @override 91 | void dispose() { 92 | urlSchemeSubscription?.cancel(); 93 | _liveActivitiesPlugin.dispose(); 94 | super.dispose(); 95 | } 96 | 97 | @override 98 | Widget build(BuildContext context) { 99 | return Scaffold( 100 | appBar: AppBar( 101 | title: const Text( 102 | 'Live Activities (Flutter)', 103 | style: TextStyle( 104 | fontSize: 19, 105 | color: Colors.white, 106 | ), 107 | ), 108 | backgroundColor: Colors.green, 109 | ), 110 | body: SizedBox.expand( 111 | child: Padding( 112 | padding: const EdgeInsets.all(16.0), 113 | child: Column( 114 | mainAxisAlignment: MainAxisAlignment.center, 115 | children: [ 116 | if (_latestActivityId != null) 117 | Padding( 118 | padding: const EdgeInsets.only(bottom: 10.0), 119 | child: Card( 120 | child: SizedBox( 121 | width: double.infinity, 122 | height: 120, 123 | child: Row( 124 | children: [ 125 | Expanded( 126 | child: ScoreWidget( 127 | score: teamAScore, 128 | teamName: teamAName, 129 | onScoreChanged: (score) { 130 | setState(() { 131 | teamAScore = score < 0 ? 0 : score; 132 | }); 133 | _updateScore(); 134 | }, 135 | ), 136 | ), 137 | Expanded( 138 | child: ScoreWidget( 139 | score: teamBScore, 140 | teamName: teamBName, 141 | onScoreChanged: (score) { 142 | setState(() { 143 | teamBScore = score < 0 ? 0 : score; 144 | }); 145 | _updateScore(); 146 | }, 147 | ), 148 | ), 149 | ], 150 | ), 151 | ), 152 | ), 153 | ), 154 | if (_latestActivityId == null) 155 | TextButton( 156 | onPressed: () async { 157 | await _liveActivitiesPlugin.endAllActivities(); 158 | await Permission.notification.request(); 159 | teamAScore = 0; 160 | teamBScore = 0; 161 | _footballGameLiveActivityModel = 162 | FootballGameLiveActivityModel( 163 | matchName: 'World cup ⚽️', 164 | teamAName: 'PSG', 165 | teamAState: 'Home', 166 | ruleFile: Platform.isIOS 167 | ? LiveActivityFileFromAsset('assets/files/rules.txt') 168 | : null, 169 | teamALogo: Platform.isIOS 170 | ? LiveActivityFileFromAsset.image( 171 | 'assets/images/psg.png') 172 | : null, 173 | teamBLogo: Platform.isIOS 174 | ? LiveActivityFileFromAsset.image( 175 | 'assets/images/chelsea.png', 176 | imageOptions: LiveActivityImageFileOptions( 177 | resizeFactor: 0.2)) 178 | : null, 179 | teamBName: 'Chelsea', 180 | teamBState: 'Guest', 181 | matchStartDate: DateTime.now(), 182 | matchEndDate: DateTime.now().add( 183 | const Duration( 184 | minutes: 6, 185 | seconds: 30, 186 | ), 187 | ), 188 | ); 189 | 190 | final activityId = 191 | await _liveActivitiesPlugin.createActivity( 192 | DateTime.now().millisecondsSinceEpoch.toString(), 193 | _footballGameLiveActivityModel!.toMap(), 194 | ); 195 | print("ActivityID: $activityId"); 196 | setState(() => _latestActivityId = activityId); 197 | }, 198 | child: const Column( 199 | children: [ 200 | Text('Start football match ⚽️'), 201 | Text( 202 | '(start a new live activity)', 203 | style: TextStyle( 204 | fontSize: 10, 205 | fontStyle: FontStyle.italic, 206 | ), 207 | ), 208 | ], 209 | ), 210 | ), 211 | if (_latestActivityId == null) 212 | TextButton( 213 | onPressed: () async { 214 | final supported = 215 | await _liveActivitiesPlugin.areActivitiesEnabled(); 216 | if (context.mounted) { 217 | showDialog( 218 | context: context, 219 | builder: (BuildContext context) { 220 | return AlertDialog( 221 | content: Text( 222 | supported ? 'Supported' : 'Not supported', 223 | ), 224 | actions: [ 225 | TextButton( 226 | onPressed: () => Navigator.of(context).pop(), 227 | child: const Text('Close'), 228 | ), 229 | ], 230 | ); 231 | }, 232 | ); 233 | } 234 | }, 235 | child: const Text('Is live activities supported ? 🤔'), 236 | ), 237 | if (_latestActivityId != null) 238 | TextButton( 239 | onPressed: () { 240 | _liveActivitiesPlugin.endAllActivities(); 241 | _latestActivityId = null; 242 | setState(() {}); 243 | }, 244 | child: const Column( 245 | children: [ 246 | Text('Stop match ✋'), 247 | Text( 248 | '(end all live activities)', 249 | style: TextStyle( 250 | fontSize: 10, 251 | fontStyle: FontStyle.italic, 252 | ), 253 | ), 254 | ], 255 | ), 256 | ), 257 | ], 258 | ), 259 | ), 260 | ), 261 | ); 262 | } 263 | 264 | Future _updateScore() async { 265 | if (_footballGameLiveActivityModel == null) { 266 | return; 267 | } 268 | 269 | final data = _footballGameLiveActivityModel!.copyWith( 270 | teamAScore: teamAScore, 271 | teamBScore: teamBScore, 272 | // teamAName: null, 273 | ); 274 | return _liveActivitiesPlugin.updateActivity( 275 | _latestActivityId!, 276 | data.toMap(), 277 | ); 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /example/ios/extension-example/extension_example.swift: -------------------------------------------------------------------------------- 1 | // 2 | // extension_example.swift 3 | // extension-example 4 | // 5 | // Created by Dimitri Dessus on 28/09/2022. 6 | // 7 | 8 | import ActivityKit 9 | import WidgetKit 10 | import SwiftUI 11 | 12 | @main 13 | struct Widgets: WidgetBundle { 14 | var body: some Widget { 15 | if #available(iOS 16.1, *) { 16 | FootballMatchApp() 17 | } 18 | } 19 | } 20 | 21 | // We need to redefined live activities pipe 22 | struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable { 23 | public typealias LiveDeliveryData = ContentState 24 | 25 | public struct ContentState: Codable, Hashable { } 26 | 27 | var id = UUID() 28 | } 29 | 30 | // Create shared default with custom group 31 | let sharedDefault = UserDefaults(suiteName: "group.dimitridessus.liveactivities")! 32 | 33 | @available(iOSApplicationExtension 16.1, *) 34 | struct FootballMatchApp: Widget { 35 | var body: some WidgetConfiguration { 36 | ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in 37 | let matchName = sharedDefault.string(forKey: context.attributes.prefixedKey("matchName"))! 38 | let ruleFile = sharedDefault.string(forKey: context.attributes.prefixedKey("ruleFile"))! 39 | 40 | let teamAName = sharedDefault.string(forKey: context.attributes.prefixedKey("teamAName"))! 41 | let teamAState = sharedDefault.string(forKey: context.attributes.prefixedKey("teamAState"))! 42 | let teamAScore = sharedDefault.integer(forKey: context.attributes.prefixedKey("teamAScore")) 43 | let teamALogo = sharedDefault.string(forKey: context.attributes.prefixedKey("teamALogo"))! 44 | 45 | let teamBName = sharedDefault.string(forKey: context.attributes.prefixedKey("teamBName"))! 46 | let teamBState = sharedDefault.string(forKey: context.attributes.prefixedKey("teamBState"))! 47 | let teamBScore = sharedDefault.integer(forKey: context.attributes.prefixedKey("teamBScore")) 48 | let teamBLogo = sharedDefault.string(forKey: context.attributes.prefixedKey("teamBLogo"))! 49 | 50 | let rule = (try? String(contentsOfFile: ruleFile, encoding: .utf8)) ?? "" 51 | let matchStartDate = Date(timeIntervalSince1970: sharedDefault.double(forKey: context.attributes.prefixedKey("matchStartDate")) / 1000) 52 | let matchEndDate = Date(timeIntervalSince1970: sharedDefault.double(forKey: context.attributes.prefixedKey("matchEndDate")) / 1000) 53 | let matchRemainingTime = matchStartDate...matchEndDate 54 | 55 | ZStack { 56 | LinearGradient(colors: [Color.black.opacity(0.5),Color.black.opacity(0.3)], startPoint: .topLeading, endPoint: .bottom) 57 | 58 | HStack { 59 | ZStack { 60 | VStack(alignment: .center, spacing: 2.0) { 61 | 62 | Spacer() 63 | 64 | Text(teamAName) 65 | .lineLimit(1) 66 | .font(.subheadline) 67 | .fontWeight(.bold) 68 | .multilineTextAlignment(.center) 69 | 70 | Text(teamAState) 71 | .lineLimit(1) 72 | .font(.footnote) 73 | .fontWeight(.bold) 74 | .multilineTextAlignment(.center) 75 | } 76 | .frame(width: 70, height: 120) 77 | .padding(.bottom, 8) 78 | .padding(.top, 8) 79 | .background(.white.opacity(0.4), in: RoundedRectangle(cornerRadius: 16, style: .continuous)) 80 | 81 | ZStack { 82 | if let uiImageTeamA = UIImage(contentsOfFile: teamALogo) 83 | { 84 | Image(uiImage: uiImageTeamA) 85 | .resizable() 86 | .frame(width: 80, height: 80) 87 | .offset(y:-20) 88 | } 89 | } 90 | } 91 | 92 | VStack(alignment: .center, spacing: 6.0) { 93 | HStack { 94 | Text("\(teamAScore)") 95 | .font(.title) 96 | .fontWeight(.bold) 97 | 98 | Text(":") 99 | .font(.title) 100 | .fontWeight(.bold) 101 | .foregroundStyle(.primary) 102 | 103 | Text("\(teamBScore)") 104 | .font(.title) 105 | .fontWeight(.bold) 106 | } 107 | .padding(.horizontal, 5.0) 108 | .background(.white.opacity(0.4), in: RoundedRectangle(cornerRadius: 8, style: .continuous)) 109 | 110 | HStack(alignment: .center, spacing: 2.0) { 111 | Text(timerInterval: matchRemainingTime, countsDown: true) 112 | .multilineTextAlignment(.center) 113 | .frame(width: 50) 114 | .monospacedDigit() 115 | .font(.footnote) 116 | .foregroundStyle(.white) 117 | } 118 | 119 | VStack(alignment: .center, spacing: 1.0) { 120 | Link(destination: URL(string: "la://my.app/stats")!) { 121 | Text("See stats 📊") 122 | }.padding(.vertical, 5).padding(.horizontal, 5) 123 | Text(matchName) 124 | .font(.footnote) 125 | .foregroundStyle(.white) 126 | .padding(.bottom, 5) 127 | Text(rule) 128 | .font(.footnote) 129 | .foregroundStyle(.white.opacity(0.5)) 130 | } 131 | } 132 | .padding(.vertical, 6.0) 133 | 134 | ZStack { 135 | VStack(alignment: .center, spacing: 2.0) { 136 | 137 | Spacer() 138 | 139 | Text(teamBName) 140 | .lineLimit(1) 141 | .font(.subheadline) 142 | .fontWeight(.bold) 143 | .multilineTextAlignment(.center) 144 | 145 | Text(teamBState) 146 | .lineLimit(1) 147 | .font(.footnote) 148 | .fontWeight(.bold) 149 | .multilineTextAlignment(.center) 150 | } 151 | .frame(width: 70, height: 120) 152 | .padding(.bottom, 8) 153 | .padding(.top, 8) 154 | .background(.white.opacity(0.4), in: RoundedRectangle(cornerRadius: 16, style: .continuous)) 155 | 156 | ZStack { 157 | if let uiImageTeamB = UIImage(contentsOfFile: teamBLogo) 158 | { 159 | Image(uiImage: uiImageTeamB) 160 | .resizable() 161 | .frame(width: 80, height: 80) 162 | .offset(y:-20) 163 | } 164 | } 165 | } 166 | } 167 | .padding(.horizontal, 2.0) 168 | }.frame(height: 160) 169 | } dynamicIsland: { context in 170 | let matchName = sharedDefault.string(forKey: context.attributes.prefixedKey("matchName"))! 171 | 172 | let teamAName = sharedDefault.string(forKey: context.attributes.prefixedKey("teamAName"))! 173 | let teamAState = sharedDefault.string(forKey: context.attributes.prefixedKey("teamAState"))! 174 | let teamAScore = sharedDefault.integer(forKey: context.attributes.prefixedKey("teamAScore")) 175 | let teamALogo = sharedDefault.string(forKey: context.attributes.prefixedKey("teamALogo"))! 176 | 177 | let teamBName = sharedDefault.string(forKey: context.attributes.prefixedKey("teamBName"))! 178 | let teamBState = sharedDefault.string(forKey: context.attributes.prefixedKey("teamBState"))! 179 | let teamBScore = sharedDefault.integer(forKey: context.attributes.prefixedKey("teamBScore")) 180 | let teamBLogo = sharedDefault.string(forKey: context.attributes.prefixedKey("teamBLogo"))! 181 | 182 | let matchStartDate = Date(timeIntervalSince1970: sharedDefault.double(forKey: context.attributes.prefixedKey("matchStartDate")) / 1000) 183 | let matchEndDate = Date(timeIntervalSince1970: sharedDefault.double(forKey: context.attributes.prefixedKey("matchEndDate")) / 1000) 184 | let matchRemainingTime = matchStartDate...matchEndDate 185 | 186 | return DynamicIsland { 187 | DynamicIslandExpandedRegion(.leading) { 188 | VStack(alignment: .center, spacing: 2.0) { 189 | 190 | if let uiImageTeamA = UIImage(contentsOfFile: teamALogo) 191 | { 192 | Image(uiImage: uiImageTeamA) 193 | .resizable() 194 | .frame(width: 80, height: 80) 195 | .offset(y:0) 196 | } 197 | 198 | Spacer() 199 | 200 | Text(teamAName) 201 | .lineLimit(1) 202 | .font(.subheadline) 203 | .fontWeight(.bold) 204 | .multilineTextAlignment(.center) 205 | 206 | Text(teamAState) 207 | .lineLimit(1) 208 | .font(.footnote) 209 | .fontWeight(.bold) 210 | .multilineTextAlignment(.center) 211 | } 212 | .frame(width: 70, height: 120) 213 | .padding(.bottom, 8) 214 | .padding(.top, 8) 215 | 216 | 217 | } 218 | DynamicIslandExpandedRegion(.trailing) { 219 | VStack(alignment: .center, spacing: 2.0) { 220 | 221 | if let uiImageTeamB = UIImage(contentsOfFile: teamBLogo) 222 | { 223 | Image(uiImage: uiImageTeamB) 224 | .resizable() 225 | .frame(width: 80, height: 80) 226 | .offset(y:0) 227 | } 228 | 229 | Spacer() 230 | 231 | Text(teamBName) 232 | .lineLimit(1) 233 | .font(.subheadline) 234 | .fontWeight(.bold) 235 | .multilineTextAlignment(.center) 236 | 237 | Text(teamBState) 238 | .lineLimit(1) 239 | .font(.footnote) 240 | .fontWeight(.bold) 241 | .multilineTextAlignment(.center) 242 | } 243 | .frame(width: 70, height: 120) 244 | .padding(.bottom, 8) 245 | .padding(.top, 8) 246 | 247 | 248 | } 249 | DynamicIslandExpandedRegion(.center) { 250 | VStack(alignment: .center, spacing: 6.0) { 251 | HStack { 252 | Text("\(teamAScore)") 253 | .font(.title) 254 | .fontWeight(.bold) 255 | 256 | Text(":") 257 | .font(.title) 258 | .fontWeight(.bold) 259 | .foregroundStyle(.primary) 260 | 261 | Text("\(teamBScore)") 262 | .font(.title) 263 | .fontWeight(.bold) 264 | } 265 | .padding(.horizontal, 5.0) 266 | .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 8, style: .continuous)) 267 | 268 | HStack(alignment: .center, spacing: 2.0) { 269 | Text(timerInterval: matchRemainingTime, countsDown: true) 270 | .multilineTextAlignment(.center) 271 | .frame(width: 50) 272 | .monospacedDigit() 273 | .font(.footnote) 274 | .foregroundStyle(.white) 275 | } 276 | 277 | VStack(alignment: .center, spacing: 1.0) { 278 | Link(destination: URL(string: "la://my.app/stats")!) { 279 | Text("See stats 📊") 280 | }.padding(.vertical, 5).padding(.horizontal, 5) 281 | Text(matchName) 282 | .font(.footnote) 283 | .foregroundStyle(.white) 284 | } 285 | 286 | } 287 | .padding(.vertical, 6.0) 288 | } 289 | } compactLeading: { 290 | HStack { 291 | if let uiImageTeamA = UIImage(contentsOfFile: teamALogo) 292 | { 293 | Image(uiImage: uiImageTeamA) 294 | .resizable() 295 | .frame(width: 26, height: 26) 296 | } 297 | 298 | Text("\(teamAScore)") 299 | .font(.title) 300 | .fontWeight(.bold) 301 | } 302 | } compactTrailing: { 303 | HStack { 304 | Text("\(teamBScore)") 305 | .font(.title) 306 | .fontWeight(.bold) 307 | if let uiImageTeamB = UIImage(contentsOfFile: teamBLogo) 308 | { 309 | Image(uiImage: uiImageTeamB) 310 | .resizable() 311 | .frame(width: 26, height: 26) 312 | } 313 | } 314 | } minimal: { 315 | ZStack { 316 | if let uiImageTeamA = UIImage(contentsOfFile: teamALogo) 317 | { 318 | Image(uiImage: uiImageTeamA) 319 | .resizable() 320 | .frame(width: 26, height: 26) 321 | .offset(x:-6) 322 | } 323 | 324 | if let uiImageTeamB = UIImage(contentsOfFile: teamBLogo) 325 | { 326 | Image(uiImage: uiImageTeamB) 327 | .resizable() 328 | .frame(width: 26, height: 26) 329 | .offset(x:6) 330 | } 331 | } 332 | } 333 | } 334 | } 335 | } 336 | 337 | extension LiveActivitiesAppAttributes { 338 | func prefixedKey(_ key: String) -> String { 339 | return "\(id)_\(key)" 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /ios/live_activities/Sources/live_activities/LiveActivitiesPlugin.swift: -------------------------------------------------------------------------------- 1 | import ActivityKit 2 | import Flutter 3 | import UIKit 4 | import Foundation 5 | import CryptoKit 6 | 7 | @available(iOS 16.1, *) 8 | class FlutterAlertConfig { 9 | let _title:String 10 | let _body:String 11 | let _sound:String? 12 | 13 | init(title:String, body:String, sound:String?) { 14 | _title = title; 15 | _body = body; 16 | _sound = sound; 17 | } 18 | 19 | func getAlertConfig() -> AlertConfiguration { 20 | return AlertConfiguration(title: LocalizedStringResource(stringLiteral: _title), body: LocalizedStringResource(stringLiteral: _body), sound: (_sound == nil) ? .default : AlertConfiguration.AlertSound.named(_sound!)); 21 | } 22 | } 23 | 24 | public class LiveActivitiesPlugin: NSObject, FlutterPlugin, FlutterStreamHandler, FlutterSceneLifeCycleDelegate { 25 | private var urlSchemeSink: FlutterEventSink? 26 | private var appGroupId: String? 27 | private var urlScheme: String? 28 | private var requireNotificationPermission: Bool = true 29 | private var sharedDefault: UserDefaults? 30 | private var appLifecycleLiveActivityIds = [String]() 31 | private var activityEventSink: FlutterEventSink? 32 | private var pushToStartTokenEventSink: FlutterEventSink? 33 | 34 | public static func register(with registrar: FlutterPluginRegistrar) { 35 | let channel = FlutterMethodChannel(name: "live_activities", binaryMessenger: registrar.messenger()) 36 | let urlSchemeChannel = FlutterEventChannel(name: "live_activities/url_scheme", binaryMessenger: registrar.messenger()) 37 | let activityStatusChannel = FlutterEventChannel(name: "live_activities/activity_status", binaryMessenger: registrar.messenger()) 38 | let pushToStartTokenUpdatesChannel = FlutterEventChannel(name: "live_activities/push_to_start_token_updates", binaryMessenger: registrar.messenger()) 39 | 40 | let instance = LiveActivitiesPlugin() 41 | 42 | registrar.addMethodCallDelegate(instance, channel: channel) 43 | urlSchemeChannel.setStreamHandler(instance) 44 | activityStatusChannel.setStreamHandler(instance) 45 | pushToStartTokenUpdatesChannel.setStreamHandler(instance) 46 | registrar.addApplicationDelegate(instance) 47 | registrar.addSceneDelegate(instance) 48 | } 49 | 50 | public func detachFromEngine(for registrar: FlutterPluginRegistrar) { 51 | urlSchemeSink = nil 52 | activityEventSink = nil 53 | pushToStartTokenEventSink = nil 54 | } 55 | 56 | public func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? { 57 | if let args = arguments as? String{ 58 | if (args == "urlSchemeStream") { 59 | urlSchemeSink = events 60 | } else if (args == "activityUpdateStream") { 61 | activityEventSink = events 62 | } else if (args == "pushToStartTokenUpdateStream") { 63 | pushToStartTokenEventSink = events 64 | startObservingPushToStartTokens() 65 | } 66 | } 67 | 68 | return nil 69 | } 70 | 71 | public func onCancel(withArguments arguments: Any?) -> FlutterError? { 72 | if let args = arguments as? String{ 73 | if (args == "urlSchemeStream") { 74 | urlSchemeSink = nil 75 | } else if (args == "activityUpdateStream") { 76 | activityEventSink = nil 77 | } else if (args == "pushToStartTokenUpdateStream") { 78 | pushToStartTokenEventSink = nil 79 | } 80 | } 81 | return nil 82 | } 83 | 84 | private func initializationGuard(result: @escaping FlutterResult) { 85 | if self.appGroupId == nil || self.sharedDefault == nil { 86 | result(FlutterError(code: "NEED_INIT", message: "you need to run 'init()' first with app group id to create live activity", details: nil)) 87 | } 88 | } 89 | 90 | public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { 91 | if (call.method == "areActivitiesSupported") { 92 | guard #available(iOS 16.1, *), !ProcessInfo.processInfo.isiOSAppOnMac else { 93 | result(false) 94 | return 95 | } 96 | result(true) 97 | return 98 | } 99 | 100 | if (call.method == "areActivitiesEnabled") { 101 | guard #available(iOS 16.1, *), !ProcessInfo.processInfo.isiOSAppOnMac else { 102 | result(false) 103 | return 104 | } 105 | 106 | result(ActivityAuthorizationInfo().areActivitiesEnabled) 107 | return 108 | } 109 | 110 | if (call.method == "allowsPushStart") { 111 | guard #available(iOS 17.2, *), !ProcessInfo.processInfo.isiOSAppOnMac else { 112 | result(false) 113 | return 114 | } 115 | 116 | // This is iOS 17.2+ so push-to-start is supported 117 | result(true) 118 | return 119 | } 120 | 121 | if #available(iOS 16.1, *) { 122 | switch call.method { 123 | case "init": 124 | guard let args = call.arguments as? [String: Any] else { 125 | return 126 | } 127 | 128 | self.urlScheme = args["urlScheme"] as? String; 129 | 130 | if let appGroupId = args["appGroupId"] as? String { 131 | self.appGroupId = appGroupId 132 | sharedDefault = UserDefaults(suiteName: self.appGroupId)! 133 | result(nil) 134 | } else { 135 | result(FlutterError(code: "WRONG_ARGS", message: "argument are not valid, check if 'appGroupId' is valid", details: nil)) 136 | } 137 | 138 | self.requireNotificationPermission = args["requireNotificationPermission"] as? Bool ?? true 139 | 140 | break 141 | case "createActivity": 142 | initializationGuard(result: result) 143 | guard let args = call.arguments as? [String: Any] else { 144 | result(FlutterError(code: "WRONG_ARGS", message: "Unknown data type in argument", details: nil)) 145 | return 146 | } 147 | 148 | if let data = args["data"] as? [String: Any], let activityId = args["activityId"] as? String? ?? nil { 149 | let removeWhenAppIsKilled = args["removeWhenAppIsKilled"] as? Bool ?? false 150 | let staleIn = args["staleIn"] as? Int? ?? nil 151 | createActivity(data: data, removeWhenAppIsKilled: removeWhenAppIsKilled, staleIn: staleIn, activityId: activityId, result: result) 152 | } else { 153 | result(FlutterError(code: "WRONG_ARGS", message: "argument are not valid, check if 'data' is valid", details: nil)) 154 | } 155 | break 156 | case "updateActivity": 157 | initializationGuard(result: result) 158 | guard let args = call.arguments as? [String: Any] else { 159 | result(FlutterError(code: "WRONG_ARGS", message: "Unknown data type in argument", details: nil)) 160 | return 161 | } 162 | if let activityId = args["activityId"] as? String, let data = args["data"] as? [String: Any] { 163 | let alertConfigMap = args["alertConfig"] as? [String:String?]; 164 | let alertTitle = alertConfigMap?["title"] as? String; 165 | let alertBody = alertConfigMap?["body"] as? String; 166 | let alertSound = alertConfigMap?["sound"] as? String; 167 | 168 | let alertConfig = (alertTitle == nil || alertBody == nil) ? nil : FlutterAlertConfig(title: alertTitle!, body: alertBody!, sound: alertSound); 169 | 170 | updateActivity(activityId: activityId, data: data, alertConfig: alertConfig, result: result) 171 | } else { 172 | result(FlutterError(code: "WRONG_ARGS", message: "argument are not valid, check if 'activityId', 'data' are valid", details: nil)) 173 | } 174 | break 175 | case "endActivity": 176 | guard let args = call.arguments as? [String: Any] else { 177 | result(FlutterError(code: "WRONG_ARGS", message: "Unknown data type in argument", details: nil)) 178 | return 179 | } 180 | if let activityId = args["activityId"] as? String { 181 | endActivity(activityId: activityId, result: result) 182 | } else { 183 | result(FlutterError(code: "WRONG_ARGS", message: "argument are not valid, check if 'activityId' is valid", details: nil)) 184 | } 185 | break 186 | case "getActivityState": 187 | guard let args = call.arguments as? [String: Any] else { 188 | result(FlutterError(code: "WRONG_ARGS", message: "Unknown data type in argument", details: nil)) 189 | return 190 | } 191 | if let activityId = args["activityId"] as? String { 192 | getActivityState(activityId: activityId, result: result) 193 | } else { 194 | result(FlutterError(code: "WRONG_ARGS", message: "argument are not valid, check if 'activityId' is valid", details: nil)) 195 | } 196 | break 197 | case "getPushToken": 198 | guard let args = call.arguments as? [String: Any] else { 199 | return 200 | } 201 | if let activityId = args["activityId"] as? String { 202 | getPushToken(activityId: activityId, result: result) 203 | } else { 204 | result(FlutterError(code: "WRONG_ARGS", message: "argument are not valid, check if 'activityId' is valid", details: nil)) 205 | } 206 | break 207 | case "getAllActivitiesIds": 208 | getAllActivitiesIds(result: result) 209 | break 210 | case "getAllActivities": 211 | getAllActivities(result: result) 212 | break 213 | case "endAllActivities": 214 | endAllActivities(result: result) 215 | break 216 | case "createOrUpdateActivity": 217 | initializationGuard(result: result) 218 | guard let args = call.arguments as? [String: Any] else { 219 | result(FlutterError(code: "WRONG_ARGS", message: "Unknown data type in argument", details: nil)) 220 | return 221 | } 222 | 223 | if let data = args["data"] as? [String: Any], let activityId = args["activityId"] as? String { 224 | let removeWhenAppIsKilled = args["removeWhenAppIsKilled"] as? Bool ?? false 225 | let staleIn = args["staleIn"] as? Int? ?? nil 226 | createOrUpdateActivity(data: data, activityId: activityId, removeWhenAppIsKilled: removeWhenAppIsKilled, staleIn: staleIn, result: result) 227 | } else { 228 | result(FlutterError(code: "WRONG_ARGS", message: "argument are not valid, check if 'data', 'activityId' is valid", details: nil)) 229 | } 230 | break 231 | default: 232 | break 233 | } 234 | } else { 235 | result(FlutterError(code: "WRONG_IOS_VERSION", message: "this version of iOS is not supported", details: nil)) 236 | } 237 | } 238 | 239 | @available(iOS 16.1, *) 240 | func createActivity(data: [String: Any], removeWhenAppIsKilled: Bool, staleIn: Int?, activityId: String? = nil, result: @escaping FlutterResult) { 241 | if self.requireNotificationPermission { 242 | let center = UNUserNotificationCenter.current() 243 | center.requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in 244 | if let error = error { 245 | result(FlutterError(code: "AUTHORIZATION_ERROR", message: "authorization error", details: error.localizedDescription)) 246 | } 247 | } 248 | } 249 | 250 | let liveDeliveryAttributes: LiveActivitiesAppAttributes 251 | if let activityId = activityId { 252 | let uuid = uuid5(name: activityId) 253 | liveDeliveryAttributes = LiveActivitiesAppAttributes(id: uuid) 254 | } else { 255 | liveDeliveryAttributes = LiveActivitiesAppAttributes() 256 | } 257 | let initialContentState = LiveActivitiesAppAttributes.LiveDeliveryData(appGroupId: appGroupId!) 258 | var deliveryActivity: Activity? 259 | let prefix = liveDeliveryAttributes.id 260 | 261 | for item in data { 262 | sharedDefault!.set(item.value, forKey: "\(prefix)_\(item.key)") 263 | } 264 | 265 | if #available(iOS 16.2, *){ 266 | let activityContent = ActivityContent( 267 | state: initialContentState, 268 | staleDate: staleIn != nil ? Calendar.current.date(byAdding: .minute, value: staleIn!, to: Date.now) : nil) 269 | do { 270 | deliveryActivity = try Activity.request( 271 | attributes: liveDeliveryAttributes, 272 | content: activityContent, 273 | pushType: .token) 274 | } catch (let error) { 275 | result(FlutterError(code: "LIVE_ACTIVITY_ERROR", message: "can't launch live activity", details: error.localizedDescription)) 276 | } 277 | } else { 278 | do { 279 | deliveryActivity = try Activity.request( 280 | attributes: liveDeliveryAttributes, 281 | contentState: initialContentState, 282 | pushType: .token) 283 | 284 | } catch (let error) { 285 | result(FlutterError(code: "LIVE_ACTIVITY_ERROR", message: "can't launch live activity", details: error.localizedDescription)) 286 | } 287 | } 288 | if (deliveryActivity != nil) { 289 | if removeWhenAppIsKilled { 290 | appLifecycleLiveActivityIds.append(deliveryActivity!.id) 291 | } 292 | monitorLiveActivity(deliveryActivity!) 293 | result(deliveryActivity!.id) 294 | } 295 | } 296 | 297 | @available(iOS 16.1, *) 298 | func updateActivity(activityId: String, data: [String: Any?], alertConfig: FlutterAlertConfig?, result: @escaping FlutterResult) { 299 | Task { 300 | let activities = await MainActor.run { Activity.activities } 301 | guard let activity = activities.first(where: { $0.id == activityId }) else { 302 | result(FlutterError(code: "ACTIVITY_ERROR", message: "Activity not found", details: nil)) 303 | return 304 | } 305 | 306 | let prefix = activity.attributes.id 307 | 308 | await MainActor.run { 309 | for (key, value) in data { 310 | if let value = value, !(value is NSNull) { 311 | sharedDefault?.set(value, forKey: "\(prefix)_\(key)") 312 | } else { 313 | sharedDefault?.removeObject(forKey: "\(prefix)_\(key)") 314 | } 315 | } 316 | } 317 | 318 | let updatedStatus = LiveActivitiesAppAttributes.LiveDeliveryData(appGroupId: self.appGroupId!) 319 | await activity.update(using: updatedStatus, alertConfiguration: alertConfig?.getAlertConfig()) 320 | 321 | result(nil) 322 | } 323 | } 324 | 325 | @available(iOS 16.1, *) 326 | func createOrUpdateActivity(data: [String: Any], activityId: String, removeWhenAppIsKilled: Bool, staleIn: Int?, result: @escaping FlutterResult) { 327 | Task { 328 | let uuid = uuid5(name: activityId) 329 | 330 | var activities: [Activity] = [] 331 | for _ in 0..<3 { // Try up to 3 times 332 | activities = await MainActor.run { Activity.activities } 333 | if !activities.isEmpty { 334 | break 335 | } 336 | try? await Task.sleep(for: .seconds(0.1)) 337 | } 338 | 339 | let existingActivity = activities.first { 340 | $0.attributes.id == uuid && $0.activityState != .dismissed && $0.activityState != .ended 341 | } 342 | 343 | if let activityId = existingActivity?.id { 344 | updateActivity(activityId: activityId, data: data, alertConfig: nil, result: result) 345 | } else { 346 | createActivity(data: data, removeWhenAppIsKilled: removeWhenAppIsKilled, staleIn: staleIn, activityId: activityId, result: result) 347 | } 348 | } 349 | } 350 | 351 | @available(iOS 16.1, *) 352 | func getActivityState(activityId: String, result: @escaping FlutterResult) { 353 | Task { 354 | let customId = uuid5(name: activityId) 355 | let matchingActivity = Activity.activities.first { 356 | $0.id == activityId || 357 | $0.attributes.id == customId || 358 | $0.attributes.id.uuidString.uppercased() == activityId.uppercased() 359 | } 360 | 361 | guard let activity = matchingActivity else { 362 | result(nil) 363 | return 364 | } 365 | 366 | let state = activityStateToString(activityState: activity.activityState) 367 | result(state) 368 | } 369 | } 370 | 371 | @available(iOS 16.1, *) 372 | func getPushToken(activityId: String, result: @escaping FlutterResult) { 373 | Task { 374 | var pushToken: String?; 375 | for activity in Activity.activities { 376 | if (activityId == activity.id) { 377 | if let data = activity.pushToken { 378 | pushToken = data.map { String(format: "%02x", $0) }.joined() 379 | } 380 | } 381 | } 382 | result(pushToken) 383 | } 384 | } 385 | 386 | @available(iOS 16.1, *) 387 | func endActivity(activityId: String, result: @escaping FlutterResult) { 388 | appLifecycleLiveActivityIds.removeAll { $0 == activityId } 389 | Task { 390 | await endActivitiesWithId(activityIds: [activityId]) 391 | result(nil) 392 | } 393 | } 394 | 395 | @available(iOS 16.1, *) 396 | func endAllActivities(result: @escaping FlutterResult) { 397 | Task { 398 | for activity in Activity.activities { 399 | await activity.end(dismissalPolicy: .immediate) 400 | } 401 | appLifecycleLiveActivityIds.removeAll() 402 | result(nil) 403 | } 404 | } 405 | 406 | private func startObservingPushToStartTokens() { 407 | if #available(iOS 17.2, *) { 408 | Task { 409 | for await data in Activity.pushToStartTokenUpdates { 410 | let token = data.map { String(format: "%02x", $0) }.joined() 411 | 412 | DispatchQueue.main.async { 413 | self.pushToStartTokenEventSink?(token) 414 | } 415 | } 416 | } 417 | } 418 | } 419 | 420 | @available(iOS 16.1, *) 421 | func getAllActivitiesIds(result: @escaping FlutterResult) { 422 | var activitiesId: [String] = [] 423 | for activity in Activity.activities { 424 | activitiesId.append(activity.id) 425 | } 426 | 427 | result(activitiesId) 428 | } 429 | 430 | @available(iOS 16.1, *) 431 | func getAllActivities(result: @escaping FlutterResult) { 432 | var activitiesState: [String: String] = [:] // Corrected here 433 | for activity in Activity.activities { 434 | activitiesState[activity.id] = activityStateToString(activityState: activity.activityState) 435 | } 436 | 437 | result(activitiesState) 438 | } 439 | 440 | @available(iOS 16.1, *) 441 | private func endActivitiesWithId(activityIds: [String]) async { 442 | for activity in Activity.activities { 443 | for id in activityIds { 444 | let customIdUuid = uuid5(name: id) 445 | if id == activity.id || 446 | id.uppercased() == activity.attributes.id.uuidString || 447 | customIdUuid == activity.attributes.id { 448 | await activity.end(dismissalPolicy: .immediate) 449 | break 450 | } 451 | } 452 | } 453 | } 454 | 455 | public func application(_ application: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool { 456 | let components = URLComponents(url: url, resolvingAgainstBaseURL: false) 457 | 458 | if components?.scheme == nil || components?.scheme != urlScheme { return false } 459 | 460 | var queryResult: Dictionary = Dictionary() 461 | 462 | queryResult["queryItems"] = components?.queryItems?.map({ (item) -> Dictionary in 463 | var queryItemResult: Dictionary = Dictionary() 464 | queryItemResult["name"] = item.name 465 | queryItemResult["value"] = item.value 466 | return queryItemResult 467 | }) 468 | queryResult["scheme"] = components?.scheme 469 | queryResult["host"] = components?.host 470 | queryResult["path"] = components?.path 471 | queryResult["url"] = components?.url?.absoluteString 472 | 473 | urlSchemeSink?.self(queryResult) 474 | return true 475 | } 476 | 477 | public func applicationWillTerminate(_ application: UIApplication) { 478 | if #available(iOS 16.1, *) { 479 | Task { 480 | await self.endActivitiesWithId(activityIds: self.appLifecycleLiveActivityIds) 481 | } 482 | } 483 | } 484 | 485 | struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable { 486 | public typealias LiveDeliveryData = ContentState 487 | 488 | public struct ContentState: Codable, Hashable { 489 | var appGroupId: String 490 | } 491 | 492 | var id = UUID() 493 | } 494 | 495 | @available(iOS 16.1, *) 496 | private func monitorLiveActivity(_ activity: Activity) { 497 | Task { 498 | for await state in activity.activityStateUpdates { 499 | switch state { 500 | case .active: 501 | monitorTokenChanges(activity) 502 | case .dismissed, .ended: 503 | DispatchQueue.main.async { 504 | var response: Dictionary = Dictionary() 505 | response["activityId"] = activity.id 506 | response["status"] = "ended" 507 | self.activityEventSink?.self(response) 508 | } 509 | case .stale: 510 | DispatchQueue.main.async { 511 | var response: Dictionary = Dictionary() 512 | response["activityId"] = activity.id 513 | response["status"] = "stale" 514 | self.activityEventSink?.self(response) 515 | } 516 | @unknown default: 517 | DispatchQueue.main.async { 518 | var response: Dictionary = Dictionary() 519 | response["activityId"] = activity.id 520 | response["status"] = "unknown" 521 | self.activityEventSink?.self(response) 522 | } 523 | } 524 | } 525 | } 526 | } 527 | 528 | @available(iOS 16.1, *) 529 | private func monitorTokenChanges(_ activity: Activity) { 530 | Task { 531 | for await data in activity.pushTokenUpdates { 532 | DispatchQueue.main.async { 533 | var response: Dictionary = Dictionary() 534 | let pushToken = data.map {String(format: "%02x", $0)}.joined() 535 | response["token"] = pushToken 536 | response["activityId"] = activity.id 537 | response["status"] = "active" 538 | self.activityEventSink?.self(response) 539 | } 540 | } 541 | } 542 | } 543 | 544 | @available(iOS 16.1, *) 545 | private func activityStateToString(activityState: ActivityState) -> String { 546 | switch activityState { 547 | case .active: 548 | return "active" 549 | case .ended: 550 | return "ended" 551 | case .dismissed: 552 | return "dismissed" 553 | case .stale: 554 | return "stale" 555 | @unknown default: 556 | return "unknown" 557 | } 558 | } 559 | 560 | private func uuid5(namespace: UUID = UUID(uuidString: "6ba7b810-9dad-11d1-80b4-00c04fd430c8")!, name: String) -> UUID { 561 | // Convert namespace UUID to bytes 562 | var namespaceBytes = withUnsafeBytes(of: namespace.uuid) { Data($0) } 563 | 564 | // Append the name bytes (as UTF-8) 565 | let nameBytes = Data(name.utf8) 566 | namespaceBytes.append(nameBytes) 567 | 568 | // SHA1 hash 569 | let hash = Insecure.SHA1.hash(data: namespaceBytes) 570 | 571 | // Take the first 16 bytes 572 | var bytes = [UInt8](hash.prefix(16)) 573 | 574 | // Set UUID version to 5 (0101) 575 | bytes[6] = (bytes[6] & 0x0F) | 0x50 576 | 577 | // Set UUID variant to RFC 4122 (10xx) 578 | bytes[8] = (bytes[8] & 0x3F) | 0x80 579 | 580 | // Convert bytes to UUID 581 | let uuid = uuid_t(bytes[0], bytes[1], bytes[2], bytes[3], 582 | bytes[4], bytes[5], bytes[6], bytes[7], 583 | bytes[8], bytes[9], bytes[10], bytes[11], 584 | bytes[12], bytes[13], bytes[14], bytes[15]) 585 | return UUID(uuid: uuid) 586 | } 587 | 588 | } 589 | --------------------------------------------------------------------------------