From 7747994c8082cab8c599c8bbccdcbe14b7bf0ca7 Mon Sep 17 00:00:00 2001 From: Riley Testut Date: Fri, 18 Aug 2023 18:16:05 -0500 Subject: [PATCH] Converts legacy RefreshAllIntent into App Shortcut (iOS 17+) --- AltStore.xcodeproj/project.pbxproj | 28 ++- AltStore/Info.plist | 11 ++ .../Intents/App Intents/AppShortcuts.swift | 29 +++ .../App Intents/RefreshAllAppsIntent.swift | 181 ++++++++++++++++++ .../Intents/{ => Legacy}/IntentHandler.swift | 0 .../{ => Legacy}/Intents.intentdefinition | 0 .../BackgroundRefreshAppsOperation.swift | 6 +- .../GradientBottom.colorset/Contents.json | 20 ++ .../Colors/GradientTop.colorset/Contents.json | 20 ++ 9 files changed, 291 insertions(+), 4 deletions(-) create mode 100644 AltStore/Intents/App Intents/AppShortcuts.swift create mode 100644 AltStore/Intents/App Intents/RefreshAllAppsIntent.swift rename AltStore/Intents/{ => Legacy}/IntentHandler.swift (100%) rename AltStore/Intents/{ => Legacy}/Intents.intentdefinition (100%) create mode 100644 AltStore/Resources/Assets.xcassets/Colors/GradientBottom.colorset/Contents.json create mode 100644 AltStore/Resources/Assets.xcassets/Colors/GradientTop.colorset/Contents.json diff --git a/AltStore.xcodeproj/project.pbxproj b/AltStore.xcodeproj/project.pbxproj index a1f1522d..01ad53fb 100644 --- a/AltStore.xcodeproj/project.pbxproj +++ b/AltStore.xcodeproj/project.pbxproj @@ -341,6 +341,7 @@ BFF615A82510042B00484D3B /* AltStoreCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BF66EE7E2501AE50007EE018 /* AltStoreCore.framework */; }; BFF7C9342578492100E55F36 /* ALTAnisetteData.m in Sources */ = {isa = PBXBuildFile; fileRef = BFB49AA823834CF900D542D9 /* ALTAnisetteData.m */; }; D513F6162A12CE4E0061EAA1 /* SourceError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D571ADCD2A02FA7400B24B63 /* SourceError.swift */; }; + D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5151BD82A8FF64300C96F28 /* RefreshAllAppsIntent.swift */; }; D5177B0D2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel in Sources */ = {isa = PBXBuildFile; fileRef = D5177B0C2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel */; }; D5189C012A01BC6800F44625 /* UserInfoValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5189C002A01BC6800F44625 /* UserInfoValue.swift */; }; D5189C022A01BC6800F44625 /* UserInfoValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5189C002A01BC6800F44625 /* UserInfoValue.swift */; }; @@ -358,6 +359,7 @@ D540E93828EE1BDE000F1B0F /* ErrorDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D540E93728EE1BDE000F1B0F /* ErrorDetailsViewController.swift */; }; D54DED1428CBC44B008B27A0 /* ErrorLogTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */; }; D552B1D82A042A740066216F /* AppPermissionsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = D552B1D72A042A740066216F /* AppPermissionsCard.swift */; }; + D55467B82A8D5E2600F4CE90 /* AppShortcuts.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */; }; D55E163728776CB700A627A1 /* ComplicationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D55E163528776CB000A627A1 /* ComplicationView.swift */; }; D561B2EB28EF5A4F006752E4 /* AltSign-Dynamic in Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; }; D561B2ED28EF5A4F006752E4 /* AltSign-Dynamic in Embed Frameworks */ = {isa = PBXBuildFile; productRef = D561B2EA28EF5A4F006752E4 /* AltSign-Dynamic */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -909,6 +911,7 @@ D52C08ED28AEC37A006C4AE5 /* AppVersion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppVersion.swift; sourceTree = ""; }; D52E988928D002D30032BE6B /* AltStore 11.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "AltStore 11.xcdatamodel"; sourceTree = ""; }; C9EEAA842DA87A88A870053B /* Pods_AltStore.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AltStore.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + D5151BD82A8FF64300C96F28 /* RefreshAllAppsIntent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshAllAppsIntent.swift; sourceTree = ""; }; D5177B0C2A26944600270065 /* AltStore12ToAltStore13.xcmappingmodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcmappingmodel; path = AltStore12ToAltStore13.xcmappingmodel; sourceTree = ""; }; D5189C002A01BC6800F44625 /* UserInfoValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserInfoValue.swift; sourceTree = ""; }; D51AD27C29356B7B00967AAA /* ALTWrappedError.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ALTWrappedError.h; sourceTree = ""; }; @@ -926,6 +929,7 @@ D540E93728EE1BDE000F1B0F /* ErrorDetailsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorDetailsViewController.swift; sourceTree = ""; }; D54DED1328CBC44B008B27A0 /* ErrorLogTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorLogTableViewCell.swift; sourceTree = ""; }; D552B1D72A042A740066216F /* AppPermissionsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppPermissionsCard.swift; sourceTree = ""; }; + D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppShortcuts.swift; sourceTree = ""; }; D55E163528776CB000A627A1 /* ComplicationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComplicationView.swift; sourceTree = ""; }; D5708416292448DA00D42D34 /* OperatingSystemVersion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OperatingSystemVersion+Comparable.swift"; sourceTree = ""; }; D571ADCD2A02FA7400B24B63 /* SourceError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SourceError.swift; sourceTree = ""; }; @@ -1898,8 +1902,8 @@ BFF00D2E2501BD4B00746320 /* Intents */ = { isa = PBXGroup; children = ( - BFF00D2F2501BD7D00746320 /* Intents.intentdefinition */, - BFF00D332501BDCF00746320 /* IntentHandler.swift */, + D55467B02A8D5E2600F4CE90 /* App Intents */, + D55FEC9C2A8FEC600057D6E6 /* Legacy */, ); path = Intents; sourceTree = ""; @@ -1967,6 +1971,24 @@ path = Previews; sourceTree = ""; }; + D55467B02A8D5E2600F4CE90 /* App Intents */ = { + isa = PBXGroup; + children = ( + D55467B12A8D5E2600F4CE90 /* AppShortcuts.swift */, + D5151BD82A8FF64300C96F28 /* RefreshAllAppsIntent.swift */, + ); + path = "App Intents"; + sourceTree = ""; + }; + D55FEC9C2A8FEC600057D6E6 /* Legacy */ = { + isa = PBXGroup; + children = ( + BFF00D2F2501BD7D00746320 /* Intents.intentdefinition */, + BFF00D332501BDCF00746320 /* IntentHandler.swift */, + ); + path = Legacy; + sourceTree = ""; + }; D586D39928EF58B0000E101F /* AltTests */ = { isa = PBXGroup; children = ( @@ -2822,8 +2844,10 @@ BFC84A4D2421A19100853474 /* SourcesViewController.swift in Sources */, BFF0B696232242D3007A79E1 /* LicensesViewController.swift in Sources */, D57FE84428C7DB7100216002 /* ErrorLogViewController.swift in Sources */, + D5151BD92A8FF64300C96F28 /* RefreshAllAppsIntent.swift in Sources */, BFBE0007250AD0E70080826E /* ViewAppIntentHandler.swift in Sources */, BFDB6A0822AAED73007EA6D6 /* ResignAppOperation.swift in Sources */, + D55467B82A8D5E2600F4CE90 /* AppShortcuts.swift in Sources */, D593F1942717749A006E82DE /* PatchAppOperation.swift in Sources */, BF770E5122BB1CF6002A40FE /* InstallAppOperation.swift in Sources */, BF9ABA4B22DD1380008935CF /* NavigationBar.swift in Sources */, diff --git a/AltStore/Info.plist b/AltStore/Info.plist index 9cfc1bc7..3f8c0929 100644 --- a/AltStore/Info.plist +++ b/AltStore/Info.plist @@ -144,6 +144,17 @@ LaunchScreen UIMainStoryboardFile Main + CFBundleIcons + + CFBundlePrimaryIcon + + NSAppIconComplementingColorNames + + GradientTop + GradientBottom + + + UIRequiredDeviceCapabilities armv7 diff --git a/AltStore/Intents/App Intents/AppShortcuts.swift b/AltStore/Intents/App Intents/AppShortcuts.swift new file mode 100644 index 00000000..15924493 --- /dev/null +++ b/AltStore/Intents/App Intents/AppShortcuts.swift @@ -0,0 +1,29 @@ +// +// AppShortcuts.swift +// AltStore +// +// Created by Riley Testut on 8/23/22. +// Copyright © 2022 Riley Testut. All rights reserved. +// + +import AppIntents + +@available(iOS 17, *) +public struct ShortcutsProvider: AppShortcutsProvider +{ + public static var appShortcuts: [AppShortcut] { + AppShortcut(intent: RefreshAllAppsIntent(), + phrases: [ + "Refresh \(.applicationName)", + "Refresh \(.applicationName) apps", + "Refresh my \(.applicationName) apps", + "Refresh apps with \(.applicationName)", + ], + shortTitle: "Refresh All Apps", + systemImageName: "arrow.triangle.2.circlepath") + } + + public static var shortcutTileColor: ShortcutTileColor { + return .teal + } +} diff --git a/AltStore/Intents/App Intents/RefreshAllAppsIntent.swift b/AltStore/Intents/App Intents/RefreshAllAppsIntent.swift new file mode 100644 index 00000000..b7de3539 --- /dev/null +++ b/AltStore/Intents/App Intents/RefreshAllAppsIntent.swift @@ -0,0 +1,181 @@ +// +// RefreshAllAppsIntent.swift +// AltStore +// +// Created by Riley Testut on 8/18/23. +// Copyright © 2023 Riley Testut. All rights reserved. +// + +import AppIntents + +import AltStoreCore + +// Shouldn't conform types we don't own to protocols we don't own, so make custom +// NSError subclass that conforms to CustomLocalizedStringResourceConvertible instead. +// +// Would prefer to just conform ALTLocalizedError to CustomLocalizedStringResourceConvertible, +// but that can't be done without raising minimum version for ALTLocalizedError to iOS 16 :/ +@available(iOS 16, *) +class IntentError: NSError, CustomLocalizedStringResourceConvertible +{ + var localizedStringResource: LocalizedStringResource { + return "\(self.localizedDescription)" + } + + init(_ error: some Error) + { + let serializedError = (error as NSError).sanitizedForSerialization() + super.init(domain: serializedError.domain, code: serializedError.code, userInfo: serializedError.userInfo) + } + + required init?(coder: NSCoder) + { + super.init(coder: coder) + } +} + +@available(iOS 17.0, *) +extension RefreshAllAppsIntent +{ + private actor OperationActor + { + private(set) var operation: BackgroundRefreshAppsOperation? + + func set(_ operation: BackgroundRefreshAppsOperation?) + { + self.operation = operation + } + } +} + +@available(iOS 17.0, *) +struct RefreshAllAppsIntent: AppIntent, CustomIntentMigratedAppIntent, PredictableIntent, ProgressReportingIntent, ForegroundContinuableIntent +{ + static let intentClassName = "RefreshAllIntent" + + static var title: LocalizedStringResource = "Refresh All Apps" + static var description = IntentDescription("Refreshes your sideloaded apps to prevent them from expiring.") + + static var parameterSummary: some ParameterSummary { + Summary("Refresh All Apps") + } + + static var predictionConfiguration: some IntentPredictionConfiguration { + IntentPrediction { + DisplayRepresentation( + title: "Refresh All Apps", + subtitle: "" + ) + } + } + + private let operationActor = OperationActor() + + init() + { + self.progress.completedUnitCount = 0 + self.progress.totalUnitCount = 1 + } + + func perform() async throws -> some IntentResult & ProvidesDialog + { + do + { + // Request foreground execution at ~27 seconds to gracefully handle timeout. + let deadline: ContinuousClock.Instant = .now + .seconds(27) + + try await withThrowingTaskGroup(of: Void.self) { taskGroup in + taskGroup.addTask { + try await self.refreshAllApps() + } + + taskGroup.addTask { + try await Task.sleep(until: deadline) + throw OperationError.timedOut + } + + do + { + for try await _ in taskGroup.prefix(1) + { + // We only care about the first child task to complete. + break + } + } + catch OperationError.timedOut + { + // We took too long to finish and return the final result, + // so we'll now present a normal notification when finished. + let operation = await self.operationActor.operation + operation?.presentsFinishedNotification = true + + try await self.requestToContinueInForeground() + } + } + + return .result(dialog: "All apps have been refreshed.") + } + catch + { + let intentError = IntentError(error) + throw intentError + } + } +} + +@available(iOS 17.0, *) +private extension RefreshAllAppsIntent +{ + func refreshAllApps() async throws + { + if !DatabaseManager.shared.isStarted + { + try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in + DatabaseManager.shared.start { error in + if let error + { + continuation.resume(throwing: error) + } + else + { + continuation.resume() + } + } + } + } + + let context = DatabaseManager.shared.persistentContainer.newBackgroundContext() + let installedApps = await context.perform { InstalledApp.fetchAppsForRefreshingAll(in: context) } + + try await withCheckedThrowingContinuation { continuation in + let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: false) { (result) in + do + { + let results = try result.get() + + for (_, result) in results + { + guard case let .failure(error) = result else { continue } + throw error + } + + continuation.resume() + } + catch ~RefreshErrorCode.noInstalledApps + { + continuation.resume() + } + catch + { + continuation.resume(throwing: error) + } + } + + self.progress.addChild(operation.progress, withPendingUnitCount: 1) + + Task { + await self.operationActor.set(operation) + } + } + } +} diff --git a/AltStore/Intents/IntentHandler.swift b/AltStore/Intents/Legacy/IntentHandler.swift similarity index 100% rename from AltStore/Intents/IntentHandler.swift rename to AltStore/Intents/Legacy/IntentHandler.swift diff --git a/AltStore/Intents/Intents.intentdefinition b/AltStore/Intents/Legacy/Intents.intentdefinition similarity index 100% rename from AltStore/Intents/Intents.intentdefinition rename to AltStore/Intents/Legacy/Intents.intentdefinition diff --git a/AltStore/Operations/BackgroundRefreshAppsOperation.swift b/AltStore/Operations/BackgroundRefreshAppsOperation.swift index da7ca64d..f65c6b8a 100644 --- a/AltStore/Operations/BackgroundRefreshAppsOperation.swift +++ b/AltStore/Operations/BackgroundRefreshAppsOperation.swift @@ -117,10 +117,10 @@ final class BackgroundRefreshAppsOperation: ResultOperation<[String: Result