diff --git a/Balance.xcodeproj/project.pbxproj b/Balance.xcodeproj/project.pbxproj index 820e368..057151b 100644 --- a/Balance.xcodeproj/project.pbxproj +++ b/Balance.xcodeproj/project.pbxproj @@ -1316,19 +1316,19 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CODE_SIGN_ENTITLEMENTS = BalanceWatchApp/BalanceWatchApp.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = 637867499T; + DEVELOPMENT_TEAM = 58MHXEFYLU; ENABLE_PREVIEWS = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = BalanceWatchApp; - INFOPLIST_KEY_NSHealthShareUsageDescription = "$(PRODUCT_NAME) would like to capture a snap shot of your heart rate for the purpose of using the data in a clinical research study. Please grant this permission to continue participating."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "$(PRODUCT_NAME) would like to update your health usage with heart rate data. Actually we never do anything of the kind but it seems Apple has lost control of X-Code and has few if any answers, so we will ask this permission."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "BalanceWatchApp would like to capture a snap shot of your heart rate for the purpose of using the data in a clinical research study. Please grant this permission to continue participating."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "BalanceWatchApp would like to update your health usage with heart rate data. Actually we never do anything of the kind but it seems Apple has lost control of X-Code and has few if any answers, so we will ask this permission."; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - INFOPLIST_KEY_WKCompanionAppBundleIdentifier = edu.stanford.cs342.2023.balance; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.celteeka.cs342.2023.balance; INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1336,8 +1336,9 @@ ); LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.cs342.2023.balance.watchApp; + PRODUCT_BUNDLE_IDENTIFIER = com.celteeka.cs342.2023.balance.watchApp; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; SDKROOT = watchos; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; @@ -1367,10 +1368,10 @@ GCC_C_LANGUAGE_STANDARD = gnu17; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_KEY_CFBundleDisplayName = BalanceWatchApp; - INFOPLIST_KEY_NSHealthShareUsageDescription = "$(PRODUCT_NAME) would like to capture a snap shot of your heart rate for the purpose of using the data in a clinical research study. Please grant this permission to continue participating."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "$(PRODUCT_NAME) would like to update your health usage with heart rate data. Actually we never do anything of the kind but it seems Apple has lost control of X-Code and has few if any answers, so we will ask this permission."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "BalanceWatchApp would like to capture a snap shot of your heart rate for the purpose of using the data in a clinical research study. Please grant this permission to continue participating."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "BalanceWatchApp would like to update your health usage with heart rate data. Actually we never do anything of the kind but it seems Apple has lost control of X-Code and has few if any answers, so we will ask this permission."; INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown"; - INFOPLIST_KEY_WKCompanionAppBundleIdentifier = edu.stanford.cs342.2023.balance; + INFOPLIST_KEY_WKCompanionAppBundleIdentifier = com.celteeka.cs342.2023.balance; INFOPLIST_KEY_WKRunsIndependentlyOfCompanionApp = NO; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", @@ -1516,11 +1517,11 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Balance/BalanceDebug.entitlements; - CODE_SIGN_IDENTITY = "iPhone Developer"; + CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; DEVELOPMENT_ASSET_PATHS = ""; - DEVELOPMENT_TEAM = 637867499T; + DEVELOPMENT_TEAM = 58MHXEFYLU; ENABLE_PREVIEWS = YES; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -1535,9 +1536,10 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Balance/Supporting Files/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Balance; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSHealthShareUsageDescription = "The CS342 2023 Balance Team Application uses the step count to demonstrate CardinalKit's integration with HealthKit."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The CS342 2023 Balance Team Application uses the step count to demonstrate CardinalKit's integration with HealthKit."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "$(PRODUCT_NAME) would like to capture a snap shot of your heart rate for the purpose of using the data in a clinical research study. Please grant this permission to continue participating."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "$(PRODUCT_NAME) would like to update your health usage with heart rate data. Actually we never do anything of the kind but it seems Apple has lost control of X-Code and has few if any answers, so we will ask this permission."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; @@ -1558,7 +1560,7 @@ ); MARKETING_VERSION = 1.0; OTHER_LDFLAGS = "-ObjC"; - PRODUCT_BUNDLE_IDENTIFIER = edu.stanford.cs342.2023.balance; + PRODUCT_BUNDLE_IDENTIFIER = com.celteeka.cs342.2023.balance; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; @@ -1598,9 +1600,10 @@ GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Balance/Supporting Files/Info.plist"; INFOPLIST_KEY_CFBundleDisplayName = Balance; + INFOPLIST_KEY_ITSAppUsesNonExemptEncryption = NO; INFOPLIST_KEY_NSCameraUsageDescription = "This message should never appear. Please adjust this when you start using camera information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; - INFOPLIST_KEY_NSHealthShareUsageDescription = "The CS342 2023 Balance Team Application uses the step count to demonstrate CardinalKit's integration with HealthKit."; - INFOPLIST_KEY_NSHealthUpdateUsageDescription = "The CS342 2023 Balance Team Application uses the step count to demonstrate CardinalKit's integration with HealthKit."; + INFOPLIST_KEY_NSHealthShareUsageDescription = "$(PRODUCT_NAME) would like to capture a snap shot of your heart rate for the purpose of using the data in a clinical research study. Please grant this permission to continue participating."; + INFOPLIST_KEY_NSHealthUpdateUsageDescription = "$(PRODUCT_NAME) would like to update your health usage with heart rate data. Actually we never do anything of the kind but it seems Apple has lost control of X-Code and has few if any answers, so we will ask this permission."; INFOPLIST_KEY_NSLocationAlwaysAndWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSLocationWhenInUseUsageDescription = "This message should never appear. Please adjust this when you start using location information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; INFOPLIST_KEY_NSMicrophoneUsageDescription = "This message should never appear. Please adjust this when you start using microphone information. We have to put this in here as ResearchKit has the possibility to use it and not putting it here returns an error on AppStore Connect."; diff --git a/Balance/ActivityLogging/ActivityLogBaseView.swift b/Balance/ActivityLogging/ActivityLogBaseView.swift index 31f2fae..c6bee12 100644 --- a/Balance/ActivityLogging/ActivityLogBaseView.swift +++ b/Balance/ActivityLogging/ActivityLogBaseView.swift @@ -11,57 +11,63 @@ import SwiftUI struct ActivityLogContainer: View where Content: View { @EnvironmentObject var activityLogEntry: ActivityLogEntry private let content: Content - + var body: some View { content.environmentObject(activityLogEntry) } - + public init(@ViewBuilder content: () -> Content) { self.content = content() } } -// This base view implements functionality to log information that will be send to the ActivityStorageManager struct ActivityLogBaseView: View where Content: View { @EnvironmentObject var activityLogEntry: ActivityLogEntry @EnvironmentObject var logStore: ActivityLogStore + @State private var isVisible = false private let viewName: String private let isDirectChildToContainer: Bool private let content: Content - + var body: some View { content - .onReceive(NotificationCenter.default.publisher(for: Notification.Name.goBackground)) { _ in - activityLogEntry.reset() + .onAppear { +#if DEBUG + print("[ActivityLogBaseView][onAppear] - View: \(viewName)") +#endif + isVisible = true + activityLogEntry.push(viewName: viewName) } - .onAppear(perform: { - activityLogEntry.addAction(actionDescription: "Opened \(viewName)") + .onDisappear { #if DEBUG - print("Opened \(viewName)") + print("[ActivityLogBaseView][onDisappear] - View: \(viewName)") #endif - }) - .onDisappear(perform: { - activityLogEntry.endLog(actionDescription: "Closed \(viewName)") - + isVisible = false + // On forward nav, push() already finalized this view before onDisappear fires. + // Only finalize if pendingEntry still matches — i.e. this is a back navigation. + if activityLogEntry.pendingEntry?.description == viewName { + activityLogEntry.finalizePending() + } if isDirectChildToContainer { -#if DEMO logStore.saveLog(activityLogEntry) -#else - ActivityStorageManager.shared.uploadActivity(activityLogEntry: activityLogEntry) -#endif - // for debugging - let activityLogEntryString = activityLogEntry.toString() + } + } + .onReceive(NotificationCenter.default.publisher(for: UIApplication.didBecomeActiveNotification)) { _ in #if DEBUG - print("Sending activity log to storage manager: \(activityLogEntryString)") + print("[ActivityLogBaseView][didBecomeActive] - View: \(viewName), isVisible: \(isVisible)") #endif + guard isVisible else { + return } - + if activityLogEntry.pendingEntry?.description != viewName { #if DEBUG - print("Closed \(viewName)") + print("[ActivityLogBaseView][didBecomeActive] - Re-pushing view: \(viewName)") #endif - }) + activityLogEntry.push(viewName: viewName) + } + } } - + public init(viewName: String, isDirectChildToContainer: Bool = false, @ViewBuilder content: () -> Content) { self.viewName = viewName self.isDirectChildToContainer = isDirectChildToContainer diff --git a/Balance/ActivityLogging/ActivityLogButtonStyle.swift b/Balance/ActivityLogging/ActivityLogButtonStyle.swift index 5480e25..845c625 100644 --- a/Balance/ActivityLogging/ActivityLogButtonStyle.swift +++ b/Balance/ActivityLogging/ActivityLogButtonStyle.swift @@ -19,14 +19,14 @@ struct ActivityLogButtonStyle: PrimitiveButtonStyle { configuration.label .onTapGesture { configuration.trigger() - activityLogEntry.addActionButton(actionDescription: "Button " + activityDescription) + activityLogEntry.addButtonEvent(description: "Button " + activityDescription) #if DEMO logStore.saveLog(activityLogEntry) - ActivityLogStore.save(logs: logStore.logs) { result in - if case .failure(let error) = result { - print(error.localizedDescription) - } - } +// ActivityLogStore.save(logs: logStore.logs) { result in +// if case .failure(let error) = result { +// print(error.localizedDescription) +// } +// } #else ActivityStorageManager.shared.uploadActivity(activityLogEntry: activityLogEntry) #endif diff --git a/Balance/ActivityLogging/ActivityStorageManager.swift b/Balance/ActivityLogging/ActivityStorageManager.swift index f14f2e2..4c35e56 100644 --- a/Balance/ActivityLogging/ActivityStorageManager.swift +++ b/Balance/ActivityLogging/ActivityStorageManager.swift @@ -3,26 +3,16 @@ // Balance // // Created by Alexis Lowber on 3/5/23. -// +// import FirebaseAuth import FirebaseCore import FirebaseFirestore import Foundation - -// Activity Log Structure: -/* - start timestamp (view appears) - action (button press) - action (button press) - end timestamp (view disappears) - total duration: - */ - // swiftlint:disable all -struct LogAction { +struct LogAction: Codable { let sessionID: String let sessionStartTime: Date let sessionEndTime: Date @@ -41,135 +31,133 @@ struct Action: Codable { var duration: Int = 0 } +private let coinsEligibleViews: Set = [ + "Image Highlight", "Image Selected", "Video Highlight", "Video Selected", + "Sudoku Game", "Crossover Game", "Tetris Game", "Simon Says Game", + "Bouncing Ball Game", "2048 Game", "Solitaire Game", "Guess the Emotion", + "How is your mood", "New Draw", "Mandala Selected", "Coloring Saved", + "Draw Saved", "Playing Spotify", "Body sensations Part", + "Breathing Feature", "Guided meditation Feature" +] + class ActivityLogEntry: ObservableObject, Codable { enum CodingKeys: String, CodingKey { case id case startTime case endTime case duration - case actions + case entries = "actions" + case pendingEntry } var id = UUID().uuidString var startTime = Date() var endTime = Date.now var duration: Int = 0 - var actions: [Action] = [] - + var entries: [Action] = [] + var pendingEntry: Action? + + init() { + NotificationCenter.default.addObserver( + self, + selector: #selector(handleWillEnterForeground), + name: UIApplication.willEnterForegroundNotification, + object: nil + ) + } + + @objc private func handleWillEnterForeground() { + let spotifyWasActive = UserDefaults.standard.bool(forKey: StorageKeys.spotifyConnect) + if !spotifyWasActive { +#if DEBUG + print("[ActivityLogEntry][handleWillEnterForeground] - Resetting entry") +#endif + reset() + } + } + + // Push new view. Finalizes previous pending entry first. + func push(viewName: String) { +#if DEBUG + print("[ActivityLogEntry][push] - viewName: \(viewName), entries: \(entries.count + 1)") +#endif + let now = Date.now + finalizePending(at: now) + pendingEntry = Action(description: viewName, startTime: now) + if entries.isEmpty { + startTime = now + } + } + + // Finalize current pending entry (onDisappear or app background). + func finalizePending() { + finalizePending(at: Date.now) + } + + private func finalizePending(at endDate: Date) { + guard var pending = pendingEntry else { return } + let interval = endDate - pending.startTime + pending.endTime = endDate + pending.duration = interval.second ?? 0 + entries.append(pending) + pendingEntry = nil + + if coinsEligibleViews.contains(pending.description) && (pending.duration > coinsTime) { + NotificationCenter.default.post(name: Notification.Name.coinsUpdate, object: nil) + } + + if let first = entries.first { + startTime = first.startTime + } + endTime = endDate + duration = (endTime - startTime).second ?? 0 +#if DEBUG + print("[ActivityLogEntry][finalizePending] - Finalized: \(pending.description), duration: \(pending.duration)s") +#endif + } + + // Instantaneous button-press event (no duration). + func addButtonEvent(description: String) { + let now = Date.now + entries.append(Action(description: description, startTime: now, endTime: now, duration: 0)) +#if DEBUG + print("[ActivityLogEntry][addButtonEvent] - \(description)") +#endif + } + func reset() { id = UUID().uuidString startTime = Date() endTime = startTime duration = 0 - actions = [] + entries = [] + pendingEntry = nil } - + func isEmpty() -> Bool { - return getDuration() == 0 + return entries.isEmpty } - - func addAction(actionDescription: String) { - let currentDate = Date.now - actions.append(Action(description: actionDescription, startTime: currentDate)) - } - - func addActionButton(actionDescription: String) { - let currentDate = Date.now - actions.append( - Action( - description: actionDescription, - startTime: currentDate, - endTime: currentDate, - duration: 0 - ) - ) - // set start time if this is the first action - startTime = currentDate - endTime = currentDate - duration = 0 - } - - func endLog(actionDescription: String) { - let currentEndDate = Date.now - let startAction = actions.last(where: { - $0.description == actionDescription.replacingOccurrences(of: "Closed", with: "Opened") - }) - - if startAction == nil { - return - } - let interval = currentEndDate - startAction!.startTime - actions.append( - Action( - description: actionDescription.replacingOccurrences(of: "Closed ", with: ""), - startTime: startAction!.startTime, - endTime: currentEndDate, - duration: interval.second ?? 0 - ) - ) - actions = actions.filter( { - $0.id != startAction?.id - }) - - if actionDescription.contains("Closed Image Highlight") || - actionDescription.contains("Closed Image Selected") || - actionDescription.contains("Closed Video Highlight") || - actionDescription.contains("Closed Video Selected") || - actionDescription.contains("Closed Sudoku Game") || - actionDescription.contains("Closed Crossover Game") || - actionDescription.contains("Closed Tetris Game") || - actionDescription.contains("Closed Simon Says Game") || - actionDescription.contains("Closed Bouncing Ball Game") || - actionDescription.contains("Closed 2048 Game") || - actionDescription.contains("Closed Solitaire Game") || - actionDescription.contains("Closed Guess the Emotion") || - actionDescription.contains("Closed How is your mood") || - actionDescription.contains("Closed New Draw") || - actionDescription.contains("Closed Mandala Selected") || - actionDescription.contains("Closed Coloring Saved") || - actionDescription.contains("Closed Draw Saved") || - actionDescription.contains("Closed Playing Spotify") || - actionDescription.contains("Closed Body sensations Part") || - actionDescription.contains("Closed Breathing Feature") || - actionDescription.contains("Closed Guided meditation Feature") { - if (interval.second ?? 0) > coinsTime { - NotificationCenter.default.post(name: Notification.Name.coinsUpdate, object: nil) - } - } - - print(interval.second ?? 0) - startTime = actions.first!.startTime - endTime = actions.last!.endTime - let intervalSession = endTime - startTime - duration = intervalSession.second ?? 0 - } - func getDuration() -> TimeInterval { return endTime.timeIntervalSinceReferenceDate - startTime.timeIntervalSinceReferenceDate } - + func toString() -> (String, String) { let idStr = dateToString(date: startTime) - let startStr = "start: " + idStr let endStr = endTime != Date(timeIntervalSinceReferenceDate: 0) ? "end: " + dateToString(date: endTime) : "" let durationStr = "duration: \(duration)" - var actionsStr = "" - - for action in actions { - actionsStr.append("\(dateToString(date: action.startTime) + " " + action.description)\n") + for entry in entries { + actionsStr.append("\(dateToString(date: entry.startTime) + " " + entry.description)\n") } - return (idStr, [startStr, actionsStr, durationStr, endStr].joined(separator: "\n")) } - + func dateToString(date: Date) -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss" formatter.timeZone = .current - return formatter.string(from: date) } } @@ -177,15 +165,13 @@ class ActivityLogEntry: ObservableObject, Codable { class ActivityStorageManager { static let shared = ActivityStorageManager() - + func uploadActivity(activityLogEntry: ActivityLogEntry) { guard !activityLogEntry.isEmpty() else { - // TODO: switch to logging statement print("Cannot send activity data without both start and end time fields") return } - - // Authorize User: users should be signed in to use the app + guard let user = Auth.auth().currentUser else { #if DEBUG print("Error finding current user (FIRUser)") @@ -193,15 +179,14 @@ class ActivityStorageManager { return } let userID = user.uid - + let database = Firestore.firestore() - let startID = "\(activityLogEntry.dateToString(date: activityLogEntry.startTime))" - + do { try database.collection("users").document("\(userID)/activity/\(startID).txt").setData(from: activityLogEntry) } catch let error { - print("Error writing city to Firestore: \(error)") + print("Error writing activity to Firestore: \(error)") } } } diff --git a/Balance/Balance.swift b/Balance/Balance.swift index c1f78df..d5b6e1a 100644 --- a/Balance/Balance.swift +++ b/Balance/Balance.swift @@ -87,14 +87,24 @@ struct Balance: App { func inactiveApp() { } func backgroundApp() { + print("[Balance][backgroundApp] - App going to BG") let value = UserDefaults.standard.bool(forKey: StorageKeys.spotifyConnect) if value == false { - activityLogEntry.reset() + activityLogEntry.finalizePending() + if !activityLogEntry.isEmpty() { +#if DEMO + print("[Balance::backgroundApp] - DEMO Mode ON. Saving values") + logStore.saveLog(activityLogEntry) +#else + ActivityStorageManager.shared.uploadActivity(activityLogEntry: activityLogEntry) +#endif + } } self.heartAlert = false } func activeApp() { + print("[Balance][activeApp] - App activating") let patientId = UserDefaults.standard.string(forKey: "lastPatient") ?? "" if patientId.isEmpty { if heartAlert == false { @@ -107,14 +117,14 @@ struct Balance: App { } func appEvent(description: String) { - activityLogEntry.addActionButton(actionDescription: description) + activityLogEntry.addButtonEvent(description: description) #if DEMO logStore.saveLog(activityLogEntry) - ActivityLogStore.save(logs: logStore.logs) { result in - if case .failure(let error) = result { - print(error.localizedDescription) - } - } +// ActivityLogStore.save(logs: logStore.logs) { result in +// if case .failure(let error) = result { +// print(error.localizedDescription) +// } +// } #else ActivityStorageManager.shared.uploadActivity(activityLogEntry: activityLogEntry) #endif diff --git a/Balance/Distraction/Music/iOS_SDK/SpotifyConfig.swift b/Balance/Distraction/Music/iOS_SDK/SpotifyConfig.swift index 3683c24..44da56f 100644 --- a/Balance/Distraction/Music/iOS_SDK/SpotifyConfig.swift +++ b/Balance/Distraction/Music/iOS_SDK/SpotifyConfig.swift @@ -8,7 +8,7 @@ import Foundation -// swiftlint:disable convenience_type force_unwrapping +// swiftlint:disable convenience_type struct SpotifyConfig { static let accessTokenKey = "access-token-key" static let redirectUri = URL(string: "balanceapp://spotify")! diff --git a/Balance/Distraction/Music/iOS_SDK/SpotifyViewController.swift b/Balance/Distraction/Music/iOS_SDK/SpotifyViewController.swift index 052b663..4506a44 100644 --- a/Balance/Distraction/Music/iOS_SDK/SpotifyViewController.swift +++ b/Balance/Distraction/Music/iOS_SDK/SpotifyViewController.swift @@ -110,7 +110,7 @@ class SpotifyViewController: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) - activityLogEntry?.addAction(actionDescription: "Opened Playing Spotify") + activityLogEntry?.push(viewName: "Playing Spotify") updateViewBasedOnConnected() } @@ -121,7 +121,7 @@ class SpotifyViewController: UIViewController { appRemote.playerAPI?.pause(nil) } } - activityLogEntry?.endLog(actionDescription: "Closed Playing Spotify") + activityLogEntry?.finalizePending() } @objc diff --git a/Balance/Home/Home.swift b/Balance/Home/Home.swift index d2ca068..765f53d 100644 --- a/Balance/Home/Home.swift +++ b/Balance/Home/Home.swift @@ -20,10 +20,12 @@ struct HomeView: View { ZStack { backgroundColor.edgesIgnoringSafeArea(.all) NavigationStack { - VStack(spacing: 0) { - HeaderHome().environmentObject(counter) - menuOptions - Spacer() + ActivityLogBaseView(viewName: "Home", isDirectChildToContainer: true) { + VStack(spacing: 0) { + HeaderHome().environmentObject(counter) + menuOptions + Spacer() + } } .navigationTitle("") .navigationBarTitle("", displayMode: .inline) @@ -43,7 +45,6 @@ struct HomeView: View { } .onChange(of: counter.count, perform: { _ in if counter.count > UserDefaults.standard.double(forKey: bpmKEY) { -// print(counter.count) alertHeartRate() } }) diff --git a/Balance/Profile/ProfileView.swift b/Balance/Profile/ProfileView.swift index e6ef508..30b192f 100644 --- a/Balance/Profile/ProfileView.swift +++ b/Balance/Profile/ProfileView.swift @@ -182,7 +182,6 @@ struct ProfileView: View { } Button("Reset") { activityLogEntry.reset() - // NotificationCenter.default.post(name: Notification.Name.goBackground, object: nil) self.logs.removeAll() logsIsEmpty = true UserImageCache.remove(key: self.patientID.appending("UploadedArray")) @@ -198,24 +197,35 @@ struct ProfileView: View { var shareOption: some View { Button(action: { - EmailHelper.shared.send( - subject: "Balance Export", - body: "ParticipantID: " + self.patientID + "\n", - file: convertToCSV(), - fileName: self.patientID + ".csv", - plainText: convertToPlainText(), - to: ["acogburn@stanford.edu"] - ) + ActivityLogStore.load { result in + if case .success(let freshLogs) = result { + self.logs = freshLogs + EmailHelper.shared.send( + subject: "Balance Export", + body: "ParticipantID: " + self.patientID + "\n", + file: self.convertToCSV(), + fileName: self.patientID + ".csv", + plainText: self.convertToPlainText(), + to: ["acogburn@stanford.edu"] + ) + } + } }) { ProfileCellView(image: "mail", text: "E-Mail data") } } - + var shareLink: some View { - ShareLink( - item: convertToCSV(), - subject: Text("Balance Export: ParticipantID: " + self.patientID) - ) { + Button(action: { + ActivityLogStore.load { result in + if case .success(let freshLogs) = result { + self.logs = freshLogs + let url = self.convertToCSV() + let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil) + UIApplication.shared.currentUIWindow()?.rootViewController?.present(activityVC, animated: true) + } + } + }) { ProfileCellView(image: "square.and.arrow.up", text: "Share data") } } @@ -426,6 +436,7 @@ struct ProfileView: View { print(error.localizedDescription) case .success(let logs): self.logs = logs + print("[ProfileView][loadLogs]: Dump: \(self.logs.toJSON())") if self.logs.isEmpty { logsIsEmpty = true } else { @@ -435,66 +446,50 @@ struct ProfileView: View { } } - func convertToCSV() -> URL { + private func buildLogActions() -> [LogAction] { var logActions = [LogAction]() - var noteAsCSV = "sessionID, sessionStartTime, sessionEndTime, sessionDuration, description, startTime, endTime, duration\n" for log in logs { - for action in log.actions { + for entry in log.entries { logActions.append( LogAction( sessionID: log.id, sessionStartTime: log.startTime, sessionEndTime: log.endTime, sessionDuration: log.duration, - description: action.description, - startTime: action.startTime, - endTime: action.endTime, - duration: action.duration + description: entry.description, + startTime: entry.startTime, + endTime: entry.endTime, + duration: entry.duration ) ) } } - - for action in logActions.sorted(by: { $0.startTime.compare($1.startTime) == .orderedAscending }) { + return logActions.sorted { $0.startTime.compare($1.startTime) == .orderedAscending } + } + + func convertToCSV() -> URL { + let logActions = buildLogActions() + var noteAsCSV = "sessionID, sessionStartTime, sessionEndTime, sessionDuration, description, startTime, endTime, duration\n" + for action in logActions { noteAsCSV.append(contentsOf: "\"\(action.sessionID)\",\"\(DateFormatter.sharedDateFormatter.string(from: action.sessionStartTime))\",\"\(DateFormatter.sharedDateFormatter.string(from: action.sessionEndTime))\",\"\(action.sessionDuration)\",\"\(action.description)\",\"\(DateFormatter.sharedDateFormatter.string(from: action.startTime))\",\"\(DateFormatter.sharedDateFormatter.string(from: action.endTime))\",\"\(action.duration)\"\n") } - let fileManager = FileManager.default do { let path = try fileManager.url(for: .documentDirectory, in: .allDomainsMask, appropriateFor: nil, create: false) let fileURL = path.appendingPathComponent(self.patientID + ".csv") try noteAsCSV.write(to: fileURL, atomically: true, encoding: .utf8) - return fileURL } catch { print("error creating file") } return URL(fileURLWithPath: "") } - + func convertToPlainText() -> String { + let logActions = buildLogActions() var noteAsCSV = "ParticipantID: " + self.patientID + "\n" noteAsCSV.append(contentsOf: "sessionID, sessionStartTime, sessionEndTime, sessionDuration, description, startTime, endTime, duration\n") - - var logActions = [LogAction]() - for log in logs { - for action in log.actions { - logActions.append( - LogAction( - sessionID: log.id, - sessionStartTime: log.startTime, - sessionEndTime: log.endTime, - sessionDuration: log.duration, - description: action.description, - startTime: action.startTime, - endTime: action.endTime, - duration: action.duration - ) - ) - } - } - - for action in logActions.sorted(by: { $0.startTime.compare($1.startTime) == .orderedAscending }) { + for action in logActions { noteAsCSV.append(contentsOf: "\"\(action.sessionID)\",\"\(DateFormatter.sharedDateFormatter.string(from: action.sessionStartTime))\",\"\(DateFormatter.sharedDateFormatter.string(from: action.sessionEndTime))\",\"\(action.sessionDuration)\",\"\(action.description)\",\"\(DateFormatter.sharedDateFormatter.string(from: action.startTime))\",\"\(DateFormatter.sharedDateFormatter.string(from: action.endTime))\",\"\(action.duration)\"\n") } return noteAsCSV @@ -506,8 +501,8 @@ struct ProfileView: View { var counts: [String: Int] = [:] for log in logs { - for action in log.actions { - logActions.append(action.description) + for entry in log.entries { + logActions.append(entry.description) } } for item in logActions { diff --git a/Balance/Utils/BalanceExtensions.swift b/Balance/Utils/BalanceExtensions.swift index c2f3a82..6ac4d9c 100644 --- a/Balance/Utils/BalanceExtensions.swift +++ b/Balance/Utils/BalanceExtensions.swift @@ -166,3 +166,18 @@ extension Array { return Array(self[0.. String { + let encoder = JSONEncoder() + encoder.outputFormatting = [.prettyPrinted, .sortedKeys] + encoder.dateEncodingStrategy = .iso8601 + guard let data = try? encoder.encode(self), + let json = String(data: data, encoding: .utf8) else { + return "" + } + return json + } +} +#endif