mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-09 06:43:25 +01:00
Converts legacy RefreshAllIntent into App Shortcut (iOS 17+)
This commit is contained in:
29
AltStore/Intents/App Intents/AppShortcuts.swift
Normal file
29
AltStore/Intents/App Intents/AppShortcuts.swift
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
181
AltStore/Intents/App Intents/RefreshAllAppsIntent.swift
Normal file
181
AltStore/Intents/App Intents/RefreshAllAppsIntent.swift
Normal file
@@ -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<Void, Error>) 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user