Skip to content

Commit

Permalink
Implement Live Activity integrated with OneSignal
Browse files Browse the repository at this point in the history
  • Loading branch information
brismithers committed Mar 21, 2024
1 parent e9cf5dd commit 891ed79
Show file tree
Hide file tree
Showing 13 changed files with 507 additions and 43 deletions.
225 changes: 225 additions & 0 deletions ios/Runner.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

19 changes: 12 additions & 7 deletions ios/Runner/AppDelegate.swift
Expand Up @@ -3,11 +3,16 @@ import Flutter

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)

// Register Flutter channels, specifically for Live Activities
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
LiveActivitiesManager.register(controller: controller)

return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
2 changes: 2 additions & 0 deletions ios/Runner/Info.plist
Expand Up @@ -49,5 +49,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>NSSupportsLiveActivities</key>
<true/>
</dict>
</plist>
71 changes: 71 additions & 0 deletions ios/Runner/LiveActivitiesManager.swift
@@ -0,0 +1,71 @@
import Foundation

import ActivityKit
import Flutter
import Foundation

class LiveActivitiesManager {
private static var callbackChannel: FlutterMethodChannel? = nil
private static var managerChannel: FlutterMethodChannel? = nil

public static func register(controller: FlutterViewController) {
// Setup the iOS native->Flutter channel. This is primarily used to allow iOS to tell Flutter
// when there is a new Live Activity update push token.
// The name here must be equivalent to the name for `_callbackMethodChannel` in `lib/live_activities_manager.dart`
callbackChannel = FlutterMethodChannel(
name: "com.example.flutter_application_la/liveActivitiesCallback",
binaryMessenger: controller.binaryMessenger
)

// Setup the Flutter->iOS native channel. Currently supports `startLiveActivity` but can be
// expanded for more if needed.
// The name here must be equivalent to the name for `_managerMethodChannel` in `lib/live_activities_manager.dart`
managerChannel = FlutterMethodChannel(
name: "com.example.flutter_application_la/liveActivitiesManager",
binaryMessenger: controller.binaryMessenger
)
managerChannel?.setMethodCallHandler(handleMethodCall)
}

static func handleMethodCall(call: FlutterMethodCall, result: FlutterResult) {
switch call.method {
case "startLiveActivity":
LiveActivitiesManager.startLiveActivity(
data: call.arguments as? Dictionary<String,Any> ?? [String: Any](),
result: result)
break
default:
result(FlutterMethodNotImplemented)
}
}

static func startLiveActivity(data: [String: Any], result: FlutterResult) {
if #unavailable(iOS 16.1) {
result(FlutterError(code: "1", message: "Live activity supported on 16.1 and higher", details: nil))
}

let attributes = WidgetExtensionAttributes(name: data["name"] as? String ?? "LA Title")

let state = WidgetExtensionAttributes.ContentState(
emoji: data["emoji"] as? String ?? "😀"
)

if #available(iOS 16.1, *) {
do {
let newActivity = try Activity<WidgetExtensionAttributes>.request(
attributes: attributes,
contentState: state,
pushType: .token)

Task {
for await pushToken in newActivity.pushTokenUpdates {
let token = pushToken.map {String(format: "%02x", $0)}.joined()
callbackChannel?.invokeMethod("updatePushTokenCallback", arguments: ["activityId": data["activityId"], "token": token ])
}
}
} catch let error {
result(FlutterError(code: "2", message: "Error requesting live activity", details: nil))
}
}
}
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,13 @@
{
"images" : [
{
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 6 additions & 0 deletions ios/WidgetExtension/Assets.xcassets/Contents.json
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
@@ -0,0 +1,11 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
11 changes: 11 additions & 0 deletions ios/WidgetExtension/Info.plist
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.widgetkit-extension</string>
</dict>
</dict>
</plist>
9 changes: 9 additions & 0 deletions ios/WidgetExtension/WidgetExtensionBundle.swift
@@ -0,0 +1,9 @@
import WidgetKit
import SwiftUI

@main
struct WidgetExtensionBundle: WidgetBundle {
var body: some Widget {
WidgetExtensionLiveActivity()
}
}
51 changes: 51 additions & 0 deletions ios/WidgetExtension/WidgetExtensionLiveActivity.swift
@@ -0,0 +1,51 @@
import ActivityKit
import WidgetKit
import SwiftUI

struct WidgetExtensionAttributes: ActivityAttributes {
public struct ContentState: Codable, Hashable {
// Dynamic stateful properties about your activity go here!
var emoji: String
}

// Fixed non-changing properties about your activity go here!
var name: String
}

@available(iOS 16.1, *)
struct WidgetExtensionLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: WidgetExtensionAttributes.self) { context in
// Lock screen/banner UI goes here
VStack {
Text("Hello \(context.attributes.name)")
Text(context.state.emoji)
}
.activityBackgroundTint(Color.cyan)
.activitySystemActionForegroundColor(Color.black)

} dynamicIsland: { context in
DynamicIsland {
// Expanded UI goes here. Compose the expanded UI through
// various regions, like leading/trailing/center/bottom
DynamicIslandExpandedRegion(.leading) {
Text("Leading")
}
DynamicIslandExpandedRegion(.trailing) {
Text("Trailing")
}
DynamicIslandExpandedRegion(.bottom) {
Text("Bottom \(context.state.emoji)")
}
} compactLeading: {
Text("L")
} compactTrailing: {
Text("T \(context.state.emoji)")
} minimal: {
Text(context.state.emoji)
}
.widgetURL(URL(string: "http://www.onesignal.com"))
.keylineTint(Color.red)
}
}
}
34 changes: 34 additions & 0 deletions lib/live_actiivties_manager.dart
@@ -0,0 +1,34 @@
import 'dart:developer';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:onesignal_flutter/onesignal_flutter.dart';

// The flutter version of LiveActivitiesManager. The other side of these channels is in `ios/Runner/LiveActivitiesManager.swift`.
class LiveActivitiesManager {
static const MethodChannel _managerMethodChannel = MethodChannel('com.example.flutter_application_la/liveActivitiesManager');
static const MethodChannel _callbackMethodChannel = MethodChannel('com.example.flutter_application_la/liveActivitiesCallback');

static register() {
_callbackMethodChannel.setMethodCallHandler(_handleCallback);
}

static Future<Null> _handleCallback(MethodCall call) async {
var args = call.arguments.cast<String, dynamic>();
switch (call.method) {
case 'updatePushTokenCallback':
OneSignal.LiveActivities.enterLiveActivity(args["activityId"], args["token"]);
default:
log("Unrecognized callback method");
}

return null;
}

static Future<void> startLiveActivity(String activityId, String name, String emoji) async {
try {
await _managerMethodChannel.invokeListMethod('startLiveActivity', {'activityId': activityId, 'name': name, 'emoji': emoji});
} catch (e, st) {
log(e.toString(), stackTrace: st);
}
}
}
87 changes: 51 additions & 36 deletions lib/main.dart
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_application_la/live_actiivties_manager.dart';
import 'package:onesignal_flutter/onesignal_flutter.dart';

void main() {
Expand All @@ -11,6 +12,8 @@ class MyApp extends StatelessWidget {
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
// Initialize the channels
LiveActivitiesManager.register();

//Remove this method to stop OneSignal Debugging
OneSignal.Debug.setLogLevel(OSLogLevel.verbose);
Expand Down Expand Up @@ -65,17 +68,12 @@ class MyHomePage extends StatefulWidget {
}

class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
final TextEditingController _activityIdController = TextEditingController(text: '');
final TextEditingController _nameController = TextEditingController(text: 'Test LA Name');
final TextEditingController _emojiController = TextEditingController(text: '😀');

void _incrementCounter() {
setState(() {
// This call to setState tells the Flutter framework that something has
// changed in this State, which causes it to rerun the build method below
// so that the display can reflect the updated values. If we changed
// _counter without calling setState(), then the build method would not be
// called again, and so nothing would appear to happen.
_counter++;
});
void _startLiveActivity() {
LiveActivitiesManager.startLiveActivity(_activityIdController.text, _nameController.text, _emojiController.text);
}

@override
Expand All @@ -99,35 +97,52 @@ class _MyHomePageState extends State<MyHomePage> {
body: Center(
// Center is a layout widget. It takes a single child and positions it
// in the middle of the parent.
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Text(
'You have pushed the button this many times:',
),
Text(
'$_counter',
style: Theme.of(context).textTheme.headlineMedium,
),
],
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
// Column is also a layout widget. It takes a list of children and
// arranges them vertically. By default, it sizes itself to fit its
// children horizontally, and tries to be as tall as its parent.
//
// Column has various properties to control how it sizes itself and
// how it positions its children. Here we use mainAxisAlignment to
// center the children vertically; the main axis here is the vertical
// axis because Columns are vertical (the cross axis would be
// horizontal).
//
// TRY THIS: Invoke "debug painting" (choose the "Toggle Debug Paint"
// action in the IDE, or press "p" in the console), to see the
// wireframe for each widget.
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
TextFormField(
controller: _activityIdController,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: 'Activity ID:',
),
),
TextFormField(
controller: _nameController,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: 'Name:',
),
),
TextFormField(
controller: _emojiController,
decoration: const InputDecoration(
border: UnderlineInputBorder(),
labelText: 'Emoji:',
),
),
],
)
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
onPressed: _startLiveActivity,
tooltip: 'Start Live Activity',
child: const Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
Expand Down

0 comments on commit 891ed79

Please sign in to comment.