2.3.0 RC 3 (WIP)

This commit is contained in:
Stephen B 2025-09-23 14:14:19 -04:00
parent e90ba11535
commit f10f56e5e5
7 changed files with 572 additions and 103 deletions

View file

@ -31,6 +31,9 @@ struct HomeView: View {
@State private var isShowingInstalledApps = false
@State private var isShowingPairingFilePicker = false
@State private var pairingFileExists: Bool = false
@State private var pairingFilePresentOnDisk: Bool = false
@State private var isValidatingPairingFile = false
@State private var lastValidatedPairingSignature: PairingFileSignature? = nil
@State private var showPairingFileMessage = false
@State private var pairingFileIsValid = false
@State private var isImportingFile = false
@ -58,7 +61,7 @@ struct HomeView: View {
@State private var isLoadingQuickApps = false
@AppStorage("showiOS26Disclaimer") private var showiOS26Disclaimer: Bool = true
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
@Environment(\.themeExpansionManager) private var themeExpansion
private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle }
@ -70,6 +73,8 @@ struct HomeView: View {
private var ddiMounted: Bool { isMounted() }
private var canConnectByApp: Bool { pairingFileExists && ddiMounted }
private let pairingFileURL = URL.documentsDirectory.appendingPathComponent("pairingFile.plist")
private var isOnOrAfteriOS26: Bool {
let v = ProcessInfo.processInfo.operatingSystemVersion
@ -351,9 +356,13 @@ struct HomeView: View {
VStack(spacing: 10) {
Button(action: primaryActionTapped) {
whiteCardButtonLabel(icon: primaryActionIcon, title: primaryActionTitle, isLoading: isProcessing)
whiteCardButtonLabel(
icon: primaryActionIcon,
title: primaryActionTitle,
isLoading: isProcessing || isValidatingPairingFile
)
}
.disabled(isProcessing)
.disabled(isProcessing || isValidatingPairingFile)
if pairingFileExists && enableAdvancedOptions {
Button(action: { showPIDSheet = true }) {
@ -373,7 +382,23 @@ struct HomeView: View {
}
private var readinessSummary: ReadinessSummary {
if isValidatingPairingFile {
return .init(
title: "Validating pairing file",
subtitle: "Hang tight while we verify the pairing file from your trusted computer.",
icon: "hourglass.circle.fill",
tint: .orange
)
}
if !pairingFileExists {
if pairingFilePresentOnDisk {
return .init(
title: "Pairing file needs attention",
subtitle: "We found a pairing file but couldn’t read it. Re-import it from your trusted computer.",
icon: "doc.badge.exclamationmark",
tint: .yellow
)
}
return .init(
title: "Import your pairing file",
subtitle: "Grab the pairing file from your computer to unlock app discovery.",
@ -414,28 +439,59 @@ struct HomeView: View {
}
private var primaryActionTitle: String {
if !pairingFileExists { return "Import Pairing File" }
if isValidatingPairingFile { return "Validating…" }
if !pairingFileExists { return pairingFilePresentOnDisk ? "Re-import Pairing File" : "Import Pairing File" }
if !ddiMounted { return "Mount Developer Disk Image" }
return "Connect by App"
}
private var primaryActionIcon: String {
if !pairingFileExists { return "doc.badge.plus" }
if isValidatingPairingFile { return "hourglass" }
if !pairingFileExists { return pairingFilePresentOnDisk ? "arrow.clockwise" : "doc.badge.plus" }
if !ddiMounted { return "externaldrive" }
return "cable.connector.horizontal"
}
private var readinessChecklist: [ChecklistItem] {
let vpnConnected = tunnel.tunnelStatus == .connected
let pairingItem: ChecklistItem
if isValidatingPairingFile {
pairingItem = ChecklistItem(
title: "Pairing file",
subtitle: "Validating pairing file…",
status: .waiting,
actionTitle: nil,
action: nil
)
} else if pairingFileExists {
pairingItem = ChecklistItem(
title: "Pairing file",
subtitle: "Imported and valid.",
status: .ready,
actionTitle: nil,
action: nil
)
} else if pairingFilePresentOnDisk {
pairingItem = ChecklistItem(
title: "Pairing file",
subtitle: "We couldn’t read the pairing file that’s on disk. Re-import it from your trusted computer.",
status: .attention,
actionTitle: "Re-import",
action: { isShowingPairingFilePicker = true }
)
} else {
pairingItem = ChecklistItem(
title: "Pairing file",
subtitle: "Import the pairing file generated from your trusted computer.",
status: .actionRequired,
actionTitle: "Import",
action: { isShowingPairingFilePicker = true }
)
}
return [
ChecklistItem(
title: "Pairing file",
subtitle: pairingFileExists ? "Imported and valid." : "Import the pairing file generated from your trusted computer.",
status: pairingFileExists ? .ready : .actionRequired,
actionTitle: pairingFileExists ? nil : "Import",
action: pairingFileExists ? nil : { isShowingPairingFilePicker = true }
),
pairingItem,
ChecklistItem(
title: "Developer Disk Image",
subtitle: ddiMounted ? "Mounted successfully." : "Open Settings → Developer Disk Image and tap Mount.",
@ -755,6 +811,7 @@ struct HomeView: View {
}
private func primaryActionTapped() {
guard !isValidatingPairingFile else { return }
if pairingFileExists {
if !ddiMounted {
showAlert(title: "Device Not Mounted".localized, message: "The Developer Disk Image has not been mounted yet. Check in settings for more information.".localized, showOk: true) { _ in }
@ -790,8 +847,43 @@ struct HomeView: View {
}
private func checkPairingFileExists() {
let fileExists = FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path)
pairingFileExists = fileExists && isPairing()
let fileExists = FileManager.default.fileExists(atPath: pairingFileURL.path)
pairingFilePresentOnDisk = fileExists
guard fileExists else {
pairingFileExists = false
lastValidatedPairingSignature = nil
isValidatingPairingFile = false
return
}
let signature = pairingFileSignature(for: pairingFileURL)
guard needsValidation(for: signature) else { return }
guard !isValidatingPairingFile else { return }
isValidatingPairingFile = true
DispatchQueue.global(qos: .utility).async {
let valid = isPairing()
DispatchQueue.main.async {
pairingFileExists = valid
lastValidatedPairingSignature = signature
isValidatingPairingFile = false
}
}
}
private func needsValidation(for signature: PairingFileSignature) -> Bool {
guard let lastSignature = lastValidatedPairingSignature else { return true }
return lastSignature != signature
}
private func pairingFileSignature(for url: URL) -> PairingFileSignature {
let attributes = (try? FileManager.default.attributesOfItem(atPath: url.path)) ?? [:]
let modificationDate = attributes[.modificationDate] as? Date
let sizeValue = (attributes[.size] as? NSNumber)?.uint64Value ?? 0
return PairingFileSignature(modificationDate: modificationDate, fileSize: sizeValue)
}
private func refreshBackground() { }
@ -1095,6 +1187,11 @@ private struct ReadinessSummary {
let tint: Color
}
private struct PairingFileSignature: Equatable {
let modificationDate: Date?
let fileSize: UInt64
}
private enum ChecklistStatus {
case ready
case waiting

View file

@ -28,12 +28,16 @@ struct InstalledAppsListView: View {
}
}
@AppStorage("loadAppIconsOnJIT") private var loadAppIconsOnJIT = true
@AppStorage("performanceMode") private var performanceMode = false
@State private var showPerformanceToast = false
@State private var launchingBundles: Set<String> = []
@State private var launchFeedback: LaunchFeedback? = nil
@State private var debuggableSearchText: String = ""
@State private var otherSearchText: String = ""
@State private var prefetchedBundleIDs: Set<String> = []
@State private var selectedTab: AppListTab = .debuggable
@State private var systemSearchText: String = ""
@Environment(\.dismiss) private var dismiss
var onSelectApp: (String) -> Void
@ -78,6 +82,7 @@ struct InstalledAppsListView: View {
private enum AppListTab: String, CaseIterable, Identifiable {
case debuggable
case system
case other
var id: String { rawValue }
@ -85,6 +90,7 @@ struct InstalledAppsListView: View {
var title: String {
switch self {
case .debuggable: return "Debuggable"
case .system: return "System Apps"
case .other: return "Other"
}
}
@ -106,6 +112,16 @@ struct InstalledAppsListView: View {
}
}
private var sortedSystemApps: [(key: String, value: String)] {
viewModel.systemApps.sorted { lhs, rhs in
let comparison = lhs.value.localizedCaseInsensitiveCompare(rhs.value)
if comparison == .orderedSame {
return lhs.key < rhs.key
}
return comparison == .orderedAscending
}
}
private var otherSearchIsActive: Bool {
!otherSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
@ -118,6 +134,18 @@ struct InstalledAppsListView: View {
return base.filter { matches(query, bundleID: $0.key, name: $0.value) }
}
private var systemSearchIsActive: Bool {
!systemSearchText.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
}
private var filteredSystemApps: [(key: String, value: String)] {
let base = sortedSystemApps
guard systemSearchIsActive else { return base }
let query = normalizedSearchString(systemSearchText)
guard !query.isEmpty else { return base }
return base.filter { matches(query, bundleID: $0.key, name: $0.value) }
}
private func normalizedSearchString(_ text: String) -> String {
text.trimmingCharacters(in: .whitespacesAndNewlines)
.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: .current)
@ -137,6 +165,8 @@ struct InstalledAppsListView: View {
return viewModel.debuggableApps.isEmpty
case .other:
return filteredOtherApps.isEmpty
case .system:
return filteredSystemApps.isEmpty
}
}
@ -232,6 +262,20 @@ struct InstalledAppsListView: View {
}
}
.preferredColorScheme(preferredScheme)
.onAppear {
prefetchedBundleIDs.removeAll()
prefetchPriorityIcons()
}
.onChange(of: favoriteApps) { _, _ in prefetchPriorityIcons() }
.onChange(of: recentApps) { _, _ in prefetchPriorityIcons() }
.onChange(of: viewModel.isLoading) { _, newValue in
if newValue {
prefetchedBundleIDs.removeAll()
} else {
prefetchPriorityIcons()
}
}
.onChange(of: selectedTab) { _, _ in prefetchPriorityIcons() }
}
// MARK: Empty State
@ -258,6 +302,18 @@ struct InstalledAppsListView: View {
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
case .system:
Text("No Hidden System Apps".localized)
.font(.title2.weight(.semibold))
.foregroundStyle(.primary)
Text("""
Hidden system apps appear here once CoreDevice exposes them. Try launching a bundle from the Other tab to surface background services.
""".localized)
.font(.body)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
case .other:
Text("Nothing To Show".localized)
.font(.title2.weight(.semibold))
@ -275,7 +331,18 @@ struct InstalledAppsListView: View {
.padding(24)
.glassCard(cornerRadius: 24, material: .thinMaterial, strokeOpacity: 0.12)
.accessibilityElement(children: .combine)
.accessibilityLabel("Empty list. Import pairing file or sign app appropriately.".localized)
.accessibilityLabel(emptyStateAccessibilityLabel(for: tab).localized)
}
private func emptyStateAccessibilityLabel(for tab: AppListTab) -> String {
switch tab {
case .debuggable:
return "No debuggable apps available"
case .system:
return "No hidden system apps available"
case .other:
return "No other apps available"
}
}
// MARK: Apps List
@ -345,36 +412,84 @@ struct InstalledAppsListView: View {
}
}
private func otherSections(apps: [(key: String, value: String)]) -> some View {
VStack(spacing: 18) {
glassSection(title: "How It Works".localized) {
VStack(alignment: .leading, spacing: 8) {
Text("Launch apps without get-task-allow by tunneling through CoreDevice. This only starts the app; attach debugging from Recents once it exposes get-task-allow.".localized)
.font(.footnote)
.foregroundStyle(.secondary)
}
}
private func prefetchPriorityIcons(limit: Int = 32) {
guard loadAppIconsOnJIT else { return }
glassSection(title: "All Installed Apps".localized) {
LazyVStack(spacing: 12) {
ForEach(apps, id: \.key) { bundleID, appName in
LaunchAppRow(
bundleID: bundleID,
appName: appName,
isLaunching: launchingBundles.contains(bundleID),
appIcons: $appIcons,
performanceMode: performanceMode
) {
startLaunching(bundleID: bundleID)
}
var priorityIDs: [String] = []
var seen = Set<String>()
func appendUnique<S: Sequence>(_ ids: S) where S.Element == String {
guard priorityIDs.count < limit else { return }
for id in ids {
guard seen.insert(id).inserted else { continue }
priorityIDs.append(id)
if priorityIDs.count >= limit { break }
}
}
appendUnique(favoriteApps)
appendUnique(recentApps)
appendUnique(debuggableSortedApps.map { $0.key })
appendUnique(sortedSystemApps.map { $0.key })
appendUnique(sortedOtherApps.map { $0.key })
let toPrefetch = priorityIDs.filter { !prefetchedBundleIDs.contains($0) }
guard !toPrefetch.isEmpty else { return }
prefetchedBundleIDs.formUnion(toPrefetch)
IconCache.shared.prefetchIcons(for: toPrefetch)
}
private func otherSections(apps: [(key: String, value: String)]) -> some View {
glassSection(title: "Installed Apps".localized) {
LazyVStack(spacing: 12) {
ForEach(apps, id: \.key) { bundleID, appName in
LaunchAppRow(
bundleID: bundleID,
appName: appName,
isLaunching: launchingBundles.contains(bundleID),
appIcons: $appIcons,
performanceMode: performanceMode
) {
startLaunching(bundleID: bundleID)
}
}
}
}
}
private func systemSections(apps: [(key: String, value: String)]) -> some View {
glassSection(title: "Hidden System Apps".localized) {
LazyVStack(spacing: 12) {
ForEach(apps, id: \.key) { bundleID, appName in
LaunchAppRow(
bundleID: bundleID,
appName: appName,
isLaunching: launchingBundles.contains(bundleID),
appIcons: $appIcons,
performanceMode: performanceMode
) {
startLaunching(bundleID: bundleID)
}
}
}
}
}
@ViewBuilder
private var tabbedContent: some View {
tabContent(for: .debuggable)
VStack(spacing: 14) {
Picker("", selection: $selectedTab) {
ForEach(AppListTab.allCases) { tab in
Text(tab.title.localized)
.tag(tab)
}
}
.pickerStyle(.segmented)
.padding(.horizontal, 20)
tabContent(for: selectedTab)
}
}
@ViewBuilder
@ -415,6 +530,8 @@ struct InstalledAppsListView: View {
.padding(.vertical, 24)
}
}
case .system:
systemTabContent(apps: filteredSystemApps)
case .other:
otherTabContent(apps: filteredOtherApps)
}
@ -446,6 +563,32 @@ struct InstalledAppsListView: View {
}
}
private func systemTabContent(apps: [(key: String, value: String)]) -> some View {
ScrollView {
VStack(spacing: 18) {
systemSearchBar
if let error = viewModel.lastError {
errorBanner(error)
}
if apps.isEmpty {
if systemSearchIsActive {
systemSearchEmptyState
.transition(.opacity.combined(with: .scale))
} else {
emptyState(for: .system)
.transition(.opacity.combined(with: .scale))
}
} else {
systemSections(apps: apps)
}
}
.padding(.horizontal, 20)
.padding(.vertical, 24)
}
}
private var debuggableSearchBar: some View {
HStack(spacing: 8) {
Image(systemName: "magnifyingglass")
@ -544,6 +687,55 @@ struct InstalledAppsListView: View {
)
}
private var systemSearchBar: some View {
HStack(spacing: 8) {
Image(systemName: "magnifyingglass")
.foregroundStyle(.secondary)
TextField("Search system apps".localized, text: $systemSearchText)
.textInputAutocapitalization(.never)
.autocorrectionDisabled(true)
if !systemSearchText.isEmpty {
Button {
systemSearchText = ""
} label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 16))
.foregroundStyle(.secondary)
}
.accessibilityLabel("Clear search".localized)
}
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.background(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.fill(.ultraThinMaterial)
.overlay(
RoundedRectangle(cornerRadius: 16, style: .continuous)
.stroke(.white.opacity(0.08), lineWidth: 1)
)
)
}
private var systemSearchEmptyState: some View {
VStack(spacing: 12) {
Image(systemName: "magnifyingglass")
.font(.system(size: 44, weight: .regular))
.foregroundStyle(.secondary)
Text("No hidden matches".localized)
.font(.title3.weight(.semibold))
Text("Try another bundle identifier or internal process name.".localized)
.font(.callout)
.foregroundStyle(.secondary)
}
.padding(24)
.glassCard(cornerRadius: 20, material: .thinMaterial, strokeOpacity: 0.12)
}
private func errorBanner(_ message: String) -> some View {
Text(message)
.font(.footnote)
@ -818,7 +1010,8 @@ struct AppButton: View {
private func loadAppIcon(for bundleID: String) {
guard loadAppIconsOnJIT else { return }
IconCache.shared.fetchIcon(for: bundleID) { image in
guard appIcons[bundleID] == nil else { return }
IconCache.shared.fetchIcon(for: bundleID, priority: .veryHigh) { image in
guard let image else { return }
let shouldAnimate = appIcons[bundleID] == nil
if shouldAnimate {
@ -934,7 +1127,9 @@ struct LaunchAppRow: View {
}
private func loadAppIcon(for bundleID: String) {
IconCache.shared.fetchIcon(for: bundleID) { image in
guard loadAppIconsOnJIT else { return }
guard appIcons[bundleID] == nil else { return }
IconCache.shared.fetchIcon(for: bundleID, priority: .veryHigh) { image in
guard let image else { return }
let shouldAnimate = appIcons[bundleID] == nil
if shouldAnimate {
@ -1024,6 +1219,7 @@ final class IconCache {
private let queue = OperationQueue()
private let callbackQueue = DispatchQueue(label: "com.stik.iconcache.callbacks")
private var pendingCallbacks: [String: [(UIImage?) -> Void]] = [:]
private var pendingOperations: [String: Operation] = [:]
private init() {
queue.maxConcurrentOperationCount = 4
@ -1032,7 +1228,11 @@ final class IconCache {
mem.totalCostLimit = 64 * 1024 * 1024
}
func fetchIcon(for bundleID: String, completion: @escaping (UIImage?) -> Void) {
func fetchIcon(
for bundleID: String,
priority: Operation.QueuePriority = .normal,
completion: @escaping (UIImage?) -> Void
) {
if let memImage = mem.object(forKey: bundleID as NSString) {
DispatchQueue.main.async { completion(memImage) }
return
@ -1043,6 +1243,9 @@ final class IconCache {
if var existing = pendingCallbacks[bundleID] {
existing.append(completion)
pendingCallbacks[bundleID] = existing
if let op = pendingOperations[bundleID], op.queuePriority.rawValue < priority.rawValue {
op.queuePriority = priority
}
} else {
pendingCallbacks[bundleID] = [completion]
shouldStartLoad = true
@ -1051,7 +1254,7 @@ final class IconCache {
guard shouldStartLoad else { return }
queue.addOperation { [weak self] in
let operation = BlockOperation { [weak self] in
guard let self else { return }
var loadedFromDisk = false
@ -1070,16 +1273,19 @@ final class IconCache {
}
if let image = result {
let cost = Int(image.size.width * image.size.height * image.scale * image.scale)
self.mem.setObject(image, forKey: bundleID as NSString, cost: cost)
let prepared = self.prepareForDisplay(image)
let cost = Int(prepared.size.width * prepared.size.height * prepared.scale * prepared.scale)
self.mem.setObject(prepared, forKey: bundleID as NSString, cost: cost)
if !loadedFromDisk {
saveIconToGroup(image, bundleID: bundleID)
saveIconToGroup(prepared, bundleID: bundleID)
}
result = prepared
}
let callbacks = self.callbackQueue.sync { () -> [(UIImage?) -> Void] in
let handlers = self.pendingCallbacks[bundleID] ?? []
self.pendingCallbacks[bundleID] = nil
self.pendingOperations[bundleID] = nil
return handlers
}
@ -1087,6 +1293,20 @@ final class IconCache {
callbacks.forEach { $0(result) }
}
}
operation.queuePriority = priority
callbackQueue.sync {
pendingOperations[bundleID] = operation
}
queue.addOperation(operation)
}
func prefetchIcons(for bundleIDs: [String]) {
let uniqueIDs = Set(bundleIDs)
for bundleID in uniqueIDs {
fetchIcon(for: bundleID, priority: .veryLow) { _ in }
}
}
private func diskCachedIcon(for bundleID: String) -> UIImage? {
@ -1097,7 +1317,25 @@ final class IconCache {
let iconsDir = containerURL.appendingPathComponent("icons", isDirectory: true)
let fileURL = iconsDir.appendingPathComponent("\(bundleID).png")
guard FileManager.default.fileExists(atPath: fileURL.path) else { return nil }
return UIImage(contentsOfFile: fileURL.path)
guard let data = try? Data(contentsOf: fileURL) else {
try? FileManager.default.removeItem(at: fileURL)
return nil
}
if let image = UIImage(data: data, scale: UIScreen.main.scale) {
return image
}
try? FileManager.default.removeItem(at: fileURL)
return nil
}
private func prepareForDisplay(_ image: UIImage) -> UIImage {
if #available(iOS 15.0, *) {
return image.preparingForDisplay() ?? image
}
return image
}
}
@ -1184,6 +1422,7 @@ extension Array: @retroactive RawRepresentable where Element: Codable {
class InstalledAppsViewModel: ObservableObject {
@Published var debuggableApps: [String: String] = [:]
@Published var nonDebuggableApps: [String: String] = [:]
@Published var systemApps: [String: String] = [:]
@Published var isLoading = false
@Published var lastError: String? = nil
@ -1200,19 +1439,35 @@ class InstalledAppsViewModel: ObservableObject {
do {
let debuggable = try JITEnableContext.shared.getAppList()
let allApps = try JITEnableContext.shared.getAllApps()
let hiddenSystem = (try? JITEnableContext.shared.getHiddenSystemApps()) ?? [:]
let nonDebuggableSequence = allApps.filter { debuggable[$0.key] == nil }
let nonDebuggable = Dictionary(uniqueKeysWithValues: nonDebuggableSequence.map { ($0.key, $0.value) })
var nonDebuggable: [String: String] = [:]
var system: [String: String] = [:]
for (bundle, name) in nonDebuggableSequence {
if let hiddenName = hiddenSystem[bundle] {
system[bundle] = hiddenName
} else {
nonDebuggable[bundle] = name
}
}
for (bundle, name) in hiddenSystem where system[bundle] == nil && debuggable[bundle] == nil {
system[bundle] = name
}
DispatchQueue.main.async {
self.debuggableApps = debuggable
self.nonDebuggableApps = nonDebuggable
self.systemApps = system
self.isLoading = false
}
} catch {
DispatchQueue.main.async {
self.debuggableApps = [:]
self.nonDebuggableApps = [:]
self.systemApps = [:]
self.isLoading = false
self.lastError = error.localizedDescription
print("Failed to load apps: \(error)")

View file

@ -23,6 +23,7 @@ typedef void (^SyslogErrorHandler)(NSError *error);
- (BOOL)debugAppWithPID:(int)pid logger:(LogFunc)logger jsCallback:(DebugAppCallback)jsCallback;
- (NSDictionary<NSString*, NSString*>*)getAppListWithError:(NSError**)error;
- (NSDictionary<NSString*, NSString*>*)getAllAppsWithError:(NSError**)error;
- (NSDictionary<NSString*, NSString*>*)getHiddenSystemAppsWithError:(NSError**)error;
- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error;
- (BOOL)launchAppWithoutDebug:(NSString*)bundleID logger:(LogFunc)logger;
- (void)startSyslogRelayWithHandler:(SyslogLineHandler)lineHandler

View file

@ -203,6 +203,22 @@ JITEnableContext* sharedJITContext = nil;
return apps;
}
- (NSDictionary<NSString*, NSString*>*)getHiddenSystemAppsWithError:(NSError**)error {
if (!provider) {
NSLog(@"Provider not initialized!");
*error = [self errorWithStr:@"Provider not initialized!" code:-1];
return nil;
}
NSString* errorStr = nil;
NSDictionary<NSString*, NSString*>* apps = list_hidden_system_apps(provider, &errorStr);
if (errorStr) {
*error = [self errorWithStr:errorStr code:-17];
return nil;
}
return apps;
}
- (UIImage*)getAppIconWithBundleId:(NSString*)bundleId error:(NSError**)error {
if (!provider) {
NSLog(@"Provider not initialized!");

View file

@ -12,6 +12,7 @@
NSDictionary<NSString*, NSString*>* list_installed_apps(IdeviceProviderHandle* provider, NSString** error);
NSDictionary<NSString*, NSString*>* list_all_apps(IdeviceProviderHandle* provider, NSString** error);
NSDictionary<NSString*, NSString*>* list_hidden_system_apps(IdeviceProviderHandle* provider, NSString** error);
UIImage* getAppIcon(IdeviceProviderHandle* provider, NSString* bundleID, NSString** error);
#endif /* APPLIST_H */

View file

@ -11,9 +11,101 @@
#include <string.h>
#import "applist.h"
static NSString *extractAppName(plist_t app)
{
plist_t displayNameNode = plist_dict_get_item(app, "CFBundleDisplayName");
if (displayNameNode) {
char *displayNameC = NULL;
plist_get_string_val(displayNameNode, &displayNameC);
if (displayNameC && displayNameC[0] != '\0') {
NSString *displayName = [NSString stringWithUTF8String:displayNameC];
free(displayNameC);
return displayName;
}
free(displayNameC);
}
plist_t nameNode = plist_dict_get_item(app, "CFBundleName");
if (nameNode) {
char *nameC = NULL;
plist_get_string_val(nameNode, &nameC);
if (nameC && nameC[0] != '\0') {
NSString *name = [NSString stringWithUTF8String:nameC];
free(nameC);
return name;
}
free(nameC);
}
return @"Unknown";
}
static BOOL nodeContainsHiddenTag(plist_t tagsNode)
{
if (!tagsNode || plist_get_node_type(tagsNode) != PLIST_ARRAY) {
return NO;
}
uint32_t tagsCount = plist_array_get_size(tagsNode);
for (uint32_t i = 0; i < tagsCount; i++) {
plist_t tagNode = plist_array_get_item(tagsNode, i);
if (!tagNode || plist_get_node_type(tagNode) != PLIST_STRING) {
continue;
}
char *tagC = NULL;
plist_get_string_val(tagNode, &tagC);
if (!tagC) {
continue;
}
BOOL isHidden = (strcmp(tagC, "hidden") == 0 || strcmp(tagC, "hidden-system-app") == 0);
free(tagC);
if (isHidden) {
return YES;
}
}
return NO;
}
static BOOL isHiddenSystemApp(plist_t app)
{
plist_t typeNode = plist_dict_get_item(app, "ApplicationType");
BOOL isSystemType = NO;
if (typeNode && plist_get_node_type(typeNode) == PLIST_STRING) {
char *typeC = NULL;
plist_get_string_val(typeNode, &typeC);
if (typeC) {
if (strcmp(typeC, "System") == 0 || strcmp(typeC, "HiddenSystemApp") == 0) {
isSystemType = YES;
}
free(typeC);
}
}
if (!isSystemType) {
return NO;
}
plist_t hiddenNode = plist_dict_get_item(app, "IsHidden");
if (hiddenNode && plist_get_node_type(hiddenNode) == PLIST_BOOLEAN) {
uint8_t hidden = 0;
plist_get_bool_val(hiddenNode, &hidden);
if (hidden) {
return YES;
}
}
plist_t tagsNode = plist_dict_get_item(app, "SBAppTags");
if (nodeContainsHiddenTag(tagsNode)) {
return YES;
}
return NO;
}
static NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
size_t count,
BOOL requireGetTaskAllow)
BOOL requireGetTaskAllow,
BOOL (^filter)(plist_t app))
{
NSMutableDictionary<NSString*, NSString*> *result = [NSMutableDictionary dictionaryWithCapacity:count];
@ -31,6 +123,10 @@ static NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
if (!isAllowed) continue;
}
if (filter && !filter(app)) {
continue;
}
plist_t bidNode = plist_dict_get_item(app, "CFBundleIdentifier");
if (!bidNode) continue;
@ -44,18 +140,7 @@ static NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
NSString *bundleID = [NSString stringWithUTF8String:bidC];
free(bidC);
NSString *appName = @"Unknown";
plist_t nameNode = plist_dict_get_item(app, "CFBundleName");
if (nameNode) {
char *nameC = NULL;
plist_get_string_val(nameNode, &nameC);
if (nameC && nameC[0] != '\0') {
appName = [NSString stringWithUTF8String:nameC];
}
free(nameC);
}
result[bundleID] = appName;
result[bundleID] = extractAppName(app);
}
return result;
@ -63,7 +148,8 @@ static NSDictionary<NSString*, NSString*> *buildAppDictionary(void *apps,
static NSDictionary<NSString*, NSString*> *performAppQuery(IdeviceProviderHandle *provider,
BOOL requireGetTaskAllow,
NSString **error)
NSString **error,
BOOL (^filter)(plist_t app))
{
InstallationProxyClientHandle *client = NULL;
if (installation_proxy_connect_tcp(provider, &client)) {
@ -79,17 +165,23 @@ static NSDictionary<NSString*, NSString*> *performAppQuery(IdeviceProviderHandle
return nil;
}
NSDictionary<NSString*, NSString*> *result = buildAppDictionary(apps, count, requireGetTaskAllow);
NSDictionary<NSString*, NSString*> *result = buildAppDictionary(apps, count, requireGetTaskAllow, filter);
installation_proxy_client_free(client);
return result;
}
NSDictionary<NSString*, NSString*>* list_installed_apps(IdeviceProviderHandle* provider, NSString** error) {
return performAppQuery(provider, YES, error);
return performAppQuery(provider, YES, error, nil);
}
NSDictionary<NSString*, NSString*>* list_all_apps(IdeviceProviderHandle* provider, NSString** error) {
return performAppQuery(provider, NO, error);
return performAppQuery(provider, NO, error, nil);
}
NSDictionary<NSString*, NSString*>* list_hidden_system_apps(IdeviceProviderHandle* provider, NSString** error) {
return performAppQuery(provider, NO, error, ^BOOL(plist_t app) {
return isHiddenSystemApp(app);
});
}
UIImage* getAppIcon(IdeviceProviderHandle* provider, NSString* bundleID, NSString** error) {

View file

@ -112,8 +112,10 @@ int debug_app(IdeviceProviderHandle* tcp_provider, const char *bundle_id, LogFun
fprintf(stderr, "Failed to create TCP adapter: [%d] %s\n", err->code,
err->message);
idevice_error_free(err);
core_device_proxy_free(core_device);
return 1;
}
core_device = NULL; // adapter takes ownership of the proxy
AdapterStreamHandle *stream = NULL;
err = adapter_connect(adapter, rsd_port, (ReadWriteOpaque **)&stream);
@ -138,6 +140,7 @@ int debug_app(IdeviceProviderHandle* tcp_provider, const char *bundle_id, LogFun
adapter_free(adapter);
return 1;
}
stream = NULL;
// Create RemoteServerClient
RemoteServerHandle *remote_server = NULL;
@ -237,8 +240,10 @@ int debug_app_pid(IdeviceProviderHandle* tcp_provider, int pid, LogFuncC logger,
fprintf(stderr, "Failed to create TCP adapter: [%d] %s\n", err->code,
err->message);
idevice_error_free(err);
core_device_proxy_free(core_device);
return 1;
}
core_device = NULL;
AdapterStreamHandle *stream = NULL;
err = adapter_connect(adapter, rsd_port, (ReadWriteOpaque **)&stream);
@ -263,6 +268,7 @@ int debug_app_pid(IdeviceProviderHandle* tcp_provider, int pid, LogFuncC logger,
adapter_free(adapter);
return 1;
}
stream = NULL;
// Create RemoteServerClient
RemoteServerHandle *remote_server = NULL;
@ -308,76 +314,66 @@ int launch_app_via_proxy(IdeviceProviderHandle* tcp_provider, const char *bundle
IdeviceFfiError* err = NULL;
CoreDeviceProxyHandle *core_device = NULL;
AdapterHandle *adapter = NULL;
AdapterStreamHandle *stream = NULL;
RsdHandshakeHandle *handshake = NULL;
RemoteServerHandle *remote_server = NULL;
ProcessControlHandle *process_control = NULL;
uint64_t pid = 0;
int result = 1;
err = core_device_proxy_connect(tcp_provider, &core_device);
if (err != NULL) {
fprintf(stderr, "Failed to connect to CoreDeviceProxy: [%d] %s\n", err->code, err->message);
idevice_error_free(err);
return 1;
goto cleanup;
}
uint16_t rsd_port;
uint16_t rsd_port = 0;
err = core_device_proxy_get_server_rsd_port(core_device, &rsd_port);
if (err != NULL) {
fprintf(stderr, "Failed to get server RSD port: [%d] %s\n", err->code, err->message);
idevice_error_free(err);
core_device_proxy_free(core_device);
return 1;
goto cleanup;
}
AdapterHandle *adapter = NULL;
err = core_device_proxy_create_tcp_adapter(core_device, &adapter);
if (err != NULL) {
fprintf(stderr, "Failed to create TCP adapter: [%d] %s\n", err->code, err->message);
idevice_error_free(err);
core_device_proxy_free(core_device);
return 1;
goto cleanup;
}
core_device = NULL; // ownership transferred to adapter
AdapterStreamHandle *stream = NULL;
err = adapter_connect(adapter, rsd_port, (ReadWriteOpaque **)&stream);
if (err != NULL) {
fprintf(stderr, "Failed to connect to RSD port: [%d] %s\n", err->code, err->message);
idevice_error_free(err);
adapter_free(adapter);
core_device_proxy_free(core_device);
return 1;
goto cleanup;
}
RsdHandshakeHandle *handshake = NULL;
err = rsd_handshake_new((ReadWriteOpaque *)stream, &handshake);
if (err != NULL) {
fprintf(stderr, "Failed to perform RSD handshake: [%d] %s\n", err->code, err->message);
idevice_error_free(err);
adapter_close(stream);
adapter_free(adapter);
core_device_proxy_free(core_device);
return 1;
goto cleanup;
}
stream = NULL; // consumed by handshake/adapter stack
RemoteServerHandle *remote_server = NULL;
err = remote_server_connect_rsd(adapter, handshake, &remote_server);
if (err != NULL) {
fprintf(stderr, "Failed to create remote server: [%d] %s\n", err->code, err->message);
idevice_error_free(err);
rsd_handshake_free(handshake);
adapter_free(adapter);
core_device_proxy_free(core_device);
return 1;
goto cleanup;
}
ProcessControlHandle *process_control = NULL;
err = process_control_new(remote_server, &process_control);
if (err != NULL) {
fprintf(stderr, "Failed to create process control client: [%d] %s\n", err->code, err->message);
idevice_error_free(err);
remote_server_free(remote_server);
rsd_handshake_free(handshake);
adapter_free(adapter);
core_device_proxy_free(core_device);
return 1;
goto cleanup;
}
uint64_t pid = 0;
err = process_control_launch_app(process_control,
bundle_id,
NULL,
@ -390,26 +386,37 @@ int launch_app_via_proxy(IdeviceProviderHandle* tcp_provider, const char *bundle
if (err != NULL) {
fprintf(stderr, "Failed to launch app: [%d] %s\n", err->code, err->message);
idevice_error_free(err);
process_control_free(process_control);
remote_server_free(remote_server);
rsd_handshake_free(handshake);
adapter_free(adapter);
core_device_proxy_free(core_device);
if (logger) {
logger("Failed to launch app: %s", bundle_id);
}
return 1;
goto cleanup;
}
if (logger) {
logger("Launched app (PID %llu)", pid);
}
process_control_free(process_control);
remote_server_free(remote_server);
rsd_handshake_free(handshake);
adapter_free(adapter);
core_device_proxy_free(core_device);
result = 0;
return 0;
cleanup:
if (process_control) {
process_control_free(process_control);
}
if (remote_server) {
remote_server_free(remote_server);
}
if (handshake) {
rsd_handshake_free(handshake);
}
if (stream) {
adapter_close(stream);
}
if (adapter) {
adapter_free(adapter);
}
if (core_device) {
core_device_proxy_free(core_device);
}
return result;
}