Implement live activity token rotation and server synchronization for push notifications
This commit is contained in:
@@ -2,112 +2,152 @@ import UIKit
|
||||
import background_fetch
|
||||
import ActivityKit
|
||||
import Flutter
|
||||
import Security
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
private var methodChannel: FlutterMethodChannel?
|
||||
private var methodChannel: FlutterMethodChannel?
|
||||
private var tokenRotationObserver: NSObjectProtocol?
|
||||
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||
fatalError("rootViewController is not type FlutterViewController")
|
||||
}
|
||||
methodChannel = FlutterMethodChannel(name: "app.firka/liveactivity",
|
||||
binaryMessenger: controller as! FlutterBinaryMessenger)
|
||||
methodChannel?.setMethodCallHandler({
|
||||
[weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
|
||||
guard call.method == "createLiveActivity" || call.method == "endLiveActivity" || call.method == "updateLiveActivity" else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
self?.handleMethodCall(call, result: result)
|
||||
})
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
override func applicationWillTerminate(_ application: UIApplication) {
|
||||
if #available(iOS 16.2, *) {
|
||||
LiveActivityManager.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
if call.method == "createLiveActivity" {
|
||||
if let args = call.arguments as? [String: Any] {
|
||||
lessonDataDictionary = args
|
||||
globalLessonData = LessonData(from: lessonDataDictionary)
|
||||
print("swift: megkapott flutter adatok:",lessonDataDictionary)
|
||||
print("Live Activity bekapcsolva az eszközön: ",checkLiveActivityFeatureAvailable())
|
||||
if(checkLiveActivityFeatureAvailable()) {
|
||||
createLiveActivity(with: lessonDataDictionary)
|
||||
result(checkLiveActivityFeatureAvailable())
|
||||
} else {
|
||||
result(nil)
|
||||
}
|
||||
|
||||
} else {
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid iOS arguments received", details: nil))
|
||||
}
|
||||
} else if call.method == "updateLiveActivity" {
|
||||
if let args = call.arguments as? [String: Any] {
|
||||
lessonDataDictionary = args
|
||||
globalLessonData = LessonData(from: lessonDataDictionary)
|
||||
updateLiveActivity(with: lessonDataDictionary)
|
||||
result(nil)
|
||||
} else {
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid iOS arguments received", details: nil))
|
||||
override func application(
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
guard let controller = window?.rootViewController as? FlutterViewController else {
|
||||
fatalError("rootViewController is not type FlutterViewController")
|
||||
}
|
||||
} else if call.method == "endLiveActivity" {
|
||||
endLiveActivity()
|
||||
result(nil)
|
||||
methodChannel = FlutterMethodChannel(
|
||||
name: "app.firka/liveactivity",
|
||||
binaryMessenger: controller as! FlutterBinaryMessenger
|
||||
)
|
||||
methodChannel?.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
|
||||
guard call.method == "createLiveActivity"
|
||||
|| call.method == "endLiveActivity"
|
||||
|| call.method == "updateLiveActivity"
|
||||
else {
|
||||
result(FlutterMethodNotImplemented)
|
||||
return
|
||||
}
|
||||
self?.handleMethodCall(call, result: result)
|
||||
})
|
||||
|
||||
// Token rotation figyelése: ha az APNs új tokent ad, értesítjük Flutter-t
|
||||
tokenRotationObserver = NotificationCenter.default.addObserver(
|
||||
forName: NSNotification.Name("LiveActivityTokenUpdated"),
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
if let newToken = notification.object as? String {
|
||||
let deviceId = self?.getOrCreateDeviceId() ?? ""
|
||||
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
||||
self?.methodChannel?.invokeMethod("liveActivityTokenUpdated", arguments: [
|
||||
"pushToken": newToken,
|
||||
"deviceId": deviceId,
|
||||
"bundleId": bundleId,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
|
||||
private func createLiveActivity(with activityData: [String: Any]) -> String? {
|
||||
var lessonData = LessonData(from: activityData)
|
||||
print("Live Activity létrehozása...")
|
||||
if #available(iOS 16.2, *) {
|
||||
LiveActivityManager.create()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private func updateLiveActivity(with activityData: [String: Any]) {
|
||||
let lessonData = LessonData(from: activityData)
|
||||
print("swift: megkapott flutter adatok:",lessonDataDictionary)
|
||||
print("Live Activity frissítés...")
|
||||
if #available(iOS 16.2, *) {
|
||||
LiveActivityManager.update()
|
||||
}
|
||||
}
|
||||
override func applicationWillTerminate(_ application: UIApplication) {
|
||||
if #available(iOS 16.2, *) {
|
||||
LiveActivityManager.stop()
|
||||
}
|
||||
if let observer = tokenRotationObserver {
|
||||
NotificationCenter.default.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func endLiveActivity() {
|
||||
print("Live Activity befejezése...")
|
||||
if #available(iOS 16.2, *) {
|
||||
LiveActivityManager.stop()
|
||||
}
|
||||
}
|
||||
|
||||
private func checkIfLiveActivityExists() -> Bool {
|
||||
if let activityID = activityID {
|
||||
if #available(iOS 16.2, *) {
|
||||
return LiveActivityManager.isRunning(activityID)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func checkLiveActivityFeatureAvailable() -> Bool {
|
||||
if #available(iOS 16.2, *) {
|
||||
guard ActivityAuthorizationInfo().areActivitiesEnabled else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
private func handleMethodCall(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
||||
if call.method == "createLiveActivity" {
|
||||
guard let args = call.arguments as? [String: Any] else {
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid iOS arguments received", details: nil))
|
||||
return
|
||||
}
|
||||
lessonDataDictionary = args
|
||||
globalLessonData = LessonData(from: lessonDataDictionary)
|
||||
print("swift: megkapott flutter adatok:", lessonDataDictionary)
|
||||
print("Live Activity bekapcsolva az eszközön:", checkLiveActivityFeatureAvailable())
|
||||
|
||||
guard checkLiveActivityFeatureAvailable() else {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if #available(iOS 16.2, *) {
|
||||
LiveActivityManager.create { [weak self] pushToken in
|
||||
guard let self = self, let token = pushToken else {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
let deviceId = self.getOrCreateDeviceId()
|
||||
let bundleId = Bundle.main.bundleIdentifier ?? ""
|
||||
result([
|
||||
"pushToken": token,
|
||||
"deviceId": deviceId,
|
||||
"bundleId": bundleId,
|
||||
])
|
||||
}
|
||||
} else {
|
||||
result(nil)
|
||||
}
|
||||
|
||||
} else if call.method == "updateLiveActivity" {
|
||||
guard let args = call.arguments as? [String: Any] else {
|
||||
result(FlutterError(code: "INVALID_ARGUMENTS", message: "Invalid iOS arguments received", details: nil))
|
||||
return
|
||||
}
|
||||
lessonDataDictionary = args
|
||||
globalLessonData = LessonData(from: lessonDataDictionary)
|
||||
if #available(iOS 16.2, *) {
|
||||
LiveActivityManager.update()
|
||||
}
|
||||
result(nil)
|
||||
|
||||
} else if call.method == "endLiveActivity" {
|
||||
if #available(iOS 16.2, *) {
|
||||
LiveActivityManager.stop()
|
||||
}
|
||||
result(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Keychain device ID
|
||||
|
||||
private func getOrCreateDeviceId() -> String {
|
||||
let keychainKey = "refilc.live.device_id"
|
||||
let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: keychainKey,
|
||||
kSecReturnData as String: true,
|
||||
kSecMatchLimit as String: kSecMatchLimitOne,
|
||||
]
|
||||
var item: CFTypeRef?
|
||||
if SecItemCopyMatching(query as CFDictionary, &item) == errSecSuccess,
|
||||
let data = item as? Data,
|
||||
let existingId = String(data: data, encoding: .utf8) {
|
||||
return existingId
|
||||
}
|
||||
// Nem létezik – generálunk egyet
|
||||
let newId = UUID().uuidString
|
||||
let addQuery: [String: Any] = [
|
||||
kSecClass as String: kSecClassGenericPassword,
|
||||
kSecAttrAccount as String: keychainKey,
|
||||
kSecValueData as String: newId.data(using: .utf8)!,
|
||||
]
|
||||
SecItemAdd(addQuery as CFDictionary, nil)
|
||||
return newId
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private func checkLiveActivityFeatureAvailable() -> Bool {
|
||||
if #available(iOS 16.2, *) {
|
||||
return ActivityAuthorizationInfo().areActivitiesEnabled
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,3 +10,4 @@ import Foundation
|
||||
var lessonDataDictionary: [String: Any] = [:]
|
||||
var globalLessonData = LessonData(from: lessonDataDictionary)
|
||||
var activityID: String? = ""
|
||||
var activityPushToken: String? = nil
|
||||
|
||||
@@ -17,7 +17,7 @@ public struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable {
|
||||
var nextSubject: String
|
||||
var nextRoom: String
|
||||
}
|
||||
|
||||
|
||||
public var id = UUID()
|
||||
}
|
||||
|
||||
@@ -26,67 +26,141 @@ final class LiveActivityManager {
|
||||
static let shared = LiveActivityManager()
|
||||
var currentActivity: Activity<LiveActivitiesAppAttributes>?
|
||||
|
||||
class func create() {
|
||||
|
||||
Task {
|
||||
/// Létrehozza a Live Activity-t pushType: .token-nel, majd visszaadja az APNs push tokent.
|
||||
class func create(completion: @escaping (String?) -> Void) {
|
||||
Task {
|
||||
do {
|
||||
let contentState = LiveActivitiesAppAttributes.ContentState(
|
||||
color: globalLessonData.color,
|
||||
icon: globalLessonData.icon,
|
||||
index: globalLessonData.index,
|
||||
title: globalLessonData.title,
|
||||
subtitle: globalLessonData.subtitle,
|
||||
description: globalLessonData.description,
|
||||
startDate: globalLessonData.startDate,
|
||||
endDate: globalLessonData.endDate,
|
||||
date: globalLessonData.date,
|
||||
nextSubject: globalLessonData.nextSubject,
|
||||
nextRoom: globalLessonData.nextRoom
|
||||
)
|
||||
|
||||
let activityContent = ActivityContent(
|
||||
state: contentState,
|
||||
staleDate: globalLessonData.endDate,
|
||||
relevanceScore: 0
|
||||
)
|
||||
|
||||
let activity = try Activity<LiveActivitiesAppAttributes>.request(
|
||||
attributes: LiveActivitiesAppAttributes(),
|
||||
content: activityContent,
|
||||
pushType: .token
|
||||
)
|
||||
|
||||
activityID = activity.id
|
||||
print("Live Activity létrehozva. Azonosító: \(activity.id)")
|
||||
|
||||
// Az APNs token aszinkron érkezik – megvárjuk az elsőt
|
||||
for await tokenData in activity.pushTokenUpdates {
|
||||
let tokenHex = tokenData.map { String(format: "%02x", $0) }.joined()
|
||||
activityPushToken = tokenHex
|
||||
print("Live Activity push token: \(tokenHex)")
|
||||
completion(tokenHex)
|
||||
// Token rotation figyelése a háttérben
|
||||
Task { await monitorTokenRotation(activity: activity) }
|
||||
break
|
||||
}
|
||||
} catch {
|
||||
print("Hiba történt a Live Activity létrehozásakor: \(error)")
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Token rotation figyelése: ha az APNs új tokent ad ki, értesítjük a Flutter oldalt.
|
||||
private class func monitorTokenRotation(activity: Activity<LiveActivitiesAppAttributes>) async {
|
||||
var isFirst = true
|
||||
for await tokenData in activity.pushTokenUpdates {
|
||||
if isFirst { isFirst = false; continue } // Az első tokent már kezeltük
|
||||
let tokenHex = tokenData.map { String(format: "%02x", $0) }.joined()
|
||||
activityPushToken = tokenHex
|
||||
print("Live Activity push token frissítve (rotation): \(tokenHex)")
|
||||
NotificationCenter.default.post(
|
||||
name: NSNotification.Name("LiveActivityTokenUpdated"),
|
||||
object: tokenHex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class func update() {
|
||||
Task {
|
||||
for activity in Activity<LiveActivitiesAppAttributes>.activities {
|
||||
do {
|
||||
let contentState = LiveActivitiesAppAttributes.ContentState(color: globalLessonData.color, icon: globalLessonData.icon, index: globalLessonData.index, title: globalLessonData.title, subtitle: globalLessonData.subtitle, description: globalLessonData.description, startDate: globalLessonData.startDate, endDate: globalLessonData.endDate, date: globalLessonData.date, nextSubject: globalLessonData.nextSubject, nextRoom: globalLessonData.nextRoom)
|
||||
|
||||
let activityContent = ActivityContent(state: contentState, staleDate: globalLessonData.endDate, relevanceScore: 0)
|
||||
|
||||
let activity = try Activity<LiveActivitiesAppAttributes>.request(
|
||||
attributes: LiveActivitiesAppAttributes(),
|
||||
content: activityContent,
|
||||
pushType: nil
|
||||
let contentState = LiveActivitiesAppAttributes.ContentState(
|
||||
color: globalLessonData.color,
|
||||
icon: globalLessonData.icon,
|
||||
index: globalLessonData.index,
|
||||
title: globalLessonData.title,
|
||||
subtitle: globalLessonData.subtitle,
|
||||
description: globalLessonData.description,
|
||||
startDate: globalLessonData.startDate,
|
||||
endDate: globalLessonData.endDate,
|
||||
date: globalLessonData.date,
|
||||
nextSubject: globalLessonData.nextSubject,
|
||||
nextRoom: globalLessonData.nextRoom
|
||||
)
|
||||
|
||||
|
||||
let activityContent = ActivityContent(
|
||||
state: contentState,
|
||||
staleDate: globalLessonData.endDate,
|
||||
relevanceScore: 0
|
||||
)
|
||||
|
||||
await activity.update(activityContent)
|
||||
activityID = activity.id
|
||||
print("Live Activity létrehozva. Azonosító: \(activity.id)")
|
||||
print("Live Activity frissítve. Azonosító: \(activity.id)")
|
||||
} catch {
|
||||
print("Hiba történt a Live Activity létrehozásakor: \(error)")
|
||||
print("Hiba történt a Live Activity frissítésekor: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class func update() {
|
||||
Task {
|
||||
for activity in Activity<LiveActivitiesAppAttributes>.activities {
|
||||
do {
|
||||
let contentState = LiveActivitiesAppAttributes.ContentState(color: globalLessonData.color, icon: globalLessonData.icon, index: globalLessonData.index, title: globalLessonData.title, subtitle: globalLessonData.subtitle, description: globalLessonData.description, startDate: globalLessonData.startDate, endDate: globalLessonData.endDate, date: globalLessonData.date, nextSubject: globalLessonData.nextSubject, nextRoom: globalLessonData.nextRoom)
|
||||
|
||||
let activityContent = ActivityContent(state: contentState, staleDate: globalLessonData.endDate, relevanceScore: 0)
|
||||
|
||||
await activity.update(activityContent)
|
||||
activityID = activity.id
|
||||
print("Live Activity frissítve. Azonosító: \(activity.id)")
|
||||
} catch {
|
||||
print("Hiba történt a Live Activity frissítésekor: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class func stop() {
|
||||
if (activityID != "") {
|
||||
if activityID != "" {
|
||||
Task {
|
||||
for activity in Activity<LiveActivitiesAppAttributes>.activities{
|
||||
let contentState = LiveActivitiesAppAttributes.ContentState(color: globalLessonData.color, icon: globalLessonData.icon, index: globalLessonData.index, title: globalLessonData.title, subtitle: globalLessonData.subtitle, description: globalLessonData.description, startDate: globalLessonData.startDate, endDate: globalLessonData.endDate, date: globalLessonData.date, nextSubject: globalLessonData.nextSubject, nextRoom: globalLessonData.nextRoom)
|
||||
|
||||
await activity.end(ActivityContent(state: contentState, staleDate: Date.distantFuture),dismissalPolicy: .immediate)
|
||||
for activity in Activity<LiveActivitiesAppAttributes>.activities {
|
||||
let contentState = LiveActivitiesAppAttributes.ContentState(
|
||||
color: globalLessonData.color,
|
||||
icon: globalLessonData.icon,
|
||||
index: globalLessonData.index,
|
||||
title: globalLessonData.title,
|
||||
subtitle: globalLessonData.subtitle,
|
||||
description: globalLessonData.description,
|
||||
startDate: globalLessonData.startDate,
|
||||
endDate: globalLessonData.endDate,
|
||||
date: globalLessonData.date,
|
||||
nextSubject: globalLessonData.nextSubject,
|
||||
nextRoom: globalLessonData.nextRoom
|
||||
)
|
||||
|
||||
await activity.end(
|
||||
ActivityContent(state: contentState, staleDate: Date.distantFuture),
|
||||
dismissalPolicy: .immediate
|
||||
)
|
||||
}
|
||||
activityID = nil
|
||||
activityPushToken = nil
|
||||
print("Live Activity sikeresen leállítva")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class func isRunning(_ activityID: String) -> Bool {
|
||||
for activity in Activity<LiveActivitiesAppAttributes>.activities {
|
||||
if activity.id == activityID {
|
||||
return true
|
||||
}
|
||||
for activity in Activity<LiveActivitiesAppAttributes>.activities {
|
||||
if activity.id == activityID {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:refilc/api/providers/liveactivity/platform_channel.dart';
|
||||
import 'package:refilc/api/providers/liveactivity/server_sync_provider.dart';
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/models/settings.dart';
|
||||
import 'package:refilc/ui/flutter_colorpicker/utils.dart';
|
||||
@@ -45,6 +46,7 @@ class LiveCardProvider extends ChangeNotifier {
|
||||
late Timer _timer;
|
||||
late final TimetableProvider _timetable;
|
||||
late final SettingsProvider _settings;
|
||||
final ServerSyncProvider _serverSync = ServerSyncProvider();
|
||||
|
||||
late Duration _delay;
|
||||
|
||||
@@ -59,6 +61,16 @@ class LiveCardProvider extends ChangeNotifier {
|
||||
_delay = settings.bellDelayEnabled
|
||||
? Duration(seconds: settings.bellDelay)
|
||||
: Duration.zero;
|
||||
|
||||
// Token rotation figyelése: ha iOS új APNs tokent ad ki, szinkronizáljuk a szerverrel
|
||||
PlatformChannel.onTokenUpdated = (pushToken, deviceId, bundleId) {
|
||||
_serverSync.refreshToken(
|
||||
pushToken: pushToken,
|
||||
bundleId: bundleId,
|
||||
liveActivityColor: '#${settings.liveActivityColor.toHexString().substring(2)}',
|
||||
);
|
||||
};
|
||||
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -325,15 +337,15 @@ class LiveCardProvider extends ChangeNotifier {
|
||||
currentState == LiveCardState.night)) {
|
||||
debugPrint(
|
||||
"Az első óra előtt állunk, kevesebb mint egy órával. Létrehozás...");
|
||||
PlatformChannel.createLiveActivity(toMap());
|
||||
hasActivityStarted = true;
|
||||
_createAndSync();
|
||||
} else if (!hasActivityStarted &&
|
||||
((currentState == LiveCardState.duringLesson &&
|
||||
currentLesson != null) ||
|
||||
currentState == LiveCardState.duringBreak)) {
|
||||
debugPrint("Óra van, vagy szünet, de nincs LiveActivity. létrehozás...");
|
||||
PlatformChannel.createLiveActivity(toMap());
|
||||
hasActivityStarted = true;
|
||||
_createAndSync();
|
||||
}
|
||||
|
||||
//UPDATE
|
||||
@@ -385,6 +397,25 @@ class LiveCardProvider extends ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _createAndSync() async {
|
||||
final result = await PlatformChannel.createLiveActivity(toMap());
|
||||
if (result != null) {
|
||||
final pushToken = result['pushToken'] ?? '';
|
||||
final deviceId = result['deviceId'] ?? '';
|
||||
final bundleId = result['bundleId'] ?? '';
|
||||
if (pushToken.isNotEmpty && deviceId.isNotEmpty) {
|
||||
await _serverSync.registerAndSync(
|
||||
deviceId: deviceId,
|
||||
pushToken: pushToken,
|
||||
bundleId: bundleId,
|
||||
liveActivityColor:
|
||||
'#${_settings.liveActivityColor.toHexString().substring(2)}',
|
||||
todayLessons: _today(_timetable),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool get show => currentState != LiveCardState.empty;
|
||||
Duration get delay => _delay;
|
||||
|
||||
|
||||
@@ -6,16 +6,41 @@ import 'package:flutter/services.dart';
|
||||
class PlatformChannel {
|
||||
static const MethodChannel _channel = MethodChannel('app.firka/liveactivity');
|
||||
|
||||
/// Létrehozza a Live Activity-t és visszaadja az APNs push tokent (ha elérhető).
|
||||
static Future<String?> createLiveActivity(
|
||||
/// Callback token rotation esetén (iOS APNs új tokent ad ki).
|
||||
/// A szerver szinkronizálásért felelős kód regisztrálhat ide.
|
||||
static void Function(String pushToken, String deviceId, String bundleId)? onTokenUpdated;
|
||||
|
||||
static void _setupTokenRotationListener() {
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
if (call.method == 'liveActivityTokenUpdated') {
|
||||
final args = call.arguments as Map?;
|
||||
if (args != null && onTokenUpdated != null) {
|
||||
onTokenUpdated!(
|
||||
args['pushToken'] as String? ?? '',
|
||||
args['deviceId'] as String? ?? '',
|
||||
args['bundleId'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Létrehozza a Live Activity-t és visszaadja az APNs push tokent,
|
||||
/// a device ID-t (Keychain UUID) és a bundle ID-t.
|
||||
static Future<Map<String, String>?> createLiveActivity(
|
||||
Map<String, dynamic> activityData) async {
|
||||
if (Platform.isIOS) {
|
||||
_setupTokenRotationListener();
|
||||
try {
|
||||
debugPrint("creating...");
|
||||
final String? pushToken = await _channel.invokeMethod<String>(
|
||||
debugPrint("creating live activity...");
|
||||
final result = await _channel.invokeMethod<Map>(
|
||||
'createLiveActivity', activityData);
|
||||
debugPrint("Live Activity push token: $pushToken");
|
||||
return pushToken;
|
||||
if (result == null) return null;
|
||||
return {
|
||||
'pushToken': result['pushToken'] as String? ?? '',
|
||||
'deviceId': result['deviceId'] as String? ?? '',
|
||||
'bundleId': result['bundleId'] as String? ?? '',
|
||||
};
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint("Hiba történt a Live Activity létrehozásakor: ${e.message}");
|
||||
}
|
||||
@@ -27,7 +52,7 @@ class PlatformChannel {
|
||||
Map<String, dynamic> activityData) async {
|
||||
if (Platform.isIOS) {
|
||||
try {
|
||||
debugPrint("updating...");
|
||||
debugPrint("updating live activity...");
|
||||
await _channel.invokeMethod('updateLiveActivity', activityData);
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint("Hiba történt a Live Activity frissítésekor: ${e.message}");
|
||||
@@ -38,11 +63,11 @@ class PlatformChannel {
|
||||
static Future<void> endLiveActivity() async {
|
||||
if (Platform.isIOS) {
|
||||
try {
|
||||
debugPrint("finishing...");
|
||||
debugPrint("ending live activity...");
|
||||
await _channel.invokeMethod('endLiveActivity');
|
||||
} on PlatformException catch (e) {
|
||||
debugPrint("Hiba történt a Live Activity befejezésekor: ${e.message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
134
refilc/lib/api/providers/liveactivity/server_sync_provider.dart
Normal file
134
refilc/lib/api/providers/liveactivity/server_sync_provider.dart
Normal file
@@ -0,0 +1,134 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:refilc/helpers/subject.dart';
|
||||
import 'package:refilc/utils/format.dart';
|
||||
import 'package:refilc_kreta_api/models/lesson.dart';
|
||||
|
||||
class ServerSyncProvider {
|
||||
static const String _baseUrl = 'https://live.firka.app';
|
||||
|
||||
String? _deviceId;
|
||||
|
||||
/// App indításkor hívandó: regisztrálja a device-t és feltölti a mai órarendet.
|
||||
Future<void> registerAndSync({
|
||||
required String deviceId,
|
||||
required String pushToken,
|
||||
required String bundleId,
|
||||
required String liveActivityColor,
|
||||
required List<Lesson> todayLessons,
|
||||
}) async {
|
||||
_deviceId = deviceId;
|
||||
|
||||
await _register(
|
||||
deviceId: deviceId,
|
||||
pushToken: pushToken,
|
||||
bundleId: bundleId,
|
||||
liveActivityColor: liveActivityColor,
|
||||
);
|
||||
|
||||
final validLessons = _filterLessons(todayLessons);
|
||||
if (validLessons.isNotEmpty) {
|
||||
await _uploadSchedule(deviceId: deviceId, lessons: validLessons);
|
||||
}
|
||||
}
|
||||
|
||||
/// Token rotation esetén frissíti a tokent a szerveren.
|
||||
Future<void> refreshToken({
|
||||
required String pushToken,
|
||||
required String bundleId,
|
||||
required String liveActivityColor,
|
||||
}) async {
|
||||
if (_deviceId == null) return;
|
||||
await _register(
|
||||
deviceId: _deviceId!,
|
||||
pushToken: pushToken,
|
||||
bundleId: bundleId,
|
||||
liveActivityColor: liveActivityColor,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _register({
|
||||
required String deviceId,
|
||||
required String pushToken,
|
||||
required String bundleId,
|
||||
required String liveActivityColor,
|
||||
}) async {
|
||||
try {
|
||||
final client = HttpClient();
|
||||
final uri = Uri.parse('$_baseUrl/register');
|
||||
final request = await client.postUrl(uri);
|
||||
request.headers.contentType = ContentType.json;
|
||||
request.write(jsonEncode({
|
||||
'device_id': deviceId,
|
||||
'apns_token': pushToken,
|
||||
'bundle_id': bundleId,
|
||||
'settings': {
|
||||
'live_activity_color': liveActivityColor,
|
||||
},
|
||||
}));
|
||||
final response =
|
||||
await request.close().timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
debugPrint('ServerSync register hiba: ${response.statusCode}');
|
||||
} else {
|
||||
debugPrint('ServerSync: device regisztrálva');
|
||||
}
|
||||
client.close();
|
||||
} catch (e) {
|
||||
debugPrint('ServerSync register kivétel: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _uploadSchedule({
|
||||
required String deviceId,
|
||||
required List<Lesson> lessons,
|
||||
}) async {
|
||||
try {
|
||||
final now = DateTime.now();
|
||||
final dateStr =
|
||||
'${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}';
|
||||
|
||||
final lessonsJson = lessons
|
||||
.map((l) => {
|
||||
'index': l.lessonIndex.toString(),
|
||||
'subject': l.subject.renamedTo ??
|
||||
ShortSubject.resolve(subject: l.subject).capital(),
|
||||
'icon': SubjectIcon.resolveName(subject: l.subject),
|
||||
'room': l.room.replaceAll('_', ' '),
|
||||
'description': l.description,
|
||||
'start': l.start.millisecondsSinceEpoch,
|
||||
'end': l.end.millisecondsSinceEpoch,
|
||||
})
|
||||
.toList();
|
||||
|
||||
final client = HttpClient();
|
||||
final uri = Uri.parse('$_baseUrl/schedule');
|
||||
final request = await client.postUrl(uri);
|
||||
request.headers.contentType = ContentType.json;
|
||||
request.write(jsonEncode({
|
||||
'device_id': deviceId,
|
||||
'date': dateStr,
|
||||
'lessons': lessonsJson,
|
||||
}));
|
||||
final response =
|
||||
await request.close().timeout(const Duration(seconds: 10));
|
||||
if (response.statusCode != 200 && response.statusCode != 201) {
|
||||
debugPrint('ServerSync schedule hiba: ${response.statusCode}');
|
||||
} else {
|
||||
debugPrint('ServerSync: ${lessons.length} óra feltöltve ($dateStr)');
|
||||
}
|
||||
client.close();
|
||||
} catch (e) {
|
||||
debugPrint('ServerSync schedule kivétel: $e');
|
||||
}
|
||||
}
|
||||
|
||||
List<Lesson> _filterLessons(List<Lesson> lessons) {
|
||||
return lessons
|
||||
.where((l) =>
|
||||
l.status?.name != 'Elmaradt' && l.subject.id != '' && !l.isEmpty)
|
||||
.toList()
|
||||
..sort((a, b) => a.start.compareTo(b.start));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user