diff --git a/refilc/ios/PrivacyInfo.xcprivacy b/refilc/ios/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..a1c5b3b --- /dev/null +++ b/refilc/ios/PrivacyInfo.xcprivacy @@ -0,0 +1,20 @@ + + + + + + NSPrivacyAccessedAPITypes + + + + NSPrivacyAccessedAPIType + NSPrivacyAccessedAPICategoryUserDefaults + + NSPrivacyAccessedAPITypeReasons + + CA92.1 + + + + + \ No newline at end of file diff --git a/refilc/ios/Runner.xcodeproj/project.pbxproj b/refilc/ios/Runner.xcodeproj/project.pbxproj index 806d9e6..e4a26c0 100644 --- a/refilc/ios/Runner.xcodeproj/project.pbxproj +++ b/refilc/ios/Runner.xcodeproj/project.pbxproj @@ -17,7 +17,13 @@ 3127F7A828EAEE8500C2EFB3 /* lesson_model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3127F7A728EAEE8500C2EFB3 /* lesson_model.swift */; }; 373A6ECB5FC71FE9D8AF2EDB /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1F0ADD56276103500A3016C8 /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 4F35BF332BE2FFA30098EF72 /* public_vars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F35BF322BE2FFA30098EF72 /* public_vars.swift */; }; + 4F35BF352BE2FFD80098EF72 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F35BF342BE2FFD80098EF72 /* LiveActivityManager.swift */; }; + 4F35BF362BE2FFD80098EF72 /* LiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F35BF342BE2FFD80098EF72 /* LiveActivityManager.swift */; }; + 4F35BF382BE300A70098EF72 /* lesson_model.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3127F7A728EAEE8500C2EFB3 /* lesson_model.swift */; }; + 4F35BF392BE300B10098EF72 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 4F35BF3A2BE301180098EF72 /* public_vars.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F35BF322BE2FFA30098EF72 /* public_vars.swift */; }; + 4F35BF3E2BE304550098EF72 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = 4F35BF3D2BE304550098EF72 /* PrivacyInfo.xcprivacy */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; @@ -73,6 +79,10 @@ 3127F7A728EAEE8500C2EFB3 /* lesson_model.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = lesson_model.swift; sourceTree = ""; }; 317DE77A294F6FFB002E323E /* livecard.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = livecard.entitlements; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4F35BF322BE2FFA30098EF72 /* public_vars.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = public_vars.swift; sourceTree = ""; }; + 4F35BF342BE2FFD80098EF72 /* LiveActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveActivityManager.swift; sourceTree = ""; }; + 4F35BF3B2BE303A40098EF72 /* app_group_directory.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = app_group_directory.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 4F35BF3D2BE304550098EF72 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; 707F8089D970F81C480F73C4 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -118,6 +128,7 @@ 3127F79728EAEDE300C2EFB3 /* Assets.xcassets */, 3127F79928EAEDE300C2EFB3 /* Info.plist */, 3127F7A528EAEE5900C2EFB3 /* livecard.swift */, + 4F35BF342BE2FFD80098EF72 /* LiveActivityManager.swift */, ); path = livecard; sourceTree = ""; @@ -125,6 +136,7 @@ 6640A963014A9D4F31026053 /* Frameworks */ = { isa = PBXGroup; children = ( + 4F35BF3B2BE303A40098EF72 /* app_group_directory.framework */, 1F0ADD56276103500A3016C8 /* Pods_Runner.framework */, 3127F73F28EAEC8A00C2EFB3 /* IntentsUI.framework */, 3127F75528EAECC800C2EFB3 /* WidgetKit.framework */, @@ -157,6 +169,7 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + 4F35BF3D2BE304550098EF72 /* PrivacyInfo.xcprivacy */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 3127F78F28EAEDE200C2EFB3 /* livecard */, @@ -187,6 +200,7 @@ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, + 4F35BF322BE2FFA30098EF72 /* public_vars.swift */, ); path = Runner; sourceTree = ""; @@ -242,7 +256,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1410; + LastSwiftUpdateCheck = 1530; LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { @@ -290,6 +304,7 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + 4F35BF3E2BE304550098EF72 /* PrivacyInfo.xcprivacy in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -320,6 +335,8 @@ buildActionMask = 12; files = ( ); + inputFileListPaths = ( + ); inputPaths = ( "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); @@ -372,7 +389,7 @@ 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; - buildActionMask = 2147483647; + buildActionMask = 12; files = ( ); inputPaths = ( @@ -391,9 +408,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 4F35BF362BE2FFD80098EF72 /* LiveActivityManager.swift in Sources */, 3127F7A828EAEE8500C2EFB3 /* lesson_model.swift in Sources */, 3127F7A428EAEE3D00C2EFB3 /* livecard.intentdefinition in Sources */, 3127F7A628EAEE5900C2EFB3 /* livecard.swift in Sources */, + 4F35BF3A2BE301180098EF72 /* public_vars.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -401,8 +420,11 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, + 4F35BF392BE300B10098EF72 /* AppDelegate.swift in Sources */, + 4F35BF382BE300A70098EF72 /* lesson_model.swift in Sources */, + 4F35BF352BE2FFD80098EF72 /* LiveActivityManager.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 4F35BF332BE2FFA30098EF72 /* public_vars.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -496,7 +518,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = 250; - DEVELOPMENT_TEAM = 4DKAF249F3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = reFilc; @@ -506,7 +528,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 5.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.refilc2.naplo; + PRODUCT_BUNDLE_IDENTIFIER = com.refilcrel.naplo; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -528,14 +550,14 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 250; - DEVELOPMENT_TEAM = 4DKAF249F3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = livecard/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = livecard; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSSupportsLiveActivities = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -544,7 +566,7 @@ MARKETING_VERSION = 5.0.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.refilc2.naplo.livecardpro; + PRODUCT_BUNDLE_IDENTIFIER = com.refilcrel.naplo.livecardpro; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -570,14 +592,14 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 250; - DEVELOPMENT_TEAM = 4DKAF249F3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = livecard/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = livecard; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSSupportsLiveActivities = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -585,7 +607,7 @@ ); MARKETING_VERSION = 5.0.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.refilc2.naplo.livecardpro; + PRODUCT_BUNDLE_IDENTIFIER = com.refilcrel.naplo.livecardpro; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -610,14 +632,14 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 250; - DEVELOPMENT_TEAM = 4DKAF249F3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; GCC_C_LANGUAGE_STANDARD = gnu11; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = livecard/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = livecard; INFOPLIST_KEY_NSHumanReadableCopyright = ""; INFOPLIST_KEY_NSSupportsLiveActivities = YES; - IPHONEOS_DEPLOYMENT_TARGET = 16.1; + IPHONEOS_DEPLOYMENT_TARGET = 16.2; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -625,7 +647,7 @@ ); MARKETING_VERSION = 5.0.0; MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = com.refilc2.naplo.livecardpro; + PRODUCT_BUNDLE_IDENTIFIER = com.refilcrel.naplo.livecardpro; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SKIP_INSTALL = YES; @@ -754,7 +776,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = 250; - DEVELOPMENT_TEAM = 4DKAF249F3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = reFilc; @@ -764,7 +786,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 5.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.refilc2.naplo; + PRODUCT_BUNDLE_IDENTIFIER = com.refilcrel.naplo; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -782,7 +804,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; CURRENT_PROJECT_VERSION = 250; - DEVELOPMENT_TEAM = 4DKAF249F3; + DEVELOPMENT_TEAM = UT7MSP4GWZ; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_KEY_CFBundleDisplayName = reFilc; @@ -792,7 +814,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 5.0.0; - PRODUCT_BUNDLE_IDENTIFIER = com.refilc2.naplo; + PRODUCT_BUNDLE_IDENTIFIER = com.refilcrel.naplo; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/refilc/ios/Runner/AppDelegate.swift b/refilc/ios/Runner/AppDelegate.swift index 2c4c46e..a3d1b29 100644 --- a/refilc/ios/Runner/AppDelegate.swift +++ b/refilc/ios/Runner/AppDelegate.swift @@ -1,25 +1,113 @@ import UIKit +import background_fetch +import ActivityKit import Flutter @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { + private var methodChannel: FlutterMethodChannel? + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { GeneratedPluginRegistrant.register(with: self) - - // here, Without this code the task will not work. - //SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback(registerPlugins) - if #available(iOS 10.0, *) { - UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate + guard let controller = window?.rootViewController as? FlutterViewController else { + fatalError("rootViewController is not type FlutterViewController") } - + methodChannel = FlutterMethodChannel(name: "hu.refilc/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) } -} -// here -func registerPlugins(registry: FlutterPluginRegistry) { - GeneratedPluginRegistrant.register(with: registry) + 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)) + } + } else if call.method == "endLiveActivity" { + endLiveActivity() + result(nil) + } + } + + 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() + } + } + + + 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 + } } diff --git a/refilc/ios/Runner/Info.plist b/refilc/ios/Runner/Info.plist index e9ca19a..e32ae93 100644 --- a/refilc/ios/Runner/Info.plist +++ b/refilc/ios/Runner/Info.plist @@ -1,137 +1,138 @@ + + BGTaskSchedulerPermittedIdentifiers + + com.transistorsoft.refilcnotification + com.transistorsoft.refilcliveactivity + + CADisableMinimumFrameDurationOnPhone + + CFBundleAlternateIcons - BGTaskSchedulerPermittedIdentifiers - - com.transistorsoft.fetch - - CADisableMinimumFrameDurationOnPhone - - CFBundleAlternateIcons + refilc_concept - refilc_concept - - CFBundleIconFiles - - refilc_concept - - UIPrerenderedIcon - - - refilc_default - - CFBundleIconFiles - - refilc_default - - UIPrerenderedIcon - - - refilc_overcomplicated - - CFBundleIconFiles - - refilc_overcomplicated - - UIPrerenderedIcon - - - refilc_pride - - CFBundleIconFiles - - refilc_pride - - UIPrerenderedIcon - - + CFBundleIconFiles + + refilc_concept + + UIPrerenderedIcon + - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIcons + refilc_default - CFBundlePrimaryIcon - - CFBundleIconFiles - - - - CFBundleIconName - - UIPrerenderedIcon - - + CFBundleIconFiles + + refilc_default + + UIPrerenderedIcon + + + refilc_overcomplicated + + CFBundleIconFiles + + refilc_overcomplicated + + UIPrerenderedIcon + + + refilc_pride + + CFBundleIconFiles + + refilc_pride + + UIPrerenderedIcon + - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - reFilc - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleURLTypes - - - CFBundleTypeRole - Editor - CFBundleURLSchemes - - refilcapp - - - - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - ITSAppUsesNonExemptEncryption - - LSApplicationQueriesSchemes - - https - http - - LSRequiresIPhoneOS - - NSCameraUsageDescription - The app requires the camera access to set a custom profile picture. - NSPhotoLibraryUsageDescription - The app requires the photo library to set a custom profile picture. - NSSupportsLiveActivities - - UIApplicationSupportsIndirectInputEvents - - UIBackgroundModes - - fetch - processing - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UIStatusBarHidden - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UIViewControllerBasedStatusBarAppearance - + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIcons + + CFBundlePrimaryIcon + + CFBundleIconFiles + + + + CFBundleIconName + + UIPrerenderedIcon + + + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + reFilc + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleSignature + ???? + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + refilcapp + + + + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + ITSAppUsesNonExemptEncryption + + LSApplicationQueriesSchemes + + https + http + + LSRequiresIPhoneOS + + NSCameraUsageDescription + The app requires the camera access to set a custom profile picture. + NSPhotoLibraryUsageDescription + The app requires the photo library to set a custom profile picture. + NSSupportsLiveActivities + + UIApplicationSupportsIndirectInputEvents + + UIBackgroundModes + + fetch + processing + + UILaunchStoryboardName + LaunchScreen + UIMainStoryboardFile + Main + UIStatusBarHidden + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UIViewControllerBasedStatusBarAppearance + + diff --git a/refilc/ios/Runner/public_vars.swift b/refilc/ios/Runner/public_vars.swift new file mode 100644 index 0000000..f084d22 --- /dev/null +++ b/refilc/ios/Runner/public_vars.swift @@ -0,0 +1,12 @@ +// +// public_vars.swift +// Runner +// +// Created by Geryy on 02/05/2024. +// + +import Foundation + +var lessonDataDictionary: [String: Any] = [:] +var globalLessonData = LessonData(from: lessonDataDictionary) +var activityID: String? = "" diff --git a/refilc/ios/livecard/LiveActivityManager.swift b/refilc/ios/livecard/LiveActivityManager.swift new file mode 100644 index 0000000..25f9eab --- /dev/null +++ b/refilc/ios/livecard/LiveActivityManager.swift @@ -0,0 +1,92 @@ +import ActivityKit +import WidgetKit +import Foundation + +public struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable { + public typealias LiveDeliveryData = ContentState + public struct ContentState: Codable, Hashable { + var color: String + var icon: String + var index: String + var title: String + var subtitle: String + var description: String + var startDate: Date + var endDate: Date + var date: ClosedRange + var nextSubject: String + var nextRoom: String + } + + public var id = UUID() +} + +@available(iOS 16.2, *) +final class LiveActivityManager { + static let shared = LiveActivityManager() + var currentActivity: Activity? + + class func create() { + + 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.request( + attributes: LiveActivitiesAppAttributes(), + content: activityContent, + pushType: nil + ) + + activityID = activity.id + print("Live Activity létrehozva. Azonosító: \(activity.id)") + } catch { + print("Hiba történt a Live Activity létrehozásakor: \(error)") + } + } + } + + class func update() { + Task { + for activity in Activity.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 != "") { + Task { + for activity in Activity.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 + print("Live Activity sikeresen leállítva") + } + } + } + + class func isRunning(_ activityID: String) -> Bool { + for activity in Activity.activities { + if activity.id == activityID { + return true + } + } + return false + } +} diff --git a/refilc/ios/livecard/lesson_model.swift b/refilc/ios/livecard/lesson_model.swift index f098b63..f0d9904 100644 --- a/refilc/ios/livecard/lesson_model.swift +++ b/refilc/ios/livecard/lesson_model.swift @@ -1,31 +1,40 @@ import Foundation +import ActivityKit -class LessonData { - var color: String - var icon: String - var index: String - var title: String - var subtitle: String - var description: String - var startDate: Date - var endDate: Date - var date: ClosedRange - var nextSubject: String - var nextRoom: String - - init?() { - let sharedDefault = UserDefaults(suiteName: "group.refilc2.livecard")! - - self.color = sharedDefault.string(forKey: "color")! - self.icon = sharedDefault.string(forKey: "icon")! - self.index = sharedDefault.string(forKey: "index")! - self.title = sharedDefault.string(forKey: "title")! - self.subtitle = sharedDefault.string(forKey: "subtitle")! - self.description = sharedDefault.string(forKey: "description")! - self.startDate = Date(timeIntervalSince1970: Double(sharedDefault.string(forKey: "startDate")!)! / 1000) - self.endDate = Date(timeIntervalSince1970: Double(sharedDefault.string(forKey: "endDate")!)! / 1000) - date = self.startDate...self.endDate - self.nextSubject = sharedDefault.string(forKey: "nextSubject")! - self.nextRoom = sharedDefault.string(forKey: "nextRoom")! - } +public struct LessonData { + var color: String + var icon: String + var index: String + var title: String + var subtitle: String + var description: String + var startDate: Date + var endDate: Date + var date: ClosedRange + var nextSubject: String + var nextRoom: String + + init(from dictionary: [String: Any]) { + self.color = dictionary["color"] as? String ?? "" + self.icon = dictionary["icon"] as? String ?? "" + self.index = dictionary["index"] as? String ?? "" + self.title = dictionary["title"] as? String ?? "" + self.subtitle = dictionary["subtitle"] as? String ?? "" + self.description = dictionary["description"] as? String ?? "" + self.nextSubject = dictionary["nextSubject"] as? String ?? "" + self.nextRoom = dictionary["nextRoom"] as? String ?? "" + + if let startDateStr = dictionary["startDate"] as? String, let startDateInt = Int(startDateStr) { + self.startDate = Date(timeIntervalSince1970: TimeInterval(startDateInt) / 1000) + } else { + self.startDate = Date() + } + + if let endDateStr = dictionary["endDate"] as? String, let endDateInt = Int(endDateStr) { + self.endDate = Date(timeIntervalSince1970: TimeInterval(endDateInt) / 1000) + } else { + self.endDate = Date() + } + date = self.startDate...self.endDate + } } diff --git a/refilc/ios/livecard/livecard.swift b/refilc/ios/livecard/livecard.swift index 3781b3e..f5f27e7 100644 --- a/refilc/ios/livecard/livecard.swift +++ b/refilc/ios/livecard/livecard.swift @@ -5,8 +5,8 @@ import SwiftUI @main struct Widgets: WidgetBundle { var body: some Widget { - if #available(iOS 16.1, *) { - LiveCardWidget() + if #available(iOS 16.2, *) { + LiveCardWidget() } } } @@ -37,169 +37,161 @@ extension Color { } } - -// We need to redefined live activities pipe -struct LiveActivitiesAppAttributes: ActivityAttributes, Identifiable { - public struct ContentState: Codable, Hashable { } - - var id = UUID() -} - struct LockScreenLiveActivityView: View { - let context: ActivityViewContext + let context: ActivityViewContext - let lesson = LessonData() - - var body: some View { - HStack(alignment: .center) { - Image(systemName: lesson!.icon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: CGFloat(30), height: CGFloat(30)) - .padding(.leading, CGFloat(24)) - - VStack(alignment: .leading) { + var body: some View { HStack(alignment: .center) { - Text(lesson!.index + lesson!.title) - .font(.title3) - .bold() - - Text(lesson!.subtitle) - .font(.subheadline) - .padding(.trailing, 12) - } - - if (lesson!.description != "") { - Text(lesson!.description) - .font(.subheadline) - } - - HStack { - Image(systemName: "arrow.right") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: CGFloat(8), height: CGFloat(8)) - Text(lesson!.nextSubject) - .font(.caption) - Text(lesson!.nextRoom) - .font(.caption2) - } - }.padding(15) - - Spacer() - - Text(timerInterval: lesson!.date, countsDown: true) - .multilineTextAlignment(.center) - .frame(width: 85) - .font(.title2) - .monospacedDigit() - .padding(.trailing, CGFloat(24)) - } - .activityBackgroundTint( - lesson!.color != "#676767" - ? Color(hex: lesson!.color) - // Ha nem megy hat nem megy - : Color.clear - ) - } -} - -@available(iOSApplicationExtension 16.1, *) -struct LiveCardWidget: Widget { - var body: some WidgetConfiguration { - /// Live Activity Notification - ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in - LockScreenLiveActivityView(context: context) - /// Dynamic Island - } dynamicIsland: { context in - let lesson = LessonData() - - /// Expanded - return DynamicIsland { - DynamicIslandExpandedRegion(.leading) { - VStack { - Spacer() - ProgressView( - timerInterval: lesson!.date, - countsDown: true, - label: { - Image(systemName: lesson!.icon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: CGFloat(32), height: CGFloat(32)) - }, - currentValueLabel: { - Image(systemName: lesson!.icon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: CGFloat(32), height: CGFloat(32)) - } - ).progressViewStyle(.circular) - } - } - DynamicIslandExpandedRegion(.center) { - VStack(alignment: .leading) { - Text(lesson!.index + lesson!.title) - .lineLimit(1) - .font(.title3) - .bold() - - Text(lesson!.description) - .lineLimit(2) - .font(.caption) - }.padding(EdgeInsets(top: 0.0, leading: 5.0, bottom: 0.0, trailing: 0.0)) - } - DynamicIslandExpandedRegion(.trailing) { - VStack { - Spacer() - Text(lesson!.subtitle) - .lineLimit(1) - .font(.subheadline) - Spacer() - } - } - - /// Compact - } compactLeading: { - Label { - Text(lesson!.title) - } icon: { - Image(systemName: lesson!.icon) - } - .font(.caption2) - } - compactTrailing: { - Text(timerInterval: lesson!.date, countsDown: true) - .multilineTextAlignment(.center) - .frame(width: 40) - .font(.caption2) - - /// Collapsed - } minimal: { - VStack(alignment: .center, content: { - ProgressView( - timerInterval: lesson!.date, - countsDown: true, - label: { - Image(systemName: lesson!.icon) + // Ikon + Image(systemName: context.state.icon) .resizable() .aspectRatio(contentMode: .fit) - .frame(width: CGFloat(12), height: CGFloat(12)) - }, - currentValueLabel: { - Image(systemName: lesson!.icon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: CGFloat(12), height: CGFloat(12)) + .frame(width: CGFloat(30), height: CGFloat(30)) + .padding(.leading, CGFloat(24)) + + VStack(alignment: .center) { + // Jelenlegi óra + VStack { + Text(context.state.index + " " + context.state.title) + .font(.body) + .bold() + .multilineTextAlignment(.center) + + Text("Terem: \(context.state.subtitle)") + .italic() + .font(.caption) + } + + // Leírás + if (context.state.description != "") { + Text(context.state.description) + .font(.subheadline) + } + + // Következő óra + HStack { + Image(systemName: "arrow.right") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: CGFloat(8), height: CGFloat(8)) + Text(context.state.nextSubject) + .font(.caption) + Text(context.state.nextRoom) + .font(.caption2) + } + .multilineTextAlignment(.center) } - ).progressViewStyle(.circular) - }) - } - .keylineTint( - lesson!.color != "#676767" - ? Color(hex: lesson!.color) - : Color.clear - ) + .padding(15) + + Spacer() + + // Visszaszámláló + Text(timerInterval: context.state.date, countsDown: true) + .multilineTextAlignment(.center) + .frame(width: 85) + .font(.title2) + .monospacedDigit() + .padding(.trailing, CGFloat(24)) + } + .activityBackgroundTint( + context.state.color != "#676767" + ? Color(hex: context.state.color) + : Color.clear + ) + } +} + +@available(iOSApplicationExtension 16.2, *) +struct LiveCardWidget: Widget { + var body: some WidgetConfiguration { + /// Live Activity Notification + ActivityConfiguration(for: LiveActivitiesAppAttributes.self) { context in + LockScreenLiveActivityView(context: context) + /// Dynamic Island + } dynamicIsland: { context in + + /// Expanded + return DynamicIsland { + DynamicIslandExpandedRegion(.leading) { + VStack { + Spacer() + ProgressView( + timerInterval: context.state.date, + countsDown: true, + label: { + Image(systemName: context.state.icon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: CGFloat(32), height: CGFloat(32)) + }, + currentValueLabel: { + Image(systemName: context.state.icon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: CGFloat(32), height: CGFloat(32)) + } + ).progressViewStyle(.circular) + } + } + DynamicIslandExpandedRegion(.center) { + VStack(alignment: .center) { + Text(context.state.index + context.state.title) + .lineLimit(1) + .font(.body) + .bold() + + Text(context.state.subtitle) + .lineLimit(1) + .font(.subheadline) + Spacer() + + Text(context.state.description) + .lineLimit(2) + .font(.caption) + }.padding(EdgeInsets(top: 0.0, leading: 5.0, bottom: 0.0, trailing: 0.0)) + } + + /// Compact + } compactLeading: { + Label { + Text(context.state.title) + } icon: { + Image(systemName: context.state.icon) + } + .font(.caption2) + } + compactTrailing: { + Text(timerInterval: context.state.date, countsDown: true) + .multilineTextAlignment(.center) + .frame(width: 40) + .font(.caption2) + + /// Collapsed + } minimal: { + VStack(alignment: .center, content: { + ProgressView( + timerInterval: context.state.date, + countsDown: true, + label: { + Image(systemName: context.state.icon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: CGFloat(12), height: CGFloat(12)) + }, + currentValueLabel: { + Image(systemName: context.state.icon) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: CGFloat(12), height: CGFloat(12)) + } + ).progressViewStyle(.circular) + }) + } + .keylineTint( + context.state.color != "#676767" + ? Color(hex: context.state.color) + : Color.clear + ) + } } - } } diff --git a/refilc/lib/api/providers/live_card_provider.dart b/refilc/lib/api/providers/live_card_provider.dart index 05197fb..7ace094 100644 --- a/refilc/lib/api/providers/live_card_provider.dart +++ b/refilc/lib/api/providers/live_card_provider.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:refilc/api/providers/liveactivity/platform_channel.dart'; import 'package:refilc/helpers/subject.dart'; import 'package:refilc/models/settings.dart'; import 'package:refilc_kreta_api/models/lesson.dart'; @@ -10,7 +11,6 @@ import 'package:refilc_kreta_api/models/week.dart'; import 'package:refilc/utils/format.dart'; import 'package:refilc_kreta_api/providers/timetable_provider.dart'; import 'package:flutter/foundation.dart'; -import 'package:live_activities/live_activities.dart'; import 'package:refilc_mobile_ui/pages/home/live_card/live_card.i18n.dart'; enum LiveCardState { @@ -29,6 +29,15 @@ class LiveCardProvider extends ChangeNotifier { Lesson? prevLesson; List? nextLessons; + // new variables + static bool hasActivityStarted = false; + static bool hasDayEnd = false; + static DateTime? storeFirstRunDate; + static bool hasActivitySettingsChanged = false; + static Map LAData = {}; + static DateTime? now; + // + LiveCardState currentState = LiveCardState.empty; late Timer _timer; late final TimetableProvider _timetable; @@ -36,9 +45,6 @@ class LiveCardProvider extends ChangeNotifier { late Duration _delay; - final _liveActivitiesPlugin = LiveActivities(); - String? _latestActivityId; - Map _lastActivity = {}; bool _hasCheckedTimetable = false; @@ -47,23 +53,6 @@ class LiveCardProvider extends ChangeNotifier { required SettingsProvider settings, }) : _timetable = timetable, _settings = settings { - if (Platform.isIOS) { - _liveActivitiesPlugin.areActivitiesEnabled().then((value) { - // Console log - if (kDebugMode) { - print("iOS LiveActivity enabled: $value"); - } - - if (value) { - _liveActivitiesPlugin.init(appGroupId: "group.refilc2.livecard"); - - _liveActivitiesPlugin.getAllActivitiesIds().then((value) { - _latestActivityId = value.isNotEmpty ? value.first : null; - }); - } - }); - } - _timer = Timer.periodic(const Duration(seconds: 1), (timer) => update()); _delay = settings.bellDelayEnabled ? Duration(seconds: settings.bellDelay) @@ -71,21 +60,6 @@ class LiveCardProvider extends ChangeNotifier { update(); } - @override - void dispose() { - _timer.cancel(); - if (Platform.isIOS) { - _liveActivitiesPlugin.areActivitiesEnabled().then((value) { - if (value) { - if (_latestActivityId != null) { - _liveActivitiesPlugin.endActivity(_latestActivityId!); - } - } - }); - } - super.dispose(); - } - // Debugging static DateTime _now() { // return DateTime(2023, 9, 27, 9, 30); @@ -110,31 +84,88 @@ class LiveCardProvider extends ChangeNotifier { Map toMap() { switch (currentState) { + case LiveCardState.morning: + return { + "color": + '#${_settings.liveActivityColor.toString().substring(10, 16)}', + "icon": nextLesson != null + ? SubjectIcon.resolveName(subject: nextLesson?.subject) + : "book", + "title": "Első órádig:", + "subtitle": "", + "description": "", + "startDate": storeFirstRunDate != null ? ((storeFirstRunDate?.millisecondsSinceEpoch ?? 0) - (_delay.inMilliseconds)).toString(): "", + "endDate": ((nextLesson?.start.millisecondsSinceEpoch ?? 0) - + _delay.inMilliseconds) + .toString(), + "nextSubject": nextLesson != null + ? nextLesson?.subject.renamedTo ?? ShortSubject.resolve(subject: nextLesson?.subject).capital() + : "", + "nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "", + }; + + case LiveCardState.afternoon: + return { + "color": + '#${_settings.liveActivityColor.toString().substring(10, 16)}', + "icon": nextLesson != null + ? SubjectIcon.resolveName(subject: nextLesson?.subject) + : "book", + "title": "Első órádig:", + "subtitle": "", + "description": "", + "startDate": storeFirstRunDate != null ? ((storeFirstRunDate?.millisecondsSinceEpoch ?? 0) - (_delay.inMilliseconds)).toString(): "", + "endDate": ((nextLesson?.start.millisecondsSinceEpoch ?? 0) - + _delay.inMilliseconds) + .toString(), + "nextSubject": nextLesson != null + ? nextLesson?.subject.renamedTo ?? ShortSubject.resolve(subject: nextLesson?.subject).capital() + : "", + "nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "", + }; + + case LiveCardState.night: + return { + "color": + '#${_settings.liveActivityColor.toString().substring(10, 16)}', + "icon": nextLesson != null + ? SubjectIcon.resolveName(subject: nextLesson?.subject) + : "book", + "title": "Első órádig:", + "subtitle": "", + "description": "", + "startDate": storeFirstRunDate != null ? ((storeFirstRunDate?.millisecondsSinceEpoch ?? 0) - (_delay.inMilliseconds)).toString(): "", + "endDate": ((nextLesson?.start.millisecondsSinceEpoch ?? 0) - + _delay.inMilliseconds) + .toString(), + "nextSubject": nextLesson != null + ? nextLesson?.subject.renamedTo ?? ShortSubject.resolve(subject: nextLesson?.subject).capital() + : "", + "nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "", + }; + case LiveCardState.duringLesson: return { "color": - '#${_settings.liveActivityColor.toString().substring(10, 16)}', + '#${_settings.liveActivityColor.toString().substring(10, 16)}', "icon": currentLesson != null ? SubjectIcon.resolveName(subject: currentLesson?.subject) : "book", "index": - currentLesson != null ? '${currentLesson!.lessonIndex}. ' : "", + currentLesson != null ? '${currentLesson!.lessonIndex}. ' : "", "title": currentLesson != null - ? currentLesson?.subject.renamedTo ?? - ShortSubject.resolve(subject: currentLesson?.subject) - .capital() + ? currentLesson?.subject.renamedTo ?? ShortSubject.resolve(subject: currentLesson?.subject).capital() : "", "subtitle": currentLesson?.room.replaceAll("_", " ") ?? "", "description": currentLesson?.description ?? "", "startDate": ((currentLesson?.start.millisecondsSinceEpoch ?? 0) - - _delay.inMilliseconds) + _delay.inMilliseconds) .toString(), "endDate": ((currentLesson?.end.millisecondsSinceEpoch ?? 0) - - _delay.inMilliseconds) + _delay.inMilliseconds) .toString(), "nextSubject": nextLesson != null - ? nextLesson?.subject.renamedTo ?? - ShortSubject.resolve(subject: nextLesson?.subject).capital() + ? nextLesson?.subject.renamedTo ?? ShortSubject.resolve(subject: nextLesson?.subject).capital() : "", "nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "", }; @@ -150,23 +181,21 @@ class LiveCardProvider extends ChangeNotifier { return { "color": - '#${_settings.liveActivityColor.toString().substring(10, 16)}', + '#${_settings.liveActivityColor.toString().substring(10, 16)}', "icon": iconFloorMap[diff] ?? "cup.and.saucer", "title": "Szünet", "description": "go $diff".i18n.fill([ diff != "to room" ? (nextLesson!.getFloor() ?? 0) : nextLesson!.room ]), "startDate": ((prevLesson?.end.millisecondsSinceEpoch ?? 0) - - _delay.inMilliseconds) + _delay.inMilliseconds) .toString(), "endDate": ((nextLesson?.start.millisecondsSinceEpoch ?? 0) - - _delay.inMilliseconds) + _delay.inMilliseconds) .toString(), "nextSubject": (nextLesson != null - ? nextLesson?.subject.renamedTo ?? - ShortSubject.resolve(subject: nextLesson?.subject) - .capital() - : "") + ? nextLesson?.subject.renamedTo ?? ShortSubject.resolve(subject: nextLesson?.subject).capital() + : "") .capital(), "nextRoom": nextLesson?.room.replaceAll("_", " ") ?? "", "index": "", @@ -178,37 +207,6 @@ class LiveCardProvider extends ChangeNotifier { } void update() async { - if (Platform.isIOS) { - _liveActivitiesPlugin.areActivitiesEnabled().then((value) { - if (value) { - final cmap = toMap(); - if (!mapEquals(cmap, _lastActivity)) { - _lastActivity = cmap; - try { - if (_lastActivity.isNotEmpty) { - if (_latestActivityId == null) { - _liveActivitiesPlugin - .createActivity(_lastActivity) - .then((value) => _latestActivityId = value); - } else { - _liveActivitiesPlugin.updateActivity( - _latestActivityId!, _lastActivity); - } - } else { - if (_latestActivityId != null) { - _liveActivitiesPlugin.endActivity(_latestActivityId!); - } - } - } catch (e) { - if (kDebugMode) { - print('ERROR: Unable to create or update iOS LiveActivity!'); - } - } - } - } - }); - } - List today = _today(_timetable); if (today.isEmpty && !_hasCheckedTimetable) { @@ -221,15 +219,22 @@ class LiveCardProvider extends ChangeNotifier { ? Duration(seconds: _settings.bellDelay) : Duration.zero; - final now = _now().add(_delay); + + DateTime now = _now().add(_delay); + + if ((currentState == LiveCardState.morning || + currentState == LiveCardState.afternoon || + currentState == LiveCardState.night) && storeFirstRunDate == null) { + storeFirstRunDate = now; + } // Filter cancelled lessons #20 // Filter label lessons #128 today = today .where((lesson) => - lesson.status?.name != "Elmaradt" && - lesson.subject.id != '' && - !lesson.isEmpty) + lesson.status?.name != "Elmaradt" && + lesson.subject.id != '' && + !lesson.isEmpty) .toList(); if (today.isNotEmpty) { @@ -237,7 +242,7 @@ class LiveCardProvider extends ChangeNotifier { today.sort((a, b) => a.start.compareTo(b.start)); final _lesson = today.firstWhere( - (l) => l.start.isBefore(now) && l.end.isAfter(now), + (l) => l.start.isBefore(now) && l.end.isAfter(now), orElse: () => Lesson.fromJson({})); if (_lesson.start.year != 0) { @@ -283,11 +288,65 @@ class LiveCardProvider extends ChangeNotifier { currentState = LiveCardState.empty; } + //LIVE ACTIVITIES + + //CREATE + if (!hasActivityStarted && nextLesson != null && nextLesson! + .start + .difference(now) + .inMinutes <= 60 && (currentState == LiveCardState.morning || + currentState == LiveCardState.afternoon || + currentState == LiveCardState.night)) { + debugPrint( + "Az első óra előtt állunk, kevesebb mint egy órával. Létrehozás..."); + PlatformChannel.createLiveActivity(toMap()); + hasActivityStarted = true; + } + 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; + } + + //UPDATE + else if (hasActivityStarted) { + if (hasActivitySettingsChanged) { + debugPrint("Valamelyik beállítás megváltozott. Frissítés..."); + PlatformChannel.updateLiveActivity(toMap()); + hasActivitySettingsChanged = false; + } + else if (nextLesson != null || currentLesson != null) { + bool afterPrevLessonEnd = prevLesson != null && + now.subtract(const Duration(seconds: 1)).isBefore( + prevLesson!.end) && now.isAfter(prevLesson!.end); + + bool afterCurrentLessonStart = currentLesson != null && + now.subtract(const Duration(seconds: 1)).isBefore( + currentLesson!.start) && now.isAfter(currentLesson!.start); + if (afterPrevLessonEnd || afterCurrentLessonStart) { + debugPrint( + "Óra kezdete/vége után 1 másodperccel vagyunk. Frissítés..."); + PlatformChannel.updateLiveActivity(toMap()); + } + } + } + + //END + if (hasActivityStarted && !hasDayEnd && nextLesson == null && + now.isAfter(prevLesson!.end)) { + debugPrint("Az utolsó óra véget ért. Befejezés..."); + PlatformChannel.endLiveActivity(); + hasDayEnd = true; + hasActivityStarted = false; + } + LAData = toMap(); notifyListeners(); } bool get show => currentState != LiveCardState.empty; - Duration get delay => _delay; bool _sameDate(DateTime a, DateTime b) => @@ -296,4 +355,4 @@ class LiveCardProvider extends ChangeNotifier { List _today(TimetableProvider p) => (p.getWeek(Week.current()) ?? []) .where((l) => _sameDate(l.date, _now())) .toList(); -} +} \ No newline at end of file diff --git a/refilc/lib/api/providers/liveactivity/platform_channel.dart b/refilc/lib/api/providers/liveactivity/platform_channel.dart new file mode 100644 index 0000000..8b782da --- /dev/null +++ b/refilc/lib/api/providers/liveactivity/platform_channel.dart @@ -0,0 +1,43 @@ +import 'dart:async'; +import 'dart:io'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/services.dart'; + +class PlatformChannel { + static const MethodChannel _channel = MethodChannel('hu.refilc/liveactivity'); + + static Future createLiveActivity( + Map activityData) async { + if (Platform.isIOS) { + try { + debugPrint("creating..."); + await _channel.invokeMethod('createLiveActivity', activityData); + } on PlatformException catch (e) { + debugPrint("Hiba történt a Live Activity létrehozásakor: ${e.message}"); + } + } + } + + static Future updateLiveActivity( + Map activityData) async { + if (Platform.isIOS) { + try { + debugPrint("updating..."); + await _channel.invokeMethod('updateLiveActivity', activityData); + } on PlatformException catch (e) { + debugPrint("Hiba történt a Live Activity frissítésekor: ${e.message}"); + } + } + } + + static Future endLiveActivity() async { + if (Platform.isIOS) { + try { + debugPrint("finishing..."); + await _channel.invokeMethod('endLiveActivity'); + } on PlatformException catch (e) { + debugPrint("Hiba történt a Live Activity befejezésekor: ${e.message}"); + } + } + } +} \ No newline at end of file diff --git a/refilc/lib/api/providers/sync.dart b/refilc/lib/api/providers/sync.dart index 52a69ae..d6be0b1 100644 --- a/refilc/lib/api/providers/sync.dart +++ b/refilc/lib/api/providers/sync.dart @@ -22,6 +22,9 @@ import 'package:flutter/widgets.dart'; import 'package:provider/provider.dart'; import 'package:home_widget/home_widget.dart'; +import 'live_card_provider.dart'; +import 'liveactivity/platform_channel.dart'; + // Mutex bool lock = false; @@ -86,10 +89,17 @@ Future syncAll(BuildContext context) { return false; } + + return Future.wait(tasks).then((value) { // Unlock lock = false; + if(Platform.isIOS && LiveCardProvider.hasActivityStarted == true){ + PlatformChannel.endLiveActivity(); + LiveCardProvider.hasActivityStarted = false; + } + // Update Widget if (Platform.isAndroid) updateWidget(); }); diff --git a/refilc/lib/helpers/live_activity_helper.dart b/refilc/lib/helpers/live_activity_helper.dart new file mode 100644 index 0000000..69a26bc --- /dev/null +++ b/refilc/lib/helpers/live_activity_helper.dart @@ -0,0 +1,15 @@ +import 'package:refilc/api/providers/live_card_provider.dart'; +import '../api/providers/liveactivity/platform_channel.dart'; + + +class LiveActivityHelper { + @pragma('vm:entry-point') + void backgroundJob() async { + // initialize provider + if (!LiveCardProvider.hasDayEnd) { + await PlatformChannel.updateLiveActivity(LiveCardProvider.LAData); + } else { + await PlatformChannel.endLiveActivity(); + } + } +} \ No newline at end of file diff --git a/refilc/lib/main.dart b/refilc/lib/main.dart index 818a38b..7b58d0f 100644 --- a/refilc/lib/main.dart +++ b/refilc/lib/main.dart @@ -16,6 +16,8 @@ import 'package:refilc_mobile_ui/screens/error_screen.dart'; import 'package:refilc_mobile_ui/screens/error_report_screen.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'helpers/live_activity_helper.dart'; + // days without touching grass: 5,843 (16 yrs) void main() async { @@ -84,6 +86,7 @@ class Startup { // Notifications setup if (!kIsWeb) { initPlatformState(); + initAdditionalBackgroundFetch(); flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); } @@ -196,7 +199,12 @@ Future initPlatformState() async { if (kDebugMode) { print("[BackgroundFetch] Event received $taskId"); } - NotificationsHelper().backgroundJob(); + if (taskId == "com.transistorsoft.refilcliveactivity") { + if (!Platform.isIOS) return; + LiveActivityHelper().backgroundJob(); + } else { + NotificationsHelper().backgroundJob(); + } BackgroundFetch.finish(taskId); }, (String taskId) async { // <-- Task timeout handler. @@ -231,6 +239,50 @@ void backgroundHeadlessTask(HeadlessTask task) { if (kDebugMode) { print('[BackgroundFetch] Headless event received.'); } - NotificationsHelper().backgroundJob(); - BackgroundFetch.finish(task.taskId); + if (taskId == "com.transistorsoft.refilcliveactivity") { + if (!Platform.isIOS) return; + LiveActivityHelper().backgroundJob(); + } else { + NotificationsHelper().backgroundJob(); + } BackgroundFetch.finish(task.taskId); +} + +Future initAdditionalBackgroundFetch() async { + int status = await BackgroundFetch.configure( + BackgroundFetchConfig( + minimumFetchInterval: 1, // 1 minute + stopOnTerminate: false, + enableHeadless: true, + requiresBatteryNotLow: false, + requiresCharging: false, + requiresStorageNotLow: false, + requiresDeviceIdle: false, + requiredNetworkType: NetworkType.ANY, + startOnBoot: true), (String taskId) async { + // <-- Event handler + + if (kDebugMode) { + print("[BackgroundFetch] Event received $taskId"); + } + LiveActivityHelper liveActivityHelper = LiveActivityHelper(); + liveActivityHelper.backgroundJob(); + + BackgroundFetch.finish(taskId); + }, (String taskId) async { + // <-- Task timeout handler. + if (kDebugMode) { + print("[BackgroundFetch] TASK TIMEOUT taskId: $taskId"); + } + BackgroundFetch.finish(taskId); + }); + if (kDebugMode) { + print('[BackgroundFetch] configure success: $status'); + } + BackgroundFetch.scheduleTask(TaskConfig( + taskId: "com.transistorsoft.refilcliveactivity", + delay: 300000, // 5 minute + periodic: true, + forceAlarmManager: true, + stopOnTerminate: false, + enableHeadless: true)); } diff --git a/refilc/lib/models/settings.dart b/refilc/lib/models/settings.dart index 04a8ade..bb1433a 100644 --- a/refilc/lib/models/settings.dart +++ b/refilc/lib/models/settings.dart @@ -1,5 +1,6 @@ import 'dart:convert'; import 'dart:developer'; +import 'dart:io'; import 'package:flutter/services.dart'; import 'package:refilc/api/providers/database_provider.dart'; @@ -10,6 +11,8 @@ import 'package:refilc/theme/colors/dark_mobile.dart'; import 'package:flutter/material.dart'; import 'package:uuid/uuid.dart'; +import '../api/providers/live_card_provider.dart'; + enum Pages { home, grades, timetable, notes, absences } enum UpdateChannel { stable, beta, dev } @@ -666,6 +669,9 @@ class SettingsProvider extends ChangeNotifier { if (bellDelay != null && bellDelay != _bellDelay) _bellDelay = bellDelay; if (bellDelayEnabled != null && bellDelayEnabled != _bellDelayEnabled) { _bellDelayEnabled = bellDelayEnabled; + if(Platform.isIOS){ + LiveCardProvider.hasActivitySettingsChanged = true; + } } if (gradeOpeningFun != null && gradeOpeningFun != _gradeOpeningFun) { _gradeOpeningFun = gradeOpeningFun; diff --git a/refilc_mobile_ui/lib/screens/settings/settings_helper.dart b/refilc_mobile_ui/lib/screens/settings/settings_helper.dart index 83c0e6a..e566eea 100644 --- a/refilc_mobile_ui/lib/screens/settings/settings_helper.dart +++ b/refilc_mobile_ui/lib/screens/settings/settings_helper.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter_svg/svg.dart'; import 'package:refilc/api/providers/database_provider.dart'; +import 'package:refilc/api/providers/live_card_provider.dart'; import 'package:refilc/api/providers/user_provider.dart'; import 'package:refilc/helpers/quick_actions.dart'; import 'package:refilc/models/settings.dart'; @@ -750,6 +751,9 @@ class _BellDelaySettingState extends State Provider.of(context, listen: false) .update(bellDelay: currentDelay.inSeconds); _tabController.index = currentDelay.inSeconds > 0 ? 1 : 0; + if(Platform.isIOS){ + LiveCardProvider.hasActivitySettingsChanged = true; + } setState(() {}); } }, @@ -760,6 +764,9 @@ class _BellDelaySettingState extends State //Provider.of(context, listen: false).update(context, rounding: (r * 10).toInt()); Provider.of(context, listen: false) .update(bellDelay: currentDelay.inSeconds); + if(Platform.isIOS){ + LiveCardProvider.hasActivitySettingsChanged = true; + } Navigator.of(context).maybePop(); }, ), @@ -897,6 +904,7 @@ class _LiveActivityColorSettingState extends State { currentColor = k as Color; settings.update( liveActivityColor: currentColor.withAlpha(255)); + LiveCardProvider.hasActivitySettingsChanged = true; Navigator.of(context).maybePop(); }); }, @@ -913,6 +921,7 @@ class _LiveActivityColorSettingState extends State { var defaultColors = SettingsProvider.defaultSettings().liveActivityColor; settings.update(liveActivityColor: defaultColors); + LiveCardProvider.hasActivitySettingsChanged = true; Navigator.of(context).maybePop(); }, child: Text(SettingsLocalization("reset").i18n),