From 17446f259ac6ca96e53594edb210d2b4a00f98e6 Mon Sep 17 00:00:00 2001 From: lukestahl Date: Sun, 14 Jan 2024 17:41:21 +0100 Subject: [PATCH] load app settings and add quickapp switch feature --- apps/expo/app/_layout.tsx | 3 +- apps/expo/components/container.tsx | 14 +- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + apps/expo/targets/intents/DigitalBreak.swift | 207 +++++++++++++++--- 4 files changed, 191 insertions(+), 41 deletions(-) create mode 100644 apps/expo/ios/digitalbreak.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/apps/expo/app/_layout.tsx b/apps/expo/app/_layout.tsx index 31aba6a..74bb17f 100644 --- a/apps/expo/app/_layout.tsx +++ b/apps/expo/app/_layout.tsx @@ -58,7 +58,8 @@ function RootLayoutNav() { console.log("App has come to the foreground!"); } else if (appState.current.match(/active/) && nextAppState === "background") { clearShortcutListener(); - void ShortCutPayload.clear(); + // void ShortCutPayload.clear(); + console.log("this would clear the shortcut listener"); console.log("App has come to the background!"); } appState.current = nextAppState; diff --git a/apps/expo/components/container.tsx b/apps/expo/components/container.tsx index 5b38622..6b97442 100644 --- a/apps/expo/components/container.tsx +++ b/apps/expo/components/container.tsx @@ -20,9 +20,11 @@ export const Container = ({ paddingTop={insets.top} paddingBottom={tabBarHeight ?? insets.bottom} flex={1} - minWidth={600} - maxWidth={600} - marginHorizontal="auto" + $gtSm={{ + minWidth: 600, + marginHorizontal: "auto", + maxWidth: 600, + }} {...(viewProps as React.ComponentProps)} > @@ -35,8 +37,10 @@ export const Container = ({ paddingTop={insets.top} paddingBottom={tabBarHeight ?? insets.bottom} flex={1} - marginHorizontal="auto" - maxWidth={600} + $gtSm={{ + marginHorizontal: "auto", + maxWidth: 600, + }} {...viewProps} > diff --git a/apps/expo/ios/digitalbreak.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/apps/expo/ios/digitalbreak.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/apps/expo/ios/digitalbreak.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/apps/expo/targets/intents/DigitalBreak.swift b/apps/expo/targets/intents/DigitalBreak.swift index 507dd62..b0dd88f 100644 --- a/apps/expo/targets/intents/DigitalBreak.swift +++ b/apps/expo/targets/intents/DigitalBreak.swift @@ -1,9 +1,32 @@ import AppIntents +import CommonCrypto +import Foundation import UIKit -enum MyError: Error { - case runtimeError(String) +struct AppInfo: Decodable { + struct ItemSettings: Decodable { + let breakDurationSeconds: Int + let quickAppSwitchDurationMinutes: Int + let dailyTimeSpentMinutes: Int + } + + let name: String + let active: Bool + let iconKey: String + let id: String + let settings: ItemSettings + let key: String } +struct Manifest: Decodable { + let apps: String +} + +struct AppIntentPayload: Codable { + var openedApp: String? +} + +typealias AppInfoList = [String: AppInfo] + struct DigitalBreak: AppIntent { static var title: LocalizedStringResource = "Check Digital Break" @@ -21,10 +44,8 @@ struct DigitalBreak: AppIntent { Summary("Digital Break when \(\.$appPrompt) opens") } - @MainActor - func perform() async throws -> some IntentResult & ReturnsValue { + func getStorageDirAndFile(fileName: String) -> (URL, URL) { let RCTStorageDirectory = "RCTAsyncLocalStorage_V1" - let RCTManifestFileName = "appintent.json" let fileManager = FileManager.default @@ -37,48 +58,164 @@ struct DigitalBreak: AppIntent { let storageDirectory = mySupportDirectory.appendingPathComponent(RCTStorageDirectory) - let storageFile = storageDirectory.appendingPathComponent(RCTManifestFileName) + let storageFile = storageDirectory.appendingPathComponent(fileName) + return (storageDirectory, storageFile) + } - var dict = [] as? [String: String] - do { + func MD5(string: String) -> String { + let digestLength = Int(CC_MD5_DIGEST_LENGTH) + let result = UnsafeMutablePointer.allocate(capacity: digestLength) - let stringFromFile = try String(contentsOf: storageFile, encoding: .utf8) + let strBytes = string.cString(using: .utf8) + let strLen = CC_LONG(string.lengthOfBytes(using: .utf8)) + + CC_MD5(strBytes, strLen, result) + + var hash = "" + for i in 0.. AppIntentPayload? { + let RCTStorageDirectory = "RCTAsyncLocalStorage_V1" + let fileManager = FileManager.default + + guard + let appSupportDirectory = fileManager.urls( + for: .applicationSupportDirectory, in: .userDomainMask + ).first + else { + print("Couldn't find application support directory.") + return nil + } + + let mySupportDirectory = appSupportDirectory.appendingPathComponent( + Bundle.main.bundleIdentifier!) + let storageDirectory = mySupportDirectory.appendingPathComponent(RCTStorageDirectory) + + let appIntentFile = storageDirectory.appendingPathComponent("appintent.json") + do { + let data = try Data(contentsOf: appIntentFile) + let decoder = JSONDecoder() + let appIntentPayload = try decoder.decode(AppIntentPayload.self, from: data) + return appIntentPayload } catch { - print("Failed to read manifest, creating new..") + print("Error: \(error)") + } + return nil + } + + func fetchAppItem(with name: String) -> AppInfo? { + let RCTStorageDirectory = "RCTAsyncLocalStorage_V1" + let fileManager = FileManager.default + + guard + let appSupportDirectory = fileManager.urls( + for: .applicationSupportDirectory, in: .userDomainMask + ).first + else { + print("Couldn't find application support directory.") + return nil } - if dict == nil { - dict = [:] + + let mySupportDirectory = appSupportDirectory.appendingPathComponent( + Bundle.main.bundleIdentifier!) + let storageDirectory = mySupportDirectory.appendingPathComponent(RCTStorageDirectory) + + let manifestFile = storageDirectory.appendingPathComponent("manifest.json") + let appsFile = storageDirectory.appendingPathComponent(MD5(string: "apps")) + let decoder = JSONDecoder() + + do { + if let data = try? Data(contentsOf: manifestFile), + let manifest = try? decoder.decode(Manifest.self, from: data), + let appsData = manifest.apps.data(using: .utf8), + let apps = try? decoder.decode([AppInfo].self, from: appsData) + { + return apps.first(where: { $0.name == name }) + } + + if let data = try? Data(contentsOf: appsFile), + let apps = try? decoder.decode([AppInfo].self, from: data) + { + return apps.first(where: { $0.name == name }) + } + } catch { + print("Error while fetching apps: \(error)") } - let redirectToApp = true - if dict!["openedApp"] != nil && dict!["openedApp"]!.contains("_app-reopen") { - dict!["openedApp"] = nil - } else { - let app = appPrompt - dict!["openedApp"] = "\(app!)_\(Date().timeIntervalSince1970)_break-start" + + return nil + } + + @MainActor + func perform() async throws -> some IntentResult & ReturnsValue { + var appIntentPayload = getAppIntentPayload() + var appInfo = fetchAppItem(with: appPrompt!) + var canOpenApp = true + do { + if appIntentPayload?.openedApp != nil && appInfo != nil { + canOpenApp = appInfo!.active + if canOpenApp, + let payload = appIntentPayload, + let openedApp = payload.openedApp, + !openedApp.contains("_app-reopen") + { + let lastOpen = appIntentPayload?.openedApp?.split(separator: "_") + let lastOpenTime = Double(lastOpen![1])! + let quickAppSwitchDurationAsTimestamp = + Double(appInfo!.settings.quickAppSwitchDurationMinutes * 60) + let timeSinceLastOpen = Date().timeIntervalSince1970 - lastOpenTime + let timeSinceLastOpenMinutes = timeSinceLastOpen / 60 + print("timeSinceLastOpen", timeSinceLastOpenMinutes) + canOpenApp = + Double(appInfo?.settings.quickAppSwitchDurationMinutes ?? 0) + < Double( + timeSinceLastOpenMinutes) + } + } + } catch { + print("Error while fetching apps: \(error)") } - if dict != nil { - do { - let stringified = asString(dict: dict!) - if !fileManager.fileExists(atPath: storageDirectory.path) { - try fileManager.createDirectory( - atPath: storageDirectory.path, withIntermediateDirectories: true, attributes: nil) + if canOpenApp { + if let payload = appIntentPayload, + let openedApp = payload.openedApp, + openedApp.contains("_app-reopen") + { + appIntentPayload?.openedApp = nil + print("dont open app because it was just reopened") + } else { + let app = appPrompt + appIntentPayload = AppIntentPayload( + openedApp: "\(app!)_\(Date().timeIntervalSince1970)_break-start") + print("open app") + } + if appIntentPayload != nil { + let (storageDirectory, storageFile) = getStorageDirAndFile(fileName: "appintent.json") + let fileManager = FileManager.default + do { + let jsonEncoder = JSONEncoder() + let jsonData = try jsonEncoder.encode(appIntentPayload) + if !fileManager.fileExists(atPath: storageDirectory.path) { + try fileManager.createDirectory( + atPath: storageDirectory.path, withIntermediateDirectories: true, attributes: nil) + } + let stringified = String(data: jsonData, encoding: .utf8)! + try stringified.write(to: storageFile, atomically: true, encoding: .utf8) + } catch { + print(error) } - try stringified.write(to: storageFile, atomically: true, encoding: .utf8) - let input = try String(contentsOf: storageFile) - print(input) - } catch { - print(error) } + } else { + print("dont open app because settings disallow it") } - print("Should open app: \(dict!["openedApp"] != nil)") + return .result( - value: dict!["openedApp"] != nil) + value: canOpenApp && appIntentPayload?.openedApp != nil) } func asString(dict: [String: String]) -> String { do {