2023-08-18 18:16:05 -05:00
|
|
|
//
|
|
|
|
|
// 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: ""
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-18 19:24:31 -05:00
|
|
|
let presentsNotifications: Bool
|
|
|
|
|
|
2023-08-18 18:16:05 -05:00
|
|
|
private let operationActor = OperationActor()
|
|
|
|
|
|
2023-08-18 19:24:31 -05:00
|
|
|
init(presentsNotifications: Bool)
|
2023-08-18 18:16:05 -05:00
|
|
|
{
|
2023-08-18 19:24:31 -05:00
|
|
|
self.presentsNotifications = presentsNotifications
|
|
|
|
|
|
2023-08-18 18:16:05 -05:00
|
|
|
self.progress.completedUnitCount = 0
|
|
|
|
|
self.progress.totalUnitCount = 1
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-18 19:24:31 -05:00
|
|
|
init()
|
|
|
|
|
{
|
|
|
|
|
self.init(presentsNotifications: false)
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-18 18:16:05 -05:00
|
|
|
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.
|
2023-08-18 19:24:31 -05:00
|
|
|
taskGroup.cancelAll()
|
2023-08-18 18:16:05 -05:00
|
|
|
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
|
2023-08-18 19:24:31 -05:00
|
|
|
let operation = AppManager.shared.backgroundRefresh(installedApps, presentsNotifications: self.presentsNotifications) { (result) in
|
2023-08-18 18:16:05 -05:00
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|