From b4d457503e0d1aee1d21dd69b6113a35e6b89014 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 22 Aug 2024 13:42:53 +0800 Subject: [PATCH 01/36] swift ui proof of conecpt --- .gitignore | 4 + LCAppDelegateSwiftUI.h | 12 + LCAppDelegateSwiftUI.m | 30 ++ .../AccentColor.colorset/Contents.json | 20 + .../AppBannerBG.colorset/Contents.json | 38 ++ .../AppIcon.appiconset/Contents.json | 13 + .../Assets.xcassets/Contents.json | 6 + .../FontColor.colorset/Contents.json | 20 + LiveContainerSwiftUI/LCAppBanner.swift | 64 +++ LiveContainerSwiftUI/LCAppListView.swift | 67 +++ LiveContainerSwiftUI/LCSettingsView.swift | 8 + LiveContainerSwiftUI/LCSwiftBridge.h | 16 + LiveContainerSwiftUI/LCSwiftBridge.m | 18 + LiveContainerSwiftUI/LCTabView.swift | 28 ++ LiveContainerSwiftUI/LCTweaksView.swift | 8 + .../LiveContainerSwiftUI-Bridging-Header.h | 13 + .../project.pbxproj | 427 ++++++++++++++++++ .../LiveContainerSwiftUIApp.swift | 16 + LiveContainerSwiftUI/Makefile | 21 + LiveContainerSwiftUI/ObjcBridge.swift | 18 + .../Preview Assets.xcassets/Contents.json | 6 + Makefile | 4 +- main.m | 5 +- 23 files changed, 858 insertions(+), 4 deletions(-) create mode 100644 LCAppDelegateSwiftUI.h create mode 100644 LCAppDelegateSwiftUI.m create mode 100644 LiveContainerSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 LiveContainerSwiftUI/Assets.xcassets/AppBannerBG.colorset/Contents.json create mode 100644 LiveContainerSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 LiveContainerSwiftUI/Assets.xcassets/Contents.json create mode 100644 LiveContainerSwiftUI/Assets.xcassets/FontColor.colorset/Contents.json create mode 100644 LiveContainerSwiftUI/LCAppBanner.swift create mode 100644 LiveContainerSwiftUI/LCAppListView.swift create mode 100644 LiveContainerSwiftUI/LCSettingsView.swift create mode 100644 LiveContainerSwiftUI/LCSwiftBridge.h create mode 100644 LiveContainerSwiftUI/LCSwiftBridge.m create mode 100644 LiveContainerSwiftUI/LCTabView.swift create mode 100644 LiveContainerSwiftUI/LCTweaksView.swift create mode 100644 LiveContainerSwiftUI/LiveContainerSwiftUI-Bridging-Header.h create mode 100644 LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj create mode 100644 LiveContainerSwiftUI/LiveContainerSwiftUIApp.swift create mode 100644 LiveContainerSwiftUI/Makefile create mode 100644 LiveContainerSwiftUI/ObjcBridge.swift create mode 100644 LiveContainerSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/.gitignore b/.gitignore index faf8687..d7927b6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ .theos/ packages/ .DS_Store +LiveContainer.xcodeproj +project.xcworkspace +xcuserdata +Resources/Assets.car \ No newline at end of file diff --git a/LCAppDelegateSwiftUI.h b/LCAppDelegateSwiftUI.h new file mode 100644 index 0000000..ae15f5a --- /dev/null +++ b/LCAppDelegateSwiftUI.h @@ -0,0 +1,12 @@ +#import +#import +@interface LCSwiftBridge : NSObject ++ (UIViewController * _Nonnull)getRootVC; +@end + +@interface LCAppDelegateSwiftUI : UIResponder + +@property (nonatomic, strong) UIWindow * _Nullable window; +@property (nonatomic, strong) UIViewController * _Nonnull rootViewController; + +@end diff --git a/LCAppDelegateSwiftUI.m b/LCAppDelegateSwiftUI.m new file mode 100644 index 0000000..6a4d04f --- /dev/null +++ b/LCAppDelegateSwiftUI.m @@ -0,0 +1,30 @@ +#import "LCAppDelegateSwiftUI.h" +#import + +@implementation LCAppDelegateSwiftUI + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + _rootViewController = [NSClassFromString(@"LCSwiftBridge") getRootVC]; + _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + _window.rootViewController = _rootViewController; + [_window makeKeyAndVisible]; + return YES; +} + +- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { + // handle page open request from URL scheme +// if([url.host isEqualToString:@"open-web-page"]) { +// NSURLComponents* urlComponent = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; +// if(urlComponent.queryItems.count == 0){ +// return YES; +// } +// +// NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:urlComponent.queryItems[0].value options:0]; +// NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; +// [((LCTabBarController*)_rootViewController) openWebPage:decodedUrl]; +// } +// return [LCUtils launchToGuestAppWithURL:url]; + return true; +} + +@end diff --git a/LiveContainerSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json b/LiveContainerSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..01c1a32 --- /dev/null +++ b/LiveContainerSwiftUI/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "158", + "red" : "64" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LiveContainerSwiftUI/Assets.xcassets/AppBannerBG.colorset/Contents.json b/LiveContainerSwiftUI/Assets.xcassets/AppBannerBG.colorset/Contents.json new file mode 100644 index 0000000..cb12ea5 --- /dev/null +++ b/LiveContainerSwiftUI/Assets.xcassets/AppBannerBG.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xF5", + "red" : "0xEC" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0x66", + "green" : "0x3F", + "red" : "0x1A" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LiveContainerSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json b/LiveContainerSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..13613e3 --- /dev/null +++ b/LiveContainerSwiftUI/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,13 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LiveContainerSwiftUI/Assets.xcassets/Contents.json b/LiveContainerSwiftUI/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LiveContainerSwiftUI/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LiveContainerSwiftUI/Assets.xcassets/FontColor.colorset/Contents.json b/LiveContainerSwiftUI/Assets.xcassets/FontColor.colorset/Contents.json new file mode 100644 index 0000000..0961451 --- /dev/null +++ b/LiveContainerSwiftUI/Assets.xcassets/FontColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0xFF", + "green" : "0xB1", + "red" : "0x66" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift new file mode 100644 index 0000000..c05112b --- /dev/null +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -0,0 +1,64 @@ +// +// LCAppBanner.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation +import SwiftUI + +struct LCAppBanner : View { + var appInfo: LCAppInfo + + + var body: some View { + + HStack { + HStack { + Image(uiImage: appInfo.icon()) + .resizable().resizable().frame(width: 60, height: 60) + .clipShape(RoundedRectangle(cornerSize: CGSize(width:12, height: 12))) + + + VStack (alignment: .leading, content: { + Text(appInfo.displayName()).font(.system(size: 16)).bold() + Text("\(appInfo.version()) - \(appInfo.bundleIdentifier())").font(.system(size: 12)).foregroundColor(Color("FontColor")) + Text(appInfo.dataUUID()).font(.system(size: 8)).foregroundColor(Color("FontColor")) + }) + } + Spacer() + Button { + runApp() + } label: { + Text("Run").bold().foregroundColor(.white) + } + .padding() + .frame(height: 32) + .background(Capsule().fill(Color("FontColor"))) + + } + .padding() + .frame(height: 88) + .background(RoundedRectangle(cornerSize: CGSize(width:22, height: 22)).fill(Color("AppBannerBG"))) + .contextMenu { + Button { + // Add this item to a list of favorites. + } label: { + Label("Add to Favorites", systemImage: "heart") + } + Button { + // Open Maps and center it on this item. + } label: { + Label("Show in Maps", systemImage: "mappin") + } + } + + + } + + func runApp() { + UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") + LCUtils.launchToGuestApp() + } +} diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift new file mode 100644 index 0000000..fc037d8 --- /dev/null +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -0,0 +1,67 @@ +// +// ContentView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import SwiftUI + +struct LCAppListView : View { + var docPath: URL + var bundlePath: URL + var apps: [LCAppInfo] + + init() { + NSLog("[NMSL] App list init!") + let fm = FileManager() + self.docPath = fm.urls(for: .documentDirectory, in: .userDomainMask).last! + self.bundlePath = self.docPath.appendingPathComponent("Applications") + do { + try fm.createDirectory(at: self.bundlePath, withIntermediateDirectories: true) + let appDirs = try fm.contentsOfDirectory(atPath: self.bundlePath.path) + self.apps = [] + for appDir in appDirs { + if !appDir.hasSuffix(".app") { + continue + } + var newApp = LCAppInfo(bundlePath: "\(self.bundlePath.path)/\(appDir)")! + newApp.relativeBundlePath = appDir + self.apps.append(newApp) + } + } catch { + self.apps = [] + NSLog("[NMSL] error:\(error)") + } + + } + + var body: some View { + NavigationView { + ScrollView { + VStack { + ForEach(apps, id: \.self) { app in + LCAppBanner(appInfo: app) + } + } + .padding() + } + + + .navigationTitle("My Apps") + .toolbar { + ToolbarItem(placement: .topBarLeading) { + Button("Add", systemImage: "plus", action: { + + }) + } + } + + + } + } +} + +#Preview { + LCAppListView() +} diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift new file mode 100644 index 0000000..2013506 --- /dev/null +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -0,0 +1,8 @@ +// +// LCSettingsView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation diff --git a/LiveContainerSwiftUI/LCSwiftBridge.h b/LiveContainerSwiftUI/LCSwiftBridge.h new file mode 100644 index 0000000..bb9ac58 --- /dev/null +++ b/LiveContainerSwiftUI/LCSwiftBridge.h @@ -0,0 +1,16 @@ +// +// ObjcBridge.h +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/22. +// + +#ifndef ObjcBridge_h +#define ObjcBridge_h +#include +#import "LiveContainerSwiftUI-Swift.h" +#endif /* ObjcBridge_h */ + +@interface LCSwiftBridge : NSObject ++ (UIViewController * _Nonnull)getRootVC; +@end diff --git a/LiveContainerSwiftUI/LCSwiftBridge.m b/LiveContainerSwiftUI/LCSwiftBridge.m new file mode 100644 index 0000000..ecd7dff --- /dev/null +++ b/LiveContainerSwiftUI/LCSwiftBridge.m @@ -0,0 +1,18 @@ +// +// ObjcBridge.m +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/22. +// + +#import + +#import "LCSwiftBridge.h" + +@implementation LCSwiftBridge + ++ (UIViewController * _Nonnull)getRootVC { + return [LCObjcBridge getRootVC]; +} + +@end diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift new file mode 100644 index 0000000..73898ec --- /dev/null +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -0,0 +1,28 @@ +// +// TabView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation +import SwiftUI + +struct LCTabView: View { + var body: some View { + TabView { + LCAppListView() + .tabItem { + Label("Apps", systemImage: "square.stack.3d.up.fill") + } + + + } + } +} + + + +#Preview { + LCTabView() +} diff --git a/LiveContainerSwiftUI/LCTweaksView.swift b/LiveContainerSwiftUI/LCTweaksView.swift new file mode 100644 index 0000000..c86b632 --- /dev/null +++ b/LiveContainerSwiftUI/LCTweaksView.swift @@ -0,0 +1,8 @@ +// +// LCTweaksView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI-Bridging-Header.h b/LiveContainerSwiftUI/LiveContainerSwiftUI-Bridging-Header.h new file mode 100644 index 0000000..b2df131 --- /dev/null +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI-Bridging-Header.h @@ -0,0 +1,13 @@ +// +// LiveContainerSwiftUI-Bridging-Header.h.h +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// +#ifndef LiveContainerSwiftUI_Bridging_Header_h_h +#define LiveContainerSwiftUI_Bridging_Header_h_h +#include "LCAppInfo.h" +#include "LCUtils.h" + + +#endif /* LiveContainerSwiftUI_Bridging_Header_h_h */ diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj new file mode 100644 index 0000000..d071dbf --- /dev/null +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -0,0 +1,427 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 173564C92C76FE3500C6C918 /* LCAppListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564BC2C76FE3500C6C918 /* LCAppListView.swift */; }; + 173564CA2C76FE3500C6C918 /* LCTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564BD2C76FE3500C6C918 /* LCTabView.swift */; }; + 173564CB2C76FE3500C6C918 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173564BE2C76FE3500C6C918 /* Preview Assets.xcassets */; }; + 173564CC2C76FE3500C6C918 /* LCTweaksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C02C76FE3500C6C918 /* LCTweaksView.swift */; }; + 173564CD2C76FE3500C6C918 /* LCSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C12C76FE3500C6C918 /* LCSettingsView.swift */; }; + 173564CE2C76FE3500C6C918 /* Makefile in Sources */ = {isa = PBXBuildFile; fileRef = 173564C32C76FE3500C6C918 /* Makefile */; }; + 173564CF2C76FE3500C6C918 /* LCSwiftBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */; }; + 173564D02C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C52C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift */; }; + 173564D12C76FE3500C6C918 /* ObjcBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C62C76FE3500C6C918 /* ObjcBridge.swift */; }; + 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C72C76FE3500C6C918 /* LCAppBanner.swift */; }; + 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173564C82C76FE3500C6C918 /* Assets.xcassets */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 173564BC2C76FE3500C6C918 /* LCAppListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCAppListView.swift; sourceTree = ""; }; + 173564BD2C76FE3500C6C918 /* LCTabView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCTabView.swift; sourceTree = ""; }; + 173564BE2C76FE3500C6C918 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; + 173564C02C76FE3500C6C918 /* LCTweaksView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCTweaksView.swift; sourceTree = ""; }; + 173564C12C76FE3500C6C918 /* LCSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCSettingsView.swift; sourceTree = ""; }; + 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LCSwiftBridge.h; sourceTree = ""; }; + 173564C32C76FE3500C6C918 /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; + 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LCSwiftBridge.m; sourceTree = ""; }; + 173564C52C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveContainerSwiftUIApp.swift; sourceTree = ""; }; + 173564C62C76FE3500C6C918 /* ObjcBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcBridge.swift; sourceTree = ""; }; + 173564C72C76FE3500C6C918 /* LCAppBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCAppBanner.swift; sourceTree = ""; }; + 173564C82C76FE3500C6C918 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 17B9B88D2C760678009D079E /* LiveContainerSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveContainerSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 17B9B88A2C760678009D079E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 173564BB2C76FE1500C6C918 /* LiveContainerSwiftUI */ = { + isa = PBXGroup; + children = ( + 173564C82C76FE3500C6C918 /* Assets.xcassets */, + 173564C72C76FE3500C6C918 /* LCAppBanner.swift */, + 173564BC2C76FE3500C6C918 /* LCAppListView.swift */, + 173564C12C76FE3500C6C918 /* LCSettingsView.swift */, + 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */, + 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */, + 173564BD2C76FE3500C6C918 /* LCTabView.swift */, + 173564C02C76FE3500C6C918 /* LCTweaksView.swift */, + 173564C52C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift */, + 173564C32C76FE3500C6C918 /* Makefile */, + 173564C62C76FE3500C6C918 /* ObjcBridge.swift */, + 173564BF2C76FE3500C6C918 /* Preview Content */, + ); + name = LiveContainerSwiftUI; + sourceTree = ""; + }; + 173564BF2C76FE3500C6C918 /* Preview Content */ = { + isa = PBXGroup; + children = ( + 173564BE2C76FE3500C6C918 /* Preview Assets.xcassets */, + ); + path = "Preview Content"; + sourceTree = ""; + }; + 17B9B8842C760678009D079E = { + isa = PBXGroup; + children = ( + 173564BB2C76FE1500C6C918 /* LiveContainerSwiftUI */, + 17B9B88E2C760678009D079E /* Products */, + ); + sourceTree = ""; + }; + 17B9B88E2C760678009D079E /* Products */ = { + isa = PBXGroup; + children = ( + 17B9B88D2C760678009D079E /* LiveContainerSwiftUI.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 17B9B88C2C760678009D079E /* LiveContainerSwiftUI */ = { + isa = PBXNativeTarget; + buildConfigurationList = 17B9B89B2C760679009D079E /* Build configuration list for PBXNativeTarget "LiveContainerSwiftUI" */; + buildPhases = ( + 17B9B8892C760678009D079E /* Sources */, + 17B9B88A2C760678009D079E /* Frameworks */, + 17B9B88B2C760678009D079E /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = LiveContainerSwiftUI; + productName = LiveContainerSwiftUI; + productReference = 17B9B88D2C760678009D079E /* LiveContainerSwiftUI.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 17B9B8852C760678009D079E /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1540; + LastUpgradeCheck = 1540; + TargetAttributes = { + 17B9B88C2C760678009D079E = { + CreatedOnToolsVersion = 15.4; + }; + }; + }; + buildConfigurationList = 17B9B8882C760678009D079E /* Build configuration list for PBXProject "LiveContainerSwiftUI" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + de, + he, + en_AU, + ar, + el, + ja, + uk, + es_419, + zh_CN, + es, + pt_BR, + da, + it, + sk, + pt_PT, + ms, + sv, + cs, + ko, + no, + hu, + zh_HK, + tr, + pl, + zh_TW, + en_GB, + vi, + ru, + fr_CA, + fr, + fi, + id, + nl, + th, + ro, + hr, + hi, + ca, + ); + mainGroup = 17B9B8842C760678009D079E; + productRefGroup = 17B9B88E2C760678009D079E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 17B9B88C2C760678009D079E /* LiveContainerSwiftUI */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 17B9B88B2C760678009D079E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */, + 173564CB2C76FE3500C6C918 /* Preview Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 17B9B8892C760678009D079E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */, + 173564CE2C76FE3500C6C918 /* Makefile in Sources */, + 173564CF2C76FE3500C6C918 /* LCSwiftBridge.m in Sources */, + 173564C92C76FE3500C6C918 /* LCAppListView.swift in Sources */, + 173564CC2C76FE3500C6C918 /* LCTweaksView.swift in Sources */, + 173564CD2C76FE3500C6C918 /* LCSettingsView.swift in Sources */, + 173564D02C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift in Sources */, + 173564D12C76FE3500C6C918 /* ObjcBridge.swift in Sources */, + 173564CA2C76FE3500C6C918 /* LCTabView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 17B9B8992C760679009D079E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 17B9B89A2C760679009D079E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 17.5; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 17B9B89C2C760679009D079E /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"LiveContainerSwiftUI/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/../LiveContainerUI\""; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.kdt.LiveContainerSwiftUI; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/LiveContainerSwiftUI-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 17B9B89D2C760679009D079E /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Manual; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_ASSET_PATHS = "\"LiveContainerSwiftUI/Preview Content\""; + DEVELOPMENT_TEAM = ""; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + HEADER_SEARCH_PATHS = "\"$(SRCROOT)/../LiveContainerUI\""; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.kdt.LiveContainerSwiftUI; + PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_INSTALL_OBJC_HEADER = YES; + SWIFT_OBJC_BRIDGING_HEADER = "$(SRCROOT)/LiveContainerSwiftUI-Bridging-Header.h"; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 17B9B8882C760678009D079E /* Build configuration list for PBXProject "LiveContainerSwiftUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 17B9B8992C760679009D079E /* Debug */, + 17B9B89A2C760679009D079E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 17B9B89B2C760679009D079E /* Build configuration list for PBXNativeTarget "LiveContainerSwiftUI" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 17B9B89C2C760679009D079E /* Debug */, + 17B9B89D2C760679009D079E /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 17B9B8852C760678009D079E /* Project object */; +} diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUIApp.swift b/LiveContainerSwiftUI/LiveContainerSwiftUIApp.swift new file mode 100644 index 0000000..e314a03 --- /dev/null +++ b/LiveContainerSwiftUI/LiveContainerSwiftUIApp.swift @@ -0,0 +1,16 @@ +// +// LiveContainerSwiftUIApp.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import SwiftUI + +struct LiveContainerSwiftUIApp: App { + var body: some Scene { + WindowGroup { + LCTabView() + } + } +} diff --git a/LiveContainerSwiftUI/Makefile b/LiveContainerSwiftUI/Makefile new file mode 100644 index 0000000..247a00a --- /dev/null +++ b/LiveContainerSwiftUI/Makefile @@ -0,0 +1,21 @@ +TARGET := iphone:clang:latest:14.0 + +include $(THEOS)/makefiles/common.mk + +FRAMEWORK_NAME = LiveContainerSwiftUI + +LiveContainerSwiftUI_FILES = \ +$(shell find ./ -name '*.swift') \ +./LCSwiftBridge.m \ +../LiveContainerUI/LCAppInfo.m \ +../LiveContainerUI/LCUtils.m \ +../LiveContainerUI/LCMachOUtils.m +LiveContainerSwiftUI_SWIFTFLAGS = -I../LiveContainerUI/ +LiveContainerSwiftUI_CFLAGS = \ + -fobjc-arc +LiveContainerSwiftUI_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks + +include $(THEOS_MAKE_PATH)/framework.mk + +all:: + @/Applications/Xcode.app/Contents/Developer/usr/bin/actool Assets.xcassets --compile ../Resources --platform iphoneos --minimum-deployment-target 14.0 diff --git a/LiveContainerSwiftUI/ObjcBridge.swift b/LiveContainerSwiftUI/ObjcBridge.swift new file mode 100644 index 0000000..2f667e7 --- /dev/null +++ b/LiveContainerSwiftUI/ObjcBridge.swift @@ -0,0 +1,18 @@ +// +// text.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation +import SwiftUI + + +@objc public class LCObjcBridge: NSObject { + @objc public static func getRootVC() -> UIViewController { + let rootView = LCTabView() + let rootVC = UIHostingController(rootView: rootView) + return rootVC + } +} diff --git a/LiveContainerSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json b/LiveContainerSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/LiveContainerSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Makefile b/Makefile index 55e5bb4..4612321 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ export CONFIG_COMMIT = $(shell git log --oneline | sed '2,10000000d' | cut -b 1- # Build the app APPLICATION_NAME = LiveContainer -$(APPLICATION_NAME)_FILES = dyld_bypass_validation.m main.m utils.m fishhook/fishhook.c LCSharedUtils.m +$(APPLICATION_NAME)_FILES = dyld_bypass_validation.m main.m utils.m fishhook/fishhook.c LCSharedUtils.m LCAppDelegateSwiftUI.m $(APPLICATION_NAME)_CODESIGN_FLAGS = -Sentitlements.xml $(APPLICATION_NAME)_CFLAGS = -fobjc-arc $(APPLICATION_NAME)_LDFLAGS = -e_LiveContainerMain -rpath @loader_path/Frameworks @@ -19,7 +19,7 @@ $(APPLICATION_NAME)_FRAMEWORKS = UIKit include $(THEOS_MAKE_PATH)/application.mk -SUBPROJECTS += LiveContainerUI TweakLoader TestJITLess +SUBPROJECTS += TweakLoader TestJITLess LiveContainerSwiftUI include $(THEOS_MAKE_PATH)/aggregate.mk # Make the executable name longer so we have space to overwrite it with the guest app's name diff --git a/main.m b/main.m index 3705399..d0ac9ae 100644 --- a/main.m +++ b/main.m @@ -309,6 +309,7 @@ int LiveContainerMain(int argc, char *argv[]) { lcUserDefaults = NSUserDefaults.standardUserDefaults; NSString *selectedApp = [lcUserDefaults stringForKey:@"selected"]; + NSLog(@"[NMSL]: selectedApp = %@", selectedApp); if (selectedApp) { NSString *launchUrl = [lcUserDefaults stringForKey:@"launchAppUrlScheme"]; [lcUserDefaults removeObjectForKey:@"selected"]; @@ -337,13 +338,13 @@ int LiveContainerMain(int argc, char *argv[]) { } } - void *LiveContainerUIHandle = dlopen("@executable_path/Frameworks/LiveContainerUI.framework/LiveContainerUI", RTLD_LAZY); + void *LiveContainerUIHandle = dlopen("@executable_path/Frameworks/LiveContainerSwiftUI.framework/LiveContainerSwiftUI", RTLD_LAZY); assert(LiveContainerUIHandle); @autoreleasepool { if ([lcUserDefaults boolForKey:@"LCLoadTweaksToSelf"]) { dlopen("@executable_path/Frameworks/TweakLoader.dylib", RTLD_LAZY); } - return UIApplicationMain(argc, argv, nil, @"LCAppDelegate"); + return UIApplicationMain(argc, argv, nil, @"LCAppDelegateSwiftUI"); } } From 33089d313908575afbadbf957506d1b8bb905f6d Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sat, 24 Aug 2024 00:32:00 +0800 Subject: [PATCH 02/36] install, uninstall, run --- LiveContainerSwiftUI/LCAppBanner.swift | 86 +++++- LiveContainerSwiftUI/LCAppListView.swift | 262 +++++++++++++++++- .../LiveContainerSwiftUI-Bridging-Header.h | 2 +- .../project.pbxproj | 10 +- .../xcschemes/LiveContainerSwiftUI.xcscheme | 78 ++++++ LiveContainerSwiftUI/Makefile | 8 +- LiveContainerSwiftUI/Resources/Info.plist | Bin 0 -> 450 bytes LiveContainerSwiftUI/Shared.swift | 12 + LiveContainerUI/LCAppInfo.h | 10 + LiveContainerUI/LCAppInfo.m | 110 ++++++++ 10 files changed, 554 insertions(+), 24 deletions(-) create mode 100644 LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/xcshareddata/xcschemes/LiveContainerSwiftUI.xcscheme create mode 100644 LiveContainerSwiftUI/Resources/Info.plist create mode 100644 LiveContainerSwiftUI/Shared.swift diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index c05112b..5936474 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -7,10 +7,25 @@ import Foundation import SwiftUI +import UniformTypeIdentifiers + +protocol LCAppBannerDelegate { + func removeApp(app: LCAppInfo) + func getDocPath() -> URL +} struct LCAppBanner : View { - var appInfo: LCAppInfo + @State var appInfo: LCAppInfo + @State private var confirmAppRemovalShow = false + @State private var confirmAppFolderRemovalShow = false + var delegate: LCAppBannerDelegate + @State var confirmAppRemoval = false + @State var confirmAppFolderRemoval = false + @State var appRemovalSemaphore = DispatchSemaphore(value: 0) + @State var appFolderRemovalSemaphore = DispatchSemaphore(value: 0) + @State var errorShow = false + @State var errorInfo = "" var body: some View { @@ -41,11 +56,13 @@ struct LCAppBanner : View { .padding() .frame(height: 88) .background(RoundedRectangle(cornerSize: CGSize(width:22, height: 22)).fill(Color("AppBannerBG"))) + + .contextMenu { - Button { - // Add this item to a list of favorites. + Button(role: .destructive) { + uninstall() } label: { - Label("Add to Favorites", systemImage: "heart") + Label("Uninstall", systemImage: "trash") } Button { // Open Maps and center it on this item. @@ -53,6 +70,36 @@ struct LCAppBanner : View { Label("Show in Maps", systemImage: "mappin") } } + + + .alert("Confirm Uninstallation", isPresented: $confirmAppRemovalShow) { + Button(role: .destructive) { + self.confirmAppRemoval = true + self.appRemovalSemaphore.signal() + } label: { + Text("Uninstall") + } + Button("Cancel", role: .cancel) { + self.confirmAppRemoval = true + self.appRemovalSemaphore.signal() + } + } message: { + Text("Are you sure you want to uninstall \(appInfo.displayName()!)?") + } + .alert("Delete Data Folder", isPresented: $confirmAppFolderRemovalShow) { + Button(role: .destructive) { + self.confirmAppFolderRemoval = true + self.appFolderRemovalSemaphore.signal() + } label: { + Text("Delete") + } + Button("Cancel", role: .cancel) { + self.confirmAppFolderRemoval = true + self.appFolderRemovalSemaphore.signal() + } + } message: { + Text("Do you also want to delete data folder of \(appInfo.displayName()!)? You can keep it for future use.") + } } @@ -61,4 +108,35 @@ struct LCAppBanner : View { UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") LCUtils.launchToGuestApp() } + + func uninstall() { + DispatchQueue.global().async { + do { + self.confirmAppRemovalShow = true; + self.appRemovalSemaphore.wait() + if !self.confirmAppRemoval { + return + } + + self.confirmAppFolderRemovalShow = true; + self.appFolderRemovalSemaphore.wait() + + let fm = FileManager() + try fm.removeItem(atPath: self.appInfo.bundlePath()!) + self.delegate.removeApp(app: self.appInfo) + if self.confirmAppFolderRemoval { + let fm = FileManager() + let dataFolderPath = self.delegate.getDocPath().appendingPathComponent("Data/Application").appendingPathComponent(appInfo.dataUUID()!) + try fm.removeItem(at: dataFolderPath) + } + + } catch { + errorShow = true + errorInfo = error.localizedDescription + + } + } + + + } } diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index fc037d8..385ccd6 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -6,45 +6,109 @@ // import SwiftUI +import UniformTypeIdentifiers -struct LCAppListView : View { - var docPath: URL +struct AppReplaceOption : Hashable, Codable { + var isReplace: Bool + var nameOfFolderToInstall: String +} + +class ProgressObserver : NSObject { + var delegate : (_ fraction: Double) -> Void; + + init(delegate: @escaping (_: Double) -> Void) { + self.delegate = delegate + } + + override func observeValue(forKeyPath keyPath: String?, + of object: Any?, + change: [NSKeyValueChangeKey : Any]?, + context: UnsafeMutableRawPointer?) { + + if let theKeyPath = keyPath { + if theKeyPath == "fractionCompleted" { + let progress = object as! Progress + self.delegate(progress.fractionCompleted) + } + } + + + } +} + +struct LCAppListView : View, LCAppBannerDelegate { + private var docPath: URL var bundlePath: URL - var apps: [LCAppInfo] + @State var apps: [LCAppInfo] +// @State var aaa: [String] + + // ipa choosing stuff + @State var choosingIPA = false + @State var installErrorShow = false + @State var installErrorInfo = "" + + // ipa installing stuff + @State var installprogressVisible = false + @State var installProgress: Progress + @State var installProgressPercentage = 0.0 + @State var installReplaceComfirmVisible = false + @State var installOptions: [AppReplaceOption] + @State var installOptionChosen: AppReplaceOption? + @State var installOptionSemaphore = DispatchSemaphore(value: 0) + + init() { - NSLog("[NMSL] App list init!") let fm = FileManager() self.docPath = fm.urls(for: .documentDirectory, in: .userDomainMask).last! self.bundlePath = self.docPath.appendingPathComponent("Applications") + _installProgress = State(initialValue: Progress.discreteProgress(totalUnitCount: 100)) + _installOptions = State(initialValue: []) + _installOptionChosen = State(initialValue: nil) + + var tempApps: [LCAppInfo] = [] + do { try fm.createDirectory(at: self.bundlePath, withIntermediateDirectories: true) let appDirs = try fm.contentsOfDirectory(atPath: self.bundlePath.path) - self.apps = [] for appDir in appDirs { if !appDir.hasSuffix(".app") { continue } - var newApp = LCAppInfo(bundlePath: "\(self.bundlePath.path)/\(appDir)")! + let newApp = LCAppInfo(bundlePath: "\(self.bundlePath.path)/\(appDir)")! newApp.relativeBundlePath = appDir - self.apps.append(newApp) + tempApps.append(newApp) } } catch { - self.apps = [] NSLog("[NMSL] error:\(error)") } - + _apps = State(initialValue: tempApps) } - + var body: some View { NavigationView { + ScrollView { - VStack { - ForEach(apps, id: \.self) { app in - LCAppBanner(appInfo: app) + LazyVStack(pinnedViews:[.sectionHeaders]) { + Section { + LazyVStack { + ForEach(apps, id: \.self) { app in + LCAppBanner(appInfo: app, delegate: self) + } + .transition(.scale) + } + .padding() + } header: { + GeometryReader{ g in + ProgressView(value: installProgressPercentage) + .labelsHidden() + .opacity(installprogressVisible ? 1 : 0) + } } + } - .padding() + .animation(.easeInOut, value: apps) + } @@ -52,6 +116,15 @@ struct LCAppListView : View { .toolbar { ToolbarItem(placement: .topBarLeading) { Button("Add", systemImage: "plus", action: { + if choosingIPA { + choosingIPA = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + choosingIPA = true + }) + } else { + choosingIPA = true + } + }) } @@ -59,7 +132,168 @@ struct LCAppListView : View { } + + .alert(isPresented: $installErrorShow){ + Alert(title: Text("Installation Failed"), message: Text(installErrorInfo)) + } + .fileImporter(isPresented: $choosingIPA, allowedContentTypes: [UTType(filenameExtension: "ipa")!]) { result in + startInstallApp(result) + + } + .alert("Installation", isPresented: $installReplaceComfirmVisible) { + ForEach(installOptions, id: \.self) { installOption in + Button(role: installOption.isReplace ? .destructive : nil, action: { + self.installOptionChosen = installOption + self.installOptionSemaphore.signal() + }, label: { + Text(installOption.isReplace ? installOption.nameOfFolderToInstall : "Install as new") + }) + + } + Button(role: .cancel, action: { + self.installOptionChosen = nil + self.installOptionSemaphore.signal() + }, label: { + Text("Abort Installation") + }) + } message: { + Text("There is an existing application with the same bundle folder name. Replace one or install as new.") + } + + } + + + + func startInstallApp(_ result:Result) { + DispatchQueue.global().async { + do { + let fileUrl = try result.get() + self.installprogressVisible = true + try installIpaFile(fileUrl) + } catch { + installErrorInfo = error.localizedDescription + installErrorShow = true + self.installprogressVisible = false + } + } + + } + + func onInstallProgress(_ fraction : Double) { + self.installProgressPercentage = fraction + } + + + func installIpaFile(_ url:URL) throws { + if(!url.startAccessingSecurityScopedResource()) { + throw "Failed to access IPA"; + } + let fm = FileManager() + + self.installProgress = Progress.discreteProgress(totalUnitCount: 100) + self.installProgressPercentage = 0.0 + let progressObserver = ProgressObserver(delegate: onInstallProgress) + self.installProgress.addObserver(progressObserver, forKeyPath: "fractionCompleted", context: nil) + let decompressProgress = Progress.discreteProgress(totalUnitCount: 100) + self.installProgress.addChild(decompressProgress, withPendingUnitCount: 80) + + // decompress + extract(url.path, fm.temporaryDirectory.path, decompressProgress) + url.stopAccessingSecurityScopedResource() + + let payloadPath = fm.temporaryDirectory.appendingPathComponent("Payload") + let payloadContents = try fm.contentsOfDirectory(atPath: payloadPath.path) + if payloadContents.count < 1 || !payloadContents[0].hasSuffix(".app") { + throw "App bundle not found" + } + let appBundleName = payloadContents[0] + let appFolderPath = payloadPath.appendingPathComponent(appBundleName) + + guard let newAppInfo = LCAppInfo(bundlePath: appFolderPath.path) else { + throw "Failed to read app's Info.plist." + } + + var appRelativePath = "\(newAppInfo.bundleIdentifier()!).app" + var outputFolder = self.bundlePath.appendingPathComponent(appRelativePath) + // Folder exist! show alert for user to choose which bundle to replace + if fm.fileExists(atPath: outputFolder.path) { + let appFolders = try fm.contentsOfDirectory(atPath: self.bundlePath.path).filter {folderName in + return folderName.hasPrefix(newAppInfo.bundleIdentifier()) && folderName.hasSuffix(".app") + } + appRelativePath = "\(newAppInfo.bundleIdentifier()!)_\(CFAbsoluteTimeGetCurrent()).app" + + self.installOptions = [AppReplaceOption(isReplace: false, nameOfFolderToInstall: appRelativePath)] + + for appFolder in appFolders { + self.installOptions.append(AppReplaceOption(isReplace: true, nameOfFolderToInstall: appFolder)) + } + self.installReplaceComfirmVisible = true + self.installOptionSemaphore.wait() + + // user cancelled + guard let installOptionChosen = self.installOptionChosen else { + self.installprogressVisible = false + try fm.removeItem(at: payloadPath) + return + } + + outputFolder = self.bundlePath.appendingPathComponent(installOptionChosen.nameOfFolderToInstall) + if installOptionChosen.isReplace { + try fm.removeItem(at: outputFolder) + self.apps.removeAll { appNow in + return appNow.relativeBundlePath == installOptionChosen.nameOfFolderToInstall + } + } + } + // Move it! + try fm.moveItem(at: appFolderPath, to: outputFolder) + let finalNewApp = LCAppInfo(bundlePath: outputFolder.path) + finalNewApp?.relativeBundlePath = appRelativePath + + // patch it + let patchResult = finalNewApp?.patchExec() + if patchResult != nil && patchResult != "SignNeeded" { + throw patchResult!; + } + if patchResult == "SignNeeded" { + // sign it + let signSemaphore = DispatchSemaphore(value: 0) + var error : Error? = nil + var success = false + let signProgress = LCUtils.signAppBundle(outputFolder) { success1, error1 in + error = error1 + success = success1 + signSemaphore.signal() + } + self.installProgress.addChild(signProgress!, withPendingUnitCount: 20) + signSemaphore.wait() + + if let error = error { + finalNewApp?.signCleanUp(withSuccessStatus: false) + throw error + } + finalNewApp?.signCleanUp(withSuccessStatus: success) + if !success { + throw "Unknow error occurred" + } + + + } + + self.apps.append(finalNewApp!) + self.installprogressVisible = false + } + + func removeApp(app: LCAppInfo) { + self.apps.removeAll { now in + return app == now + } } + + func getDocPath() -> URL { + return self.docPath + } + } #Preview { diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI-Bridging-Header.h b/LiveContainerSwiftUI/LiveContainerSwiftUI-Bridging-Header.h index b2df131..4e9c47c 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI-Bridging-Header.h +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI-Bridging-Header.h @@ -8,6 +8,6 @@ #define LiveContainerSwiftUI_Bridging_Header_h_h #include "LCAppInfo.h" #include "LCUtils.h" - +#include "unarchive.h" #endif /* LiveContainerSwiftUI_Bridging_Header_h_h */ diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj index d071dbf..891068b 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 173564D12C76FE3500C6C918 /* ObjcBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C62C76FE3500C6C918 /* ObjcBridge.swift */; }; 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C72C76FE3500C6C918 /* LCAppBanner.swift */; }; 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173564C82C76FE3500C6C918 /* Assets.xcassets */; }; + 178B4C3E2C77654400DD1F74 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178B4C3D2C77654400DD1F74 /* Shared.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -33,6 +34,8 @@ 173564C62C76FE3500C6C918 /* ObjcBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcBridge.swift; sourceTree = ""; }; 173564C72C76FE3500C6C918 /* LCAppBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCAppBanner.swift; sourceTree = ""; }; 173564C82C76FE3500C6C918 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 178B4C3D2C77654400DD1F74 /* Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; + 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LiveContainerSwiftUI-Bridging-Header.h"; sourceTree = ""; }; 17B9B88D2C760678009D079E /* LiveContainerSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveContainerSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -61,7 +64,9 @@ 173564C52C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift */, 173564C32C76FE3500C6C918 /* Makefile */, 173564C62C76FE3500C6C918 /* ObjcBridge.swift */, + 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */, 173564BF2C76FE3500C6C918 /* Preview Content */, + 178B4C3D2C77654400DD1F74 /* Shared.swift */, ); name = LiveContainerSwiftUI; sourceTree = ""; @@ -198,6 +203,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 178B4C3E2C77654400DD1F74 /* Shared.swift in Sources */, 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */, 173564CE2C76FE3500C6C918 /* Makefile in Sources */, 173564CF2C76FE3500C6C918 /* LCSwiftBridge.m in Sources */, @@ -349,7 +355,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -383,7 +389,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - IPHONEOS_DEPLOYMENT_TARGET = 14.0; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/xcshareddata/xcschemes/LiveContainerSwiftUI.xcscheme b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/xcshareddata/xcschemes/LiveContainerSwiftUI.xcscheme new file mode 100644 index 0000000..097865d --- /dev/null +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/xcshareddata/xcschemes/LiveContainerSwiftUI.xcscheme @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LiveContainerSwiftUI/Makefile b/LiveContainerSwiftUI/Makefile index 247a00a..9b3ece0 100644 --- a/LiveContainerSwiftUI/Makefile +++ b/LiveContainerSwiftUI/Makefile @@ -1,4 +1,4 @@ -TARGET := iphone:clang:latest:14.0 +TARGET := iphone:clang:latest:15.0 include $(THEOS)/makefiles/common.mk @@ -9,10 +9,12 @@ $(shell find ./ -name '*.swift') \ ./LCSwiftBridge.m \ ../LiveContainerUI/LCAppInfo.m \ ../LiveContainerUI/LCUtils.m \ -../LiveContainerUI/LCMachOUtils.m -LiveContainerSwiftUI_SWIFTFLAGS = -I../LiveContainerUI/ +../LiveContainerUI/LCMachOUtils.m \ +../LiveContainerUI/unarchive.m +LiveContainerSwiftUI_SWIFTFLAGS = -I../LiveContainerUI LiveContainerSwiftUI_CFLAGS = \ -fobjc-arc +LiveContainerSwiftUI_LIBRARIES = archive LiveContainerSwiftUI_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks include $(THEOS_MAKE_PATH)/framework.mk diff --git a/LiveContainerSwiftUI/Resources/Info.plist b/LiveContainerSwiftUI/Resources/Info.plist new file mode 100644 index 0000000000000000000000000000000000000000..905a258424ddf5bae46ab606015bfc438b842ae3 GIT binary patch literal 450 zcmZvXOHRWu5QgmnrIbhA2S_0G0xY6*!=f7mEtLwCmZm%wl_EEpq*fC>RNuiuuPbJ4!Mhx};*7T_3n&0RuTU=!(YRlCpU^EaX&WQnR{YT^@)w^OSa^aSl}V_nXXWe-6I{C}j= zySsR<-Lt_|aOgYN4OBpN)I@zWMhSXCFX$D$p%0wLMSO@)a0j2`9`-TAGpx{e #import +@interface SignTmpStatus : NSObject +@property NSUInteger newSignId; +@property NSString *tmpExecPath; +@property NSString *infoPath; + +@end + @interface LCAppInfo : NSObject { NSMutableDictionary* _info; NSString* _bundlePath; @@ -20,4 +27,7 @@ - (instancetype)initWithBundlePath:(NSString*)bundlePath; - (NSDictionary *)generateWebClipConfig; - (void)save; +@property SignTmpStatus* _signStatus; +- (NSString*)patchExec; +- (void) signCleanUpWithSuccessStatus:(BOOL)isSignSuccess; @end diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index cd6f85b..3df2193 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -1,6 +1,12 @@ +@import CommonCrypto; + #import #import #import "LCAppInfo.h" +#import "LCUtils.h" + +@implementation SignTmpStatus +@end @implementation LCAppInfo - (instancetype)initWithBundlePath:(NSString*)bundlePath { @@ -152,4 +158,108 @@ - (NSDictionary *)generateWebClipConfig { - (void)save { [_info writeToFile:[NSString stringWithFormat:@"%@/Info.plist", _bundlePath] atomically:YES]; } + +- (void)preprocessBundleBeforeSiging:(NSURL *)bundleURL completion:(dispatch_block_t)completion { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Remove faulty file + [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"LiveContainer"] error:nil]; + // Remove PlugIns folder + [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"PlugIns"] error:nil]; + // Remove code signature from all library files + [LCUtils removeCodeSignatureFromBundleURL:bundleURL]; + dispatch_async(dispatch_get_main_queue(), completion); + }); +} + +// return "SignNeeded" if sign is needed, other wise return an error +- (NSString*)patchExec { + NSString *appPath = self.bundlePath; + NSString *infoPath = [NSString stringWithFormat:@"%@/Info.plist", appPath]; + NSMutableDictionary *info = [NSMutableDictionary dictionaryWithContentsOfFile:infoPath]; + if (!info) { + return @"Info.plist not found"; + } + + // Update patch + int currentPatchRev = 5; + if ([info[@"LCPatchRevision"] intValue] < currentPatchRev) { + NSString *execPath = [NSString stringWithFormat:@"%@/%@", appPath, info[@"CFBundleExecutable"]]; + NSString *error = LCParseMachO(execPath.UTF8String, ^(const char *path, struct mach_header_64 *header) { + LCPatchExecSlice(path, header); + }); + if (error) { + return error; + } + info[@"LCPatchRevision"] = @(currentPatchRev); + [info writeToFile:infoPath atomically:YES]; + } + + if (!LCUtils.certificatePassword) { + return nil; + } + + int signRevision = 1; + + // We're only getting the first 8 bytes for comparison + NSUInteger signID; + if (LCUtils.certificateData) { + uint8_t digest[CC_SHA1_DIGEST_LENGTH]; + CC_SHA1(LCUtils.certificateData.bytes, (CC_LONG)LCUtils.certificateData.length, digest); + signID = *(uint64_t *)digest + signRevision; + } else { + return @"Failed to find ALTCertificate.p12. Please refresh your store and try again."; + } + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + + // Sign app if JIT-less is set up + if ([info[@"LCJITLessSignID"] unsignedLongValue] != signID) { + NSURL *appPathURL = [NSURL fileURLWithPath:appPath]; + [self preprocessBundleBeforeSiging:appPathURL completion:^{ + dispatch_semaphore_signal(sema); + }]; + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + + // We need to temporarily fake bundle ID and main executable to sign properly + NSString *tmpExecPath = [appPath stringByAppendingPathComponent:@"LiveContainer.tmp"]; + if (!info[@"LCBundleIdentifier"]) { + // Don't let main executable get entitlements + [NSFileManager.defaultManager copyItemAtPath:NSBundle.mainBundle.executablePath toPath:tmpExecPath error:nil]; + + info[@"LCBundleExecutable"] = info[@"CFBundleExecutable"]; + info[@"LCBundleIdentifier"] = info[@"CFBundleIdentifier"]; + info[@"CFBundleExecutable"] = tmpExecPath.lastPathComponent; + info[@"CFBundleIdentifier"] = NSBundle.mainBundle.bundleIdentifier; + [info writeToFile:infoPath atomically:YES]; + } + info[@"CFBundleExecutable"] = info[@"LCBundleExecutable"]; + info[@"CFBundleIdentifier"] = info[@"LCBundleIdentifier"]; + [info removeObjectForKey:@"LCBundleExecutable"]; + [info removeObjectForKey:@"LCBundleIdentifier"]; + self._signStatus = [[SignTmpStatus alloc] init]; + self._signStatus.newSignId = signID; + self._signStatus.tmpExecPath = tmpExecPath; + self._signStatus.infoPath = infoPath; + + return @"SignNeeded"; + + } + return nil; +} + +- (void) signCleanUpWithSuccessStatus:(BOOL)isSignSuccess { + if(self._signStatus == nil) { + return; + } + if (isSignSuccess) { + _info[@"LCJITLessSignID"] = @(self._signStatus.newSignId); + } + + // Remove fake main executable + [NSFileManager.defaultManager removeItemAtPath:self._signStatus.tmpExecPath error:nil]; + + // Save sign ID and restore bundle ID + [_info writeToFile:self._signStatus.infoPath atomically:YES]; + self._signStatus = nil; + return; +} @end From 690d45f18b4571873b1cd4efe10581f5f29cf754 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sat, 24 Aug 2024 12:13:34 +0800 Subject: [PATCH 03/36] Create folder until app launch --- LiveContainerSwiftUI/LCAppBanner.swift | 16 ++++++++++------ LiveContainerSwiftUI/LCAppListView.swift | 24 +++++++++++++++--------- LiveContainerUI/LCAppInfo.h | 1 + LiveContainerUI/LCAppInfo.m | 4 ++++ main.m | 11 ++++++++++- 5 files changed, 40 insertions(+), 16 deletions(-) diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 5936474..5f8b645 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -39,7 +39,7 @@ struct LCAppBanner : View { VStack (alignment: .leading, content: { Text(appInfo.displayName()).font(.system(size: 16)).bold() Text("\(appInfo.version()) - \(appInfo.bundleIdentifier())").font(.system(size: 12)).foregroundColor(Color("FontColor")) - Text(appInfo.dataUUID()).font(.system(size: 8)).foregroundColor(Color("FontColor")) + Text(appInfo.getDataUUIDNoAssign() == nil ? "Data folder not created yet" : appInfo.getDataUUIDNoAssign()).font(.system(size: 8)).foregroundColor(Color("FontColor")) }) } Spacer() @@ -58,7 +58,8 @@ struct LCAppBanner : View { .background(RoundedRectangle(cornerSize: CGSize(width:22, height: 22)).fill(Color("AppBannerBG"))) - .contextMenu { + .contextMenu{ + Text(appInfo.relativeBundlePath) Button(role: .destructive) { uninstall() } label: { @@ -71,7 +72,6 @@ struct LCAppBanner : View { } } - .alert("Confirm Uninstallation", isPresented: $confirmAppRemovalShow) { Button(role: .destructive) { self.confirmAppRemoval = true @@ -117,9 +117,13 @@ struct LCAppBanner : View { if !self.confirmAppRemoval { return } - - self.confirmAppFolderRemovalShow = true; - self.appFolderRemovalSemaphore.wait() + if self.appInfo.getDataUUIDNoAssign() != nil { + self.confirmAppFolderRemovalShow = true; + self.appFolderRemovalSemaphore.wait() + } else { + self.confirmAppFolderRemoval = false; + } + let fm = FileManager() try fm.removeItem(atPath: self.appInfo.bundlePath()!) diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 385ccd6..78518c7 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -8,9 +8,10 @@ import SwiftUI import UniformTypeIdentifiers -struct AppReplaceOption : Hashable, Codable { +struct AppReplaceOption : Hashable { var isReplace: Bool var nameOfFolderToInstall: String + var appToReplace: LCAppInfo? } class ProgressObserver : NSObject { @@ -157,7 +158,7 @@ struct LCAppListView : View, LCAppBannerDelegate { Text("Abort Installation") }) } message: { - Text("There is an existing application with the same bundle folder name. Replace one or install as new.") + Text("There is an existing application with the same bundle identifier. Replace one or install as new.") } } @@ -215,17 +216,18 @@ struct LCAppListView : View, LCAppBannerDelegate { var appRelativePath = "\(newAppInfo.bundleIdentifier()!).app" var outputFolder = self.bundlePath.appendingPathComponent(appRelativePath) + var appToReplace : LCAppInfo? = nil // Folder exist! show alert for user to choose which bundle to replace - if fm.fileExists(atPath: outputFolder.path) { - let appFolders = try fm.contentsOfDirectory(atPath: self.bundlePath.path).filter {folderName in - return folderName.hasPrefix(newAppInfo.bundleIdentifier()) && folderName.hasSuffix(".app") - } + let sameBundleIdApp = self.apps.filter { app in + return app.bundleIdentifier()! == newAppInfo.bundleIdentifier() + } + if fm.fileExists(atPath: outputFolder.path) || sameBundleIdApp.count > 0 { appRelativePath = "\(newAppInfo.bundleIdentifier()!)_\(CFAbsoluteTimeGetCurrent()).app" self.installOptions = [AppReplaceOption(isReplace: false, nameOfFolderToInstall: appRelativePath)] - for appFolder in appFolders { - self.installOptions.append(AppReplaceOption(isReplace: true, nameOfFolderToInstall: appFolder)) + for app in sameBundleIdApp { + self.installOptions.append(AppReplaceOption(isReplace: true, nameOfFolderToInstall: app.relativeBundlePath, appToReplace: app)) } self.installReplaceComfirmVisible = true self.installOptionSemaphore.wait() @@ -238,6 +240,7 @@ struct LCAppListView : View, LCAppBannerDelegate { } outputFolder = self.bundlePath.appendingPathComponent(installOptionChosen.nameOfFolderToInstall) + appToReplace = installOptionChosen.appToReplace if installOptionChosen.isReplace { try fm.removeItem(at: outputFolder) self.apps.removeAll { appNow in @@ -279,7 +282,10 @@ struct LCAppListView : View, LCAppBannerDelegate { } - + // set data folder to the folder of the chosen app + if let appToReplace = appToReplace { + finalNewApp?.setDataUUID(appToReplace.getDataUUIDNoAssign()) + } self.apps.append(finalNewApp!) self.installprogressVisible = false } diff --git a/LiveContainerUI/LCAppInfo.h b/LiveContainerUI/LCAppInfo.h index 82a62d8..86018ad 100644 --- a/LiveContainerUI/LCAppInfo.h +++ b/LiveContainerUI/LCAppInfo.h @@ -20,6 +20,7 @@ - (NSString*)bundleIdentifier; - (NSString*)version; - (NSString*)dataUUID; +- (NSString*)getDataUUIDNoAssign; - (NSString*)tweakFolder; - (NSMutableArray*) urlSchemes; - (void)setDataUUID:(NSString *)uuid; diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index 3df2193..047d4b7 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -74,6 +74,10 @@ - (NSString*)dataUUID { return _info[@"LCDataUUID"]; } +- (NSString*)getDataUUIDNoAssign { + return _info[@"LCDataUUID"]; +} + - (NSString*)tweakFolder { return _info[@"LCTweakFolder"]; } diff --git a/main.m b/main.m index d0ac9ae..1f6323a 100644 --- a/main.m +++ b/main.m @@ -238,9 +238,18 @@ static void overwriteExecPath(NSString *bundlePath) { [NSProcessInfo.processInfo performSelector:@selector(setArguments:) withObject:objcArgv]; NSProcessInfo.processInfo.processName = appBundle.infoDictionary[@"CFBundleExecutable"]; *_CFGetProgname() = NSProcessInfo.processInfo.processName.UTF8String; + + // Set & save the folder it it does not exist in Info.plist + NSString* dataUUID = appBundle.infoDictionary[@"LCDataUUID"]; + if(dataUUID == nil) { + NSMutableDictionary* infoDict = [NSMutableDictionary dictionaryWithContentsOfFile:[NSString stringWithFormat:@"%@/Info.plist", bundlePath]]; + dataUUID = NSUUID.UUID.UUIDString; + infoDict[@"LCDataUUID"] = dataUUID; + [infoDict writeToFile:[NSString stringWithFormat:@"%@/Info.plist", bundlePath] atomically:YES]; + } // Overwrite home and tmp path - NSString *newHomePath = [NSString stringWithFormat:@"%@/Data/Application/%@", docPath, appBundle.infoDictionary[@"LCDataUUID"]]; + NSString *newHomePath = [NSString stringWithFormat:@"%@/Data/Application/%@", docPath, dataUUID]; NSString *newTmpPath = [newHomePath stringByAppendingPathComponent:@"tmp"]; remove(newTmpPath.UTF8String); symlink(getenv("TMPDIR"), newTmpPath.UTF8String); From efa48ddcc195fe2fdeaec61669e7961a677d6ee1 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sat, 24 Aug 2024 21:27:18 +0800 Subject: [PATCH 04/36] data &tweak folder choosing --- LiveContainerSwiftUI/LCAppBanner.swift | 182 ++++++++++++++++-- LiveContainerSwiftUI/LCAppListView.swift | 48 ++++- LiveContainerSwiftUI/Shared.swift | 69 +++++++ .../LCAppDelegateSwiftUI.h | 1 + .../LCAppDelegateSwiftUI.m | 4 +- Makefile | 2 +- main.m | 1 - 7 files changed, 282 insertions(+), 25 deletions(-) rename LCAppDelegateSwiftUI.h => LiveContainerUI/LCAppDelegateSwiftUI.h (86%) rename LCAppDelegateSwiftUI.m => LiveContainerUI/LCAppDelegateSwiftUI.m (94%) diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 5f8b645..53ee66b 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -16,16 +16,41 @@ protocol LCAppBannerDelegate { struct LCAppBanner : View { @State var appInfo: LCAppInfo + var delegate: LCAppBannerDelegate + @State var appDataFolders: [String] + @State var tweakFolders: [String] + + @State private var uiDataFolder : String? + @State private var uiTweakFolder : String? + @State private var uiPickerDataFolder : String? + @State private var uiPickerTweakFolder : String? + @State private var confirmAppRemovalShow = false @State private var confirmAppFolderRemovalShow = false - var delegate: LCAppBannerDelegate - @State var confirmAppRemoval = false - @State var confirmAppFolderRemoval = false - @State var appRemovalSemaphore = DispatchSemaphore(value: 0) - @State var appFolderRemovalSemaphore = DispatchSemaphore(value: 0) - @State var errorShow = false - @State var errorInfo = "" + @State private var confirmAppRemoval = false + @State private var confirmAppFolderRemoval = false + @State private var appRemovalSemaphore = DispatchSemaphore(value: 0) + @State private var appFolderRemovalSemaphore = DispatchSemaphore(value: 0) + + @State private var renameFolderShow = false + @State private var renameFolderContent = "" + @State private var renameFolerSemaphore = DispatchSemaphore(value: 0) + + @State private var errorShow = false + @State private var errorInfo = "" + + init(appInfo: LCAppInfo, delegate: LCAppBannerDelegate, appDataFolders: [String], tweakFolders: [String]) { + _appInfo = State(initialValue: appInfo) + _appDataFolders = State(initialValue: appDataFolders) + _tweakFolders = State(initialValue: tweakFolders) + self.delegate = delegate + _uiDataFolder = State(initialValue: appInfo.getDataUUIDNoAssign()) + _uiTweakFolder = State(initialValue: appInfo.tweakFolder()) + _uiPickerDataFolder = _uiDataFolder + _uiPickerTweakFolder = _uiTweakFolder + + } var body: some View { @@ -39,7 +64,7 @@ struct LCAppBanner : View { VStack (alignment: .leading, content: { Text(appInfo.displayName()).font(.system(size: 16)).bold() Text("\(appInfo.version()) - \(appInfo.bundleIdentifier())").font(.system(size: 12)).foregroundColor(Color("FontColor")) - Text(appInfo.getDataUUIDNoAssign() == nil ? "Data folder not created yet" : appInfo.getDataUUIDNoAssign()).font(.system(size: 8)).foregroundColor(Color("FontColor")) + Text(uiDataFolder == nil ? "Data folder not created yet" : uiDataFolder!).font(.system(size: 8)).foregroundColor(Color("FontColor")) }) } Spacer() @@ -66,11 +91,56 @@ struct LCAppBanner : View { Label("Uninstall", systemImage: "trash") } Button { - // Open Maps and center it on this item. + // Add to home screen } label: { - Label("Show in Maps", systemImage: "mappin") + Label("Add to home screen", systemImage: "plus.app") } + Menu(content: { + Button { + createFolder() + } label: { + Label("New data folder", systemImage: "plus") + } + if uiDataFolder != nil { + Button { + renameDataFolder() + } label: { + Label("Rename data folder", systemImage: "pencel") + } + } + + Picker(selection: $uiPickerDataFolder , label: Text("")) { + ForEach(appDataFolders, id:\.self) { folderName in + Button(folderName) { + setDataFolder(folderName: folderName) + }.tag(Optional(folderName)) + } + } + }, label: { + Label("Change Data Folder", systemImage: "folder.badge.questionmark") + }) + + Menu(content: { + Picker(selection: $uiPickerTweakFolder , label: Text("")) { + Label("None", systemImage: "nosign").tag(Optional(nil)) + ForEach(tweakFolders, id:\.self) { folderName in + Text(folderName).tag(Optional(folderName)) + } + } + }, label: { + Label("Change Tweak Folder", systemImage: "gear") + }) } + .onChange(of: uiPickerDataFolder, perform: { newValue in + if newValue != uiDataFolder { + setDataFolder(folderName: newValue) + } + }) + .onChange(of: uiPickerTweakFolder, perform: { newValue in + if newValue != uiTweakFolder { + setTweakFolder(folderName: newValue) + } + }) .alert("Confirm Uninstallation", isPresented: $confirmAppRemovalShow) { Button(role: .destructive) { @@ -80,7 +150,7 @@ struct LCAppBanner : View { Text("Uninstall") } Button("Cancel", role: .cancel) { - self.confirmAppRemoval = true + self.confirmAppRemoval = false self.appRemovalSemaphore.signal() } } message: { @@ -94,12 +164,32 @@ struct LCAppBanner : View { Text("Delete") } Button("Cancel", role: .cancel) { - self.confirmAppFolderRemoval = true + self.confirmAppFolderRemoval = false self.appFolderRemovalSemaphore.signal() } } message: { Text("Do you also want to delete data folder of \(appInfo.displayName()!)? You can keep it for future use.") } + .textFieldAlert( + isPresented: $renameFolderShow, + title: "Enter the name of new folder", + text: $renameFolderContent, + placeholder: "", + action: { newText in + self.renameFolderContent = newText! + renameFolerSemaphore.signal() + }, + actionCancel: {_ in + self.renameFolderContent = "" + renameFolerSemaphore.signal() + } + ) + .alert("Error", isPresented: $errorShow) { + Button("OK", action: { + }) + } message: { + Text(errorInfo) + } } @@ -109,6 +199,74 @@ struct LCAppBanner : View { LCUtils.launchToGuestApp() } + func setDataFolder(folderName: String?) { + self.appInfo.setDataUUID(folderName!) + self.uiDataFolder = folderName + self.uiPickerDataFolder = folderName + } + + func createFolder() { + DispatchQueue.global().async { + self.renameFolderContent = NSUUID().uuidString + self.renameFolderShow = true + self.renameFolerSemaphore.wait() + if self.renameFolderContent == "" { + return + } + let fm = FileManager() + let dest = self.delegate.getDocPath().appendingPathComponent("Data/Application").appendingPathComponent(self.renameFolderContent) + do { + try fm.createDirectory(at: dest, withIntermediateDirectories: false) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + + self.appDataFolders.append(self.renameFolderContent) + self.setDataFolder(folderName: self.renameFolderContent) + } + } + + func renameDataFolder() { + if self.appInfo.getDataUUIDNoAssign() == nil { + return + } + + DispatchQueue.global().async { + self.renameFolderContent = self.uiDataFolder == nil ? "" : self.uiDataFolder! + self.renameFolderShow = true + self.renameFolerSemaphore.wait() + if self.renameFolderContent == "" { + return + } + let fm = FileManager() + let orig = self.delegate.getDocPath().appendingPathComponent("Data/Application").appendingPathComponent(appInfo.getDataUUIDNoAssign()) + let dest = self.delegate.getDocPath().appendingPathComponent("Data/Application").appendingPathComponent(self.renameFolderContent) + do { + try fm.moveItem(at: orig, to: dest) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + + let i = self.appDataFolders.firstIndex(of: self.appInfo.getDataUUIDNoAssign()) + guard let i = i else { + return + } + + self.appDataFolders[i] = self.renameFolderContent + self.setDataFolder(folderName: self.renameFolderContent) + } + } + + func setTweakFolder(folderName: String?) { + self.appInfo.setTweakFolder(folderName) + self.uiTweakFolder = folderName + self.uiPickerTweakFolder = folderName + } + func uninstall() { DispatchQueue.global().async { do { diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 78518c7..cc33806 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -40,8 +40,11 @@ class ProgressObserver : NSObject { struct LCAppListView : View, LCAppBannerDelegate { private var docPath: URL var bundlePath: URL + var dataPath: URL + var tweakPath: URL @State var apps: [LCAppInfo] -// @State var aaa: [String] + @State var appDataFolderNames: [String] + @State var tweakFolderNames: [String] // ipa choosing stuff @State var choosingIPA = false @@ -50,26 +53,30 @@ struct LCAppListView : View, LCAppBannerDelegate { // ipa installing stuff @State var installprogressVisible = false - @State var installProgress: Progress @State var installProgressPercentage = 0.0 @State var installReplaceComfirmVisible = false @State var installOptions: [AppReplaceOption] @State var installOptionChosen: AppReplaceOption? @State var installOptionSemaphore = DispatchSemaphore(value: 0) + init() { let fm = FileManager() self.docPath = fm.urls(for: .documentDirectory, in: .userDomainMask).last! self.bundlePath = self.docPath.appendingPathComponent("Applications") - _installProgress = State(initialValue: Progress.discreteProgress(totalUnitCount: 100)) + self.dataPath = self.docPath.appendingPathComponent("Data/Application") + self.tweakPath = self.docPath.appendingPathComponent("Tweaks") _installOptions = State(initialValue: []) _installOptionChosen = State(initialValue: nil) + var tempAppDataFolderNames : [String] = [] + var tempTweakFolderNames : [String] = [] var tempApps: [LCAppInfo] = [] do { + // load apps try fm.createDirectory(at: self.bundlePath, withIntermediateDirectories: true) let appDirs = try fm.contentsOfDirectory(atPath: self.bundlePath.path) for appDir in appDirs { @@ -80,10 +87,33 @@ struct LCAppListView : View, LCAppBannerDelegate { newApp.relativeBundlePath = appDir tempApps.append(newApp) } + // load document folders + try fm.createDirectory(at: self.dataPath, withIntermediateDirectories: true) + let dataDirs = try fm.contentsOfDirectory(atPath: self.dataPath.path) + for dataDir in dataDirs { + let dataDirUrl = self.dataPath.appendingPathComponent(dataDir) + if !dataDirUrl.hasDirectoryPath { + continue + } + tempAppDataFolderNames.append(dataDir) + } + + // load tweak folders + try fm.createDirectory(at: self.tweakPath, withIntermediateDirectories: true) + let tweakDirs = try fm.contentsOfDirectory(atPath: self.tweakPath.path) + for tweakDir in tweakDirs { + let tweakDirUrl = self.tweakPath.appendingPathComponent(tweakDir) + if !tweakDirUrl.hasDirectoryPath { + continue + } + tempTweakFolderNames.append(tweakDir) + } } catch { - NSLog("[NMSL] error:\(error)") + NSLog("[LC] error:\(error)") } _apps = State(initialValue: tempApps) + _appDataFolderNames = State(initialValue: tempAppDataFolderNames) + _tweakFolderNames = State(initialValue: tempTweakFolderNames) } var body: some View { @@ -94,7 +124,7 @@ struct LCAppListView : View, LCAppBannerDelegate { Section { LazyVStack { ForEach(apps, id: \.self) { app in - LCAppBanner(appInfo: app, delegate: self) + LCAppBanner(appInfo: app, delegate: self, appDataFolders: appDataFolderNames, tweakFolders: tweakFolderNames) } .transition(.scale) } @@ -191,12 +221,12 @@ struct LCAppListView : View, LCAppBannerDelegate { } let fm = FileManager() - self.installProgress = Progress.discreteProgress(totalUnitCount: 100) + let installProgress = Progress.discreteProgress(totalUnitCount: 100) self.installProgressPercentage = 0.0 let progressObserver = ProgressObserver(delegate: onInstallProgress) - self.installProgress.addObserver(progressObserver, forKeyPath: "fractionCompleted", context: nil) + installProgress.addObserver(progressObserver, forKeyPath: "fractionCompleted", context: nil) let decompressProgress = Progress.discreteProgress(totalUnitCount: 100) - self.installProgress.addChild(decompressProgress, withPendingUnitCount: 80) + installProgress.addChild(decompressProgress, withPendingUnitCount: 80) // decompress extract(url.path, fm.temporaryDirectory.path, decompressProgress) @@ -268,7 +298,7 @@ struct LCAppListView : View, LCAppBannerDelegate { success = success1 signSemaphore.signal() } - self.installProgress.addChild(signProgress!, withPendingUnitCount: 20) + installProgress.addChild(signProgress!, withPendingUnitCount: 20) signSemaphore.wait() if let error = error { diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 118d1b6..5033c53 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -10,3 +10,72 @@ import SwiftUI extension String: LocalizedError { public var errorDescription: String? { return self } } + +// https://stackoverflow.com/questions/56726663/how-to-add-a-textfield-to-alert-in-swiftui +extension View { + + public func textFieldAlert( + isPresented: Binding, + title: String, + text: Binding, + placeholder: String = "", + action: @escaping (String?) -> Void, + actionCancel: @escaping (String?) -> Void + ) -> some View { + self.modifier(TextFieldAlertModifier(isPresented: isPresented, title: title, text: text, placeholder: placeholder, action: action, actionCancel: actionCancel)) + } + +} + +public struct TextFieldAlertModifier: ViewModifier { + + @State private var alertController: UIAlertController? + + @Binding var isPresented: Bool + + let title: String + let text: Binding + let placeholder: String + let action: (String?) -> Void + let actionCancel: (String?) -> Void + + public func body(content: Content) -> some View { + content.onChange(of: isPresented) { isPresented in + if isPresented, alertController == nil { + let alertController = makeAlertController() + self.alertController = alertController + guard let scene = UIApplication.shared.connectedScenes.first as? UIWindowScene else { + return + } + scene.windows.first?.rootViewController?.present(alertController, animated: true) + } else if !isPresented, let alertController = alertController { + alertController.dismiss(animated: true) + self.alertController = nil + } + } + } + + private func makeAlertController() -> UIAlertController { + let controller = UIAlertController(title: title, message: nil, preferredStyle: .alert) + controller.addTextField { + $0.placeholder = self.placeholder + $0.text = self.text.wrappedValue + $0.clearButtonMode = .always + } + controller.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in + self.actionCancel(nil) + shutdown() + }) + controller.addAction(UIAlertAction(title: "OK", style: .default) { _ in + self.action(controller.textFields?.first?.text) + shutdown() + }) + return controller + } + + private func shutdown() { + isPresented = false + alertController = nil + } + +} diff --git a/LCAppDelegateSwiftUI.h b/LiveContainerUI/LCAppDelegateSwiftUI.h similarity index 86% rename from LCAppDelegateSwiftUI.h rename to LiveContainerUI/LCAppDelegateSwiftUI.h index ae15f5a..6fbb895 100644 --- a/LCAppDelegateSwiftUI.h +++ b/LiveContainerUI/LCAppDelegateSwiftUI.h @@ -2,6 +2,7 @@ #import @interface LCSwiftBridge : NSObject + (UIViewController * _Nonnull)getRootVC; ++ (BOOL)launchToGuestAppWithURL:(NSURL * _Nullable)url; @end @interface LCAppDelegateSwiftUI : UIResponder diff --git a/LCAppDelegateSwiftUI.m b/LiveContainerUI/LCAppDelegateSwiftUI.m similarity index 94% rename from LCAppDelegateSwiftUI.m rename to LiveContainerUI/LCAppDelegateSwiftUI.m index 6a4d04f..b381e5e 100644 --- a/LCAppDelegateSwiftUI.m +++ b/LiveContainerUI/LCAppDelegateSwiftUI.m @@ -1,5 +1,6 @@ #import "LCAppDelegateSwiftUI.h" #import +#import "LCUtils.h" @implementation LCAppDelegateSwiftUI @@ -23,8 +24,7 @@ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(N // NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; // [((LCTabBarController*)_rootViewController) openWebPage:decodedUrl]; // } -// return [LCUtils launchToGuestAppWithURL:url]; - return true; + return [LCUtils launchToGuestAppWithURL:url]; } @end diff --git a/Makefile b/Makefile index 4612321..7e89547 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ export CONFIG_COMMIT = $(shell git log --oneline | sed '2,10000000d' | cut -b 1- # Build the app APPLICATION_NAME = LiveContainer -$(APPLICATION_NAME)_FILES = dyld_bypass_validation.m main.m utils.m fishhook/fishhook.c LCSharedUtils.m LCAppDelegateSwiftUI.m +$(APPLICATION_NAME)_FILES = dyld_bypass_validation.m main.m utils.m fishhook/fishhook.c LCSharedUtils.m LiveContainerUI/LCAppDelegateSwiftUI.m LiveContainerUI/LCUtils.m LiveContainerUI/LCMachOUtils.m $(APPLICATION_NAME)_CODESIGN_FLAGS = -Sentitlements.xml $(APPLICATION_NAME)_CFLAGS = -fobjc-arc $(APPLICATION_NAME)_LDFLAGS = -e_LiveContainerMain -rpath @loader_path/Frameworks diff --git a/main.m b/main.m index 1f6323a..3aa7ec4 100644 --- a/main.m +++ b/main.m @@ -318,7 +318,6 @@ int LiveContainerMain(int argc, char *argv[]) { lcUserDefaults = NSUserDefaults.standardUserDefaults; NSString *selectedApp = [lcUserDefaults stringForKey:@"selected"]; - NSLog(@"[NMSL]: selectedApp = %@", selectedApp); if (selectedApp) { NSString *launchUrl = [lcUserDefaults stringForKey:@"launchAppUrlScheme"]; [lcUserDefaults removeObjectForKey:@"selected"]; From 19e7a47b651e892acae8e0f3e7607964ede7713f Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Mon, 26 Aug 2024 01:06:06 +0800 Subject: [PATCH 05/36] user can now input url scheme --- LiveContainerSwiftUI/LCAppListView.swift | 129 ++++++++- LiveContainerSwiftUI/LCSwiftBridge.h | 1 + LiveContainerSwiftUI/LCSwiftBridge.m | 4 + LiveContainerSwiftUI/LCWebView.swift | 262 ++++++++++++++++++ .../project.pbxproj | 4 + LiveContainerSwiftUI/ObjcBridge.swift | 11 + LiveContainerUI/LCAppDelegateSwiftUI.h | 2 +- LiveContainerUI/LCAppDelegateSwiftUI.m | 20 +- 8 files changed, 412 insertions(+), 21 deletions(-) create mode 100644 LiveContainerSwiftUI/LCWebView.swift diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index cc33806..50cd706 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -46,21 +46,27 @@ struct LCAppListView : View, LCAppBannerDelegate { @State var appDataFolderNames: [String] @State var tweakFolderNames: [String] + @State var didAppear = false // ipa choosing stuff @State var choosingIPA = false - @State var installErrorShow = false - @State var installErrorInfo = "" + @State var errorShow = false + @State var errorInfo = "" // ipa installing stuff @State var installprogressVisible = false @State var installProgressPercentage = 0.0 + @State var uiInstallProgressPercentage = 0.0 @State var installReplaceComfirmVisible = false @State var installOptions: [AppReplaceOption] @State var installOptionChosen: AppReplaceOption? @State var installOptionSemaphore = DispatchSemaphore(value: 0) - + @State var webViewOpened = false + @State var webViewURL : URL = URL(string: "https://www.google.com")! + @State private var webViewUrlInputOpened = false + @State private var webViewUrlInputContent = "" + @State private var webViewUrlInputSemaphore = DispatchSemaphore(value: 0) init() { let fm = FileManager() @@ -118,7 +124,6 @@ struct LCAppListView : View, LCAppBannerDelegate { var body: some View { NavigationView { - ScrollView { LazyVStack(pinnedViews:[.sectionHeaders]) { Section { @@ -131,9 +136,19 @@ struct LCAppListView : View, LCAppBannerDelegate { .padding() } header: { GeometryReader{ g in - ProgressView(value: installProgressPercentage) + ProgressView(value: uiInstallProgressPercentage) .labelsHidden() .opacity(installprogressVisible ? 1 : 0) + .scaleEffect(y: 0.5) + .onChange(of: installProgressPercentage) { newValue in + if newValue > uiInstallProgressPercentage { + withAnimation(.easeIn(duration: 0.3)) { + uiInstallProgressPercentage = newValue + } + } else { + uiInstallProgressPercentage = newValue + } + } } } @@ -141,7 +156,12 @@ struct LCAppListView : View, LCAppBannerDelegate { .animation(.easeInOut, value: apps) } - + .onAppear { + if !didAppear { + didAppear = true + checkIfAppDelegateNeedOpenWebPage() + } + } .navigationTitle("My Apps") .toolbar { @@ -159,13 +179,18 @@ struct LCAppListView : View, LCAppBannerDelegate { }) } + ToolbarItem(placement: .topBarTrailing) { + Button("Open Link", systemImage: "link", action: { + onOpenWebViewTapped() + }) + } } } - .alert(isPresented: $installErrorShow){ - Alert(title: Text("Installation Failed"), message: Text(installErrorInfo)) + .alert(isPresented: $errorShow){ + Alert(title: Text("Error"), message: Text(errorInfo)) } .fileImporter(isPresented: $choosingIPA, allowedContentTypes: [UTType(filenameExtension: "ipa")!]) { result in startInstallApp(result) @@ -190,9 +215,93 @@ struct LCAppListView : View, LCAppBannerDelegate { } message: { Text("There is an existing application with the same bundle identifier. Replace one or install as new.") } + .textFieldAlert( + isPresented: $webViewUrlInputOpened, + title: "Enter Url or Url Scheme", + text: $webViewUrlInputContent, + placeholder: "scheme://", + action: { newText in + self.webViewUrlInputContent = newText! + webViewUrlInputSemaphore.signal() + }, + actionCancel: {_ in + self.webViewUrlInputContent = "" + webViewUrlInputSemaphore.signal() + } + ) + .fullScreenCover(isPresented: $webViewOpened) { + LCWebView(url: $webViewURL, apps: $apps, isPresent: $webViewOpened) + } } + func onOpenWebViewTapped() { + DispatchQueue.global().async { + webViewUrlInputOpened = true + webViewUrlInputSemaphore.wait() + if webViewUrlInputContent == "" { + return + } + openWebView(urlString: webViewUrlInputContent) + webViewUrlInputContent = "" + } + } + + func checkIfAppDelegateNeedOpenWebPage() { + LCObjcBridge.openUrlStrFunc = openWebView; + if LCObjcBridge.urlStrToOpen != nil { + self.openWebView(urlString: LCObjcBridge.urlStrToOpen!) + LCObjcBridge.urlStrToOpen = nil + } + } + + func openWebView(urlString: String) { + guard var urlToOpen = URLComponents(string: urlString), urlToOpen.url != nil else { + errorInfo = "The input url is invalid. Please check and try again" + errorShow = true + webViewUrlInputContent = "" + return + } + webViewUrlInputContent = "" + if urlToOpen.scheme == nil || urlToOpen.scheme! == "" { + urlToOpen.scheme = "https" + } + if urlToOpen.scheme != "https" && urlToOpen.scheme != "http" { + var appToLaunch : LCAppInfo? = nil + appLoop: + for app in apps { + if let schemes = app.urlSchemes() { + for scheme in schemes { + if let scheme = scheme as? String, scheme == urlToOpen.scheme { + appToLaunch = app + break appLoop + } + } + } + } + guard let appToLaunch = appToLaunch else { + errorInfo = "Scheme \"\(urlToOpen.scheme!)\" cannot be opened by any app installed in LiveContainer." + errorShow = true + return + } + + UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") + UserDefaults.standard.setValue(urlToOpen.url!.absoluteString, forKey: "launchAppUrlScheme") + LCUtils.launchToGuestApp() + + return + } + webViewURL = urlToOpen.url! + if webViewOpened { + webViewOpened = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + webViewOpened = true + }) + } else { + webViewOpened = true + } + } + func startInstallApp(_ result:Result) { @@ -202,8 +311,8 @@ struct LCAppListView : View, LCAppBannerDelegate { self.installprogressVisible = true try installIpaFile(fileUrl) } catch { - installErrorInfo = error.localizedDescription - installErrorShow = true + errorInfo = error.localizedDescription + errorShow = true self.installprogressVisible = false } } diff --git a/LiveContainerSwiftUI/LCSwiftBridge.h b/LiveContainerSwiftUI/LCSwiftBridge.h index bb9ac58..8846c9a 100644 --- a/LiveContainerSwiftUI/LCSwiftBridge.h +++ b/LiveContainerSwiftUI/LCSwiftBridge.h @@ -13,4 +13,5 @@ @interface LCSwiftBridge : NSObject + (UIViewController * _Nonnull)getRootVC; ++ (void)openWebPageWithUrlStr:(NSURL* _Nonnull)url; @end diff --git a/LiveContainerSwiftUI/LCSwiftBridge.m b/LiveContainerSwiftUI/LCSwiftBridge.m index ecd7dff..a1499e0 100644 --- a/LiveContainerSwiftUI/LCSwiftBridge.m +++ b/LiveContainerSwiftUI/LCSwiftBridge.m @@ -15,4 +15,8 @@ + (UIViewController * _Nonnull)getRootVC { return [LCObjcBridge getRootVC]; } ++ (void)openWebPageWithUrlStr:(NSString* _Nonnull)urlStr { + [LCObjcBridge openWebPageWithUrlStr:urlStr]; +} + @end diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift new file mode 100644 index 0000000..af52ac0 --- /dev/null +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -0,0 +1,262 @@ +// +// SwiftUIView.swift +// nmsl +// +// Created by s s on 2024/8/23. +// + +import SwiftUI +import WebKit + +struct LCWebView: View { + @State private var webView : WebView + @State private var didAppear = false + + @Binding var url : URL + @Binding var isPresent: Bool + @State private var loadStatus = 0.0 + @State private var uiLoadStatus = 0.0 + @State private var pageTitle = "" + + @Binding var apps : [LCAppInfo] + + @State private var runAppAlertShow = false + @State private var runAppAlertMsg = "" + @State private var doRunApp = false + @State private var renameFolderContent = "" + @State private var doRunAppSemaphore = DispatchSemaphore(value: 0) + + @State private var errorShow = false + @State private var errorInfo = "" + + init(url: Binding, apps: Binding<[LCAppInfo]>, isPresent: Binding) { + self.webView = WebView() + self._url = url + self._apps = apps + self._isPresent = isPresent + } + + var body: some View { + + VStack(spacing: 0) { + HStack { + HStack { + Button(action: { + webView.goBack() + }, label: { + Image(systemName: "chevron.backward") + }) + + Button(action: { + webView.goForward() + }, label: { + Image(systemName: "chevron.forward") + }).padding(.horizontal) + } + + Spacer() + Text(pageTitle) + .lineLimit(1) + Spacer() + Button(action: { + webView.reload() + }, label: { + Image(systemName: "arrow.clockwise") + }).padding(.horizontal) + Button(action: { + isPresent = false + }, label: { + Text("Done") + }) + + } + .padding([.bottom, .horizontal]) + .background(Color(.systemGray6)) + .overlay(alignment: .bottomTrailing) { + ProgressView(value: uiLoadStatus) + .opacity(loadStatus == 1.0 ? 0 : 1) + .scaleEffect(y: 0.5) + .offset(y: 1) + .onChange(of: loadStatus) { newValue in + if newValue > uiLoadStatus { + withAnimation(.easeIn(duration: 0.3)) { + uiLoadStatus = newValue + } + } else { + uiLoadStatus = newValue + } + } + + } + webView + } + .onAppear(){ + webView.loadURL(url: url) + if !didAppear { + onViewAppear() + didAppear = true + } + + } + .alert("Run App", isPresented: $runAppAlertShow) { + Button("Run", action: { + self.doRunApp = true + self.doRunAppSemaphore.signal() + }) + Button("Cancel", role: .cancel, action: { + self.doRunApp = false + self.doRunAppSemaphore.signal() + }) + } message: { + Text(runAppAlertMsg) + } + .alert("Error", isPresented: $errorShow) { + Button("OK", action: { + }) + } message: { + Text(errorInfo) + } + + } + + func onViewAppear() { + let observer = WebViewLoadObserver(loadStatus: $loadStatus, webView: self.webView.webView) + let webViewDelegate = WebViewDelegate(pageTitle: $pageTitle, urlSchemeHandler:onURLSchemeDetected) + webView.setDelegate(delegete: webViewDelegate) + webView.setObserver(observer: observer) + } + + public func onURLSchemeDetected(url: URL) { + DispatchQueue.global().async { + var appToLaunch : LCAppInfo? = nil + appLoop: for app in apps { + if let schemes = app.urlSchemes() { + for scheme in schemes { + if let scheme = scheme as? String, scheme == url.scheme { + appToLaunch = app + break appLoop + } + } + } + } + + guard let appToLaunch = appToLaunch else { + errorInfo = "Scheme \"\(url.scheme!)\" cannot be opened by any app installed in LiveContainer." + errorShow = true + return + } + + runAppAlertMsg = "This web page is trying to launch \"\(appToLaunch.displayName()!)\", continue?" + runAppAlertShow = true + self.doRunAppSemaphore.wait() + if !doRunApp { + return + } + + UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") + UserDefaults.standard.setValue(url.absoluteString, forKey: "launchAppUrlScheme") + LCUtils.launchToGuestApp() + } + } +} + +class WebViewLoadObserver : NSObject { + private var loadStatus: Binding + private var webView: WKWebView + + init(loadStatus: Binding, webView: WKWebView) { + self.loadStatus = loadStatus + self.webView = webView + super.init() + self.webView.addObserver(self, forKeyPath: "estimatedProgress", options: .new, context: nil); + } + + override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) { + if keyPath == "estimatedProgress" { + loadStatus.wrappedValue = self.webView.estimatedProgress + } + } + + +} + +class WebViewDelegate : NSObject,WKNavigationDelegate { + private var pageTitle: Binding + private var urlSchemeHandler: (URL) -> Void + + init(pageTitle: Binding, urlSchemeHandler: @escaping (URL) -> Void) { + self.pageTitle = pageTitle + self.urlSchemeHandler = urlSchemeHandler + super.init() + } + + func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + decisionHandler((WKNavigationActionPolicy)(rawValue: WKNavigationActionPolicy.allow.rawValue + 2)!) + guard let scheme = navigationAction.request.url?.scheme else { + return + } + if(scheme == "https" || scheme == "http" || scheme == "about" || scheme == "itms-appss") { + return; + } + urlSchemeHandler(navigationAction.request.url!) + + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.pageTitle.wrappedValue = webView.title! + } + + +} + +struct WebView: UIViewRepresentable { + + let webView: WKWebView + var observer: WebViewLoadObserver? + var delegate: WKNavigationDelegate? + + init() { + self.webView = WKWebView() + } + + mutating func setDelegate(delegete: WKNavigationDelegate) { + self.delegate = delegete + self.webView.navigationDelegate = delegete + } + + mutating func setObserver(observer: WebViewLoadObserver) { + self.observer = observer + } + + func makeUIView(context: Context) -> WKWebView { + webView.allowsBackForwardNavigationGestures = true + webView.customUserAgent = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_6_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1" + return webView + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + + } + + func reload() { + webView.reload() + } + + func goBack(){ + webView.goBack() + } + + func goForward(){ + webView.goForward() + } + + + func loadURL(url: URL) { + webView.load(URLRequest(url: url)) + } + + +} + diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj index 891068b..3f10d90 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 173564D12C76FE3500C6C918 /* ObjcBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C62C76FE3500C6C918 /* ObjcBridge.swift */; }; 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C72C76FE3500C6C918 /* LCAppBanner.swift */; }; 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173564C82C76FE3500C6C918 /* Assets.xcassets */; }; + 173F18402C7B7B74002953AA /* LCWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173F183F2C7B7B74002953AA /* LCWebView.swift */; }; 178B4C3E2C77654400DD1F74 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178B4C3D2C77654400DD1F74 /* Shared.swift */; }; /* End PBXBuildFile section */ @@ -34,6 +35,7 @@ 173564C62C76FE3500C6C918 /* ObjcBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcBridge.swift; sourceTree = ""; }; 173564C72C76FE3500C6C918 /* LCAppBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCAppBanner.swift; sourceTree = ""; }; 173564C82C76FE3500C6C918 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; + 173F183F2C7B7B74002953AA /* LCWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCWebView.swift; sourceTree = ""; }; 178B4C3D2C77654400DD1F74 /* Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LiveContainerSwiftUI-Bridging-Header.h"; sourceTree = ""; }; 17B9B88D2C760678009D079E /* LiveContainerSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveContainerSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -53,6 +55,7 @@ 173564BB2C76FE1500C6C918 /* LiveContainerSwiftUI */ = { isa = PBXGroup; children = ( + 173F183F2C7B7B74002953AA /* LCWebView.swift */, 173564C82C76FE3500C6C918 /* Assets.xcassets */, 173564C72C76FE3500C6C918 /* LCAppBanner.swift */, 173564BC2C76FE3500C6C918 /* LCAppListView.swift */, @@ -205,6 +208,7 @@ files = ( 178B4C3E2C77654400DD1F74 /* Shared.swift in Sources */, 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */, + 173F18402C7B7B74002953AA /* LCWebView.swift in Sources */, 173564CE2C76FE3500C6C918 /* Makefile in Sources */, 173564CF2C76FE3500C6C918 /* LCSwiftBridge.m in Sources */, 173564C92C76FE3500C6C918 /* LCAppListView.swift in Sources */, diff --git a/LiveContainerSwiftUI/ObjcBridge.swift b/LiveContainerSwiftUI/ObjcBridge.swift index 2f667e7..53e4def 100644 --- a/LiveContainerSwiftUI/ObjcBridge.swift +++ b/LiveContainerSwiftUI/ObjcBridge.swift @@ -10,6 +10,17 @@ import SwiftUI @objc public class LCObjcBridge: NSObject { + public static var urlStrToOpen: String? = nil + public static var openUrlStrFunc: ((String) -> Void)? + + @objc public static func openWebPage(urlStr: String) { + if openUrlStrFunc == nil { + urlStrToOpen = urlStr + } else { + openUrlStrFunc!(urlStr) + } + } + @objc public static func getRootVC() -> UIViewController { let rootView = LCTabView() let rootVC = UIHostingController(rootView: rootView) diff --git a/LiveContainerUI/LCAppDelegateSwiftUI.h b/LiveContainerUI/LCAppDelegateSwiftUI.h index 6fbb895..2637696 100644 --- a/LiveContainerUI/LCAppDelegateSwiftUI.h +++ b/LiveContainerUI/LCAppDelegateSwiftUI.h @@ -2,7 +2,7 @@ #import @interface LCSwiftBridge : NSObject + (UIViewController * _Nonnull)getRootVC; -+ (BOOL)launchToGuestAppWithURL:(NSURL * _Nullable)url; ++ (void)openWebPageWithUrlStr:(NSString* _Nonnull)urlStr; @end @interface LCAppDelegateSwiftUI : UIResponder diff --git a/LiveContainerUI/LCAppDelegateSwiftUI.m b/LiveContainerUI/LCAppDelegateSwiftUI.m index b381e5e..638ad39 100644 --- a/LiveContainerUI/LCAppDelegateSwiftUI.m +++ b/LiveContainerUI/LCAppDelegateSwiftUI.m @@ -14,16 +14,16 @@ - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:( - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(NSDictionary *)options { // handle page open request from URL scheme -// if([url.host isEqualToString:@"open-web-page"]) { -// NSURLComponents* urlComponent = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; -// if(urlComponent.queryItems.count == 0){ -// return YES; -// } -// -// NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:urlComponent.queryItems[0].value options:0]; -// NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; -// [((LCTabBarController*)_rootViewController) openWebPage:decodedUrl]; -// } + if([url.host isEqualToString:@"open-web-page"]) { + NSURLComponents* urlComponent = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + if(urlComponent.queryItems.count == 0){ + return YES; + } + + NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:urlComponent.queryItems[0].value options:0]; + NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + [NSClassFromString(@"LCSwiftBridge") openWebPageWithUrlStr:decodedUrl]; + } return [LCUtils launchToGuestAppWithURL:url]; } From 81a3bb692b8912bfe4a6f41d5b4e4e669e7d3801 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Mon, 26 Aug 2024 17:19:28 +0800 Subject: [PATCH 06/36] allow open url by universal links --- LiveContainerSwiftUI/LCWebView.swift | 98 ++++++++++++++++++++++++++-- LiveContainerSwiftUI/Shared.swift | 33 ++++++++++ TweakLoader/UIKit+GuestHooks.m | 53 ++++++++++++--- UIKitPrivate.h | 5 ++ 4 files changed, 175 insertions(+), 14 deletions(-) diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index af52ac0..ff25d9e 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -121,7 +121,7 @@ struct LCWebView: View { func onViewAppear() { let observer = WebViewLoadObserver(loadStatus: $loadStatus, webView: self.webView.webView) - let webViewDelegate = WebViewDelegate(pageTitle: $pageTitle, urlSchemeHandler:onURLSchemeDetected) + let webViewDelegate = WebViewDelegate(pageTitle: $pageTitle, urlSchemeHandler:onURLSchemeDetected, universalLinkHandler: onUniversalLinkDetected) webView.setDelegate(delegete: webViewDelegate) webView.setObserver(observer: observer) } @@ -158,6 +158,37 @@ struct LCWebView: View { LCUtils.launchToGuestApp() } } + + public func onUniversalLinkDetected(url: URL, bundleIDs: [String]) { + DispatchQueue.global().async { + var bundleIDToAppDict: [String: LCAppInfo] = [:] + for app in apps { + bundleIDToAppDict[app.bundleIdentifier()!] = app + } + + var appToLaunch: LCAppInfo? = nil + for bundleID in bundleIDs { + if let app = bundleIDToAppDict[bundleID] { + appToLaunch = app + break + } + } + guard let appToLaunch = appToLaunch else { + return + } + + runAppAlertMsg = "This web page is can be opened in \"\(appToLaunch.displayName()!)\" according to its Associated Domains, continue?" + runAppAlertShow = true + self.doRunAppSemaphore.wait() + if !doRunApp { + return + } + UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") + UserDefaults.standard.setValue(url.absoluteString, forKey: "launchAppUrlScheme") + LCUtils.launchToGuestApp() + } + + } } class WebViewLoadObserver : NSObject { @@ -183,10 +214,13 @@ class WebViewLoadObserver : NSObject { class WebViewDelegate : NSObject,WKNavigationDelegate { private var pageTitle: Binding private var urlSchemeHandler: (URL) -> Void + private var universalLinkHandler: (URL , [String]) -> Void // url, [String] of all apps that can open this web page + var domainBundleIdDict : [String:[String]] = [:] - init(pageTitle: Binding, urlSchemeHandler: @escaping (URL) -> Void) { + init(pageTitle: Binding, urlSchemeHandler: @escaping (URL) -> Void, universalLinkHandler: @escaping (URL , [String]) -> Void) { self.pageTitle = pageTitle self.urlSchemeHandler = urlSchemeHandler + self.universalLinkHandler = universalLinkHandler super.init() } @@ -194,13 +228,23 @@ class WebViewDelegate : NSObject,WKNavigationDelegate { decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { decisionHandler((WKNavigationActionPolicy)(rawValue: WKNavigationActionPolicy.allow.rawValue + 2)!) - guard let scheme = navigationAction.request.url?.scheme else { + guard let url = navigationAction.request.url, let scheme = navigationAction.request.url?.scheme else { return } - if(scheme == "https" || scheme == "http" || scheme == "about" || scheme == "itms-appss") { + if(scheme == "https") { + DispatchQueue.global().async { + self.loadDomainAssociations(url: url) + if let host = url.host, let appIDs = self.domainBundleIdDict[host] { + self.universalLinkHandler(url, appIDs) + } + + } + return + } + if(scheme == "http" || scheme == "about" || scheme == "itms-appss") { return; } - urlSchemeHandler(navigationAction.request.url!) + urlSchemeHandler(url) } @@ -209,6 +253,50 @@ class WebViewDelegate : NSObject,WKNavigationDelegate { } + func loadDomainAssociations(url: URL) { + if url.scheme != "https" || url.host == nil { + return + } + if self.domainBundleIdDict[url.host!] != nil { + return + } + guard let host = url.host else { + return + } + + var loadSemaphore = DispatchSemaphore(value: 0) + // download and read apple-app-site-association + let appleAppSiteAssociationURLs = [ + URL(string: "https://\(host)/apple-app-site-association")!, + URL(string: "https://\(host)/.well-known/apple-app-site-association")! + ] + for siteAssociationURL in appleAppSiteAssociationURLs { + let task = URLSession.shared.dataTask(with: siteAssociationURL) { data, response, error in + do { + guard let data = data else { + loadSemaphore.signal() + return + } + let siteAssociationObj = try JSONDecoder().decode(SiteAssociation.self, from: data) + guard let detailItems = siteAssociationObj.applinks?.details else { + loadSemaphore.signal() + return + } + self.domainBundleIdDict[host] = [] + for item in detailItems { + self.domainBundleIdDict[host]!.append(contentsOf: item.getBundleIds()) + } + } catch { + + } + loadSemaphore.signal() + } + task.resume() + } + for siteAssociationURL in appleAppSiteAssociationURLs { + loadSemaphore.wait() + } + } } struct WebView: UIViewRepresentable { diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 5033c53..6149560 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -79,3 +79,36 @@ public struct TextFieldAlertModifier: ViewModifier { } } + +struct SiteAssociationDetailItem : Codable { + var appID: String? + var appIDs: [String]? + + func getBundleIds() -> [String] { + var ans : [String] = [] + // get rid of developer id + if let appID = appID, appID.count > 11 { + let index = appID.index(appID.startIndex, offsetBy: 11) + let modifiedString = String(appID[index...]) + ans.append(modifiedString) + } + if let appIDs = appIDs { + for appID in appIDs { + if appID.count > 11 { + let index = appID.index(appID.startIndex, offsetBy: 11) + let modifiedString = String(appID[index...]) + ans.append(modifiedString) + } + } + } + return ans + } +} + +struct AppLinks : Codable { + var details : [SiteAssociationDetailItem]? +} + +struct SiteAssociation : Codable { + var applinks: AppLinks? +} diff --git a/TweakLoader/UIKit+GuestHooks.m b/TweakLoader/UIKit+GuestHooks.m index 7fd1875..8ccb5af 100644 --- a/TweakLoader/UIKit+GuestHooks.m +++ b/TweakLoader/UIKit+GuestHooks.m @@ -35,8 +35,26 @@ void LCShowSwitchAppConfirmation(NSURL *url) { objc_setAssociatedObject(alert, @"window", window, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +void openUniversalLink(NSString* decodedUrl) { + UIActivityContinuationManager* uacm = [[UIApplication sharedApplication] _getActivityContinuationManager]; + NSUserActivity* activity = [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; + activity.webpageURL = [NSURL URLWithString: decodedUrl]; + NSDictionary* dict = @{ + @"UIApplicationLaunchOptionsUserActivityKey": activity, + @"UICanvasConnectionOptionsUserActivityKey": activity, + @"UIApplicationLaunchOptionsUserActivityIdentifierKey": NSUUID.UUID.UUIDString, + @"UINSUserActivitySourceApplicationKey": @"com.apple.mobilesafari", + @"UIApplicationLaunchOptionsUserActivityTypeKey": NSUserActivityTypeBrowsingWeb, + @"_UISceneConnectionOptionsUserActivityTypeKey": NSUserActivityTypeBrowsingWeb, + @"_UISceneConnectionOptionsUserActivityKey": activity, + @"UICanvasConnectionOptionsUserActivityTypeKey": NSUserActivityTypeBrowsingWeb + }; + + [uacm handleActivityContinuation:dict isSuspended:nil]; +} + void LCOpenWebPage(NSString* webPageUrlString) { - NSString *message = [NSString stringWithFormat:@"Are you sure you want to open the web page and launch an app? Doing so will terminate this app."]; + NSString *message = [NSString stringWithFormat:@"Are you sure you want to open the web page and launch an app? Doing so will terminate this app. You can try to open it in the current app if it supports Universal Links."]; UIWindow *window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"LiveContainer" message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { @@ -44,6 +62,11 @@ void LCOpenWebPage(NSString* webPageUrlString) { [NSClassFromString(@"LCSharedUtils") launchToGuestApp]; }]; [alert addAction:okAction]; + UIAlertAction* openNowAction = [UIAlertAction actionWithTitle:@"Current App" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + openUniversalLink(webPageUrlString); + window.windowScene = nil; + }]; + [alert addAction:openNowAction]; UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { window.windowScene = nil; }]; @@ -83,9 +106,15 @@ - (void)hook__applicationOpenURLAction:(id)action payload:(NSDictionary *)payloa // Convert the base64 encoded url into String NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:realUrlEncoded options:0]; NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; - NSMutableDictionary* newPayload = [payload mutableCopy]; - newPayload[UIApplicationLaunchOptionsURLKey] = decodedUrl; - [self hook__applicationOpenURLAction:action payload:newPayload origin:origin]; + // it's a Universal link, let's call -[UIActivityContinuationManager handleActivityContinuation:isSuspended:] + if([decodedUrl hasPrefix:@"https"]) { + openUniversalLink(decodedUrl); + } else { + NSMutableDictionary* newPayload = [payload mutableCopy]; + newPayload[UIApplicationLaunchOptionsURLKey] = decodedUrl; + [self hook__applicationOpenURLAction:action payload:newPayload origin:origin]; + } + return; } else if ([url hasPrefix:@"livecontainer://livecontainer-launch?"]) { if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { @@ -139,11 +168,17 @@ - (void)hook_scene:(id)scene didReceiveActions:(NSSet *)actions fromTransitionCo NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:realUrlEncoded options:0]; NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; - NSMutableSet *newActions = actions.mutableCopy; - [newActions removeObject:urlAction]; - UIOpenURLAction *newUrlAction = [[UIOpenURLAction alloc] initWithURL:[NSURL URLWithString:decodedUrl]]; - [newActions addObject:newUrlAction]; - [self hook_scene:scene didReceiveActions:newActions fromTransitionContext:context]; + // it's a Universal link, let's call -[UIActivityContinuationManager handleActivityContinuation:isSuspended:] + if([decodedUrl hasPrefix:@"https"]) { + openUniversalLink(decodedUrl); + } else { + NSMutableSet *newActions = actions.mutableCopy; + [newActions removeObject:urlAction]; + UIOpenURLAction *newUrlAction = [[UIOpenURLAction alloc] initWithURL:[NSURL URLWithString:decodedUrl]]; + [newActions addObject:newUrlAction]; + [self hook_scene:scene didReceiveActions:newActions fromTransitionContext:context]; + } + return; } else if ([url hasPrefix:@"livecontainer://livecontainer-launch?"]){ // If it's not current app, then switch diff --git a/UIKitPrivate.h b/UIKitPrivate.h index 2f99c5e..22613e3 100644 --- a/UIKitPrivate.h +++ b/UIKitPrivate.h @@ -16,8 +16,13 @@ @property(nonatomic, copy) id shouldDismissHandler; @end +@interface UIActivityContinuationManager : UIResponder +- (NSDictionary*)handleActivityContinuation:(NSDictionary*)activityDict isSuspended:(id)isSuspended; +@end + @interface UIApplication(private) - (void)suspend; +- (UIActivityContinuationManager*)_getActivityContinuationManager; @end @interface UIContextMenuInteraction(private) From 48a308525c04a85b4d12b141f2bf2effd1d62a3b Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 27 Aug 2024 15:54:59 +0800 Subject: [PATCH 07/36] apps can be signed again if signature is invalid --- LiveContainerSwiftUI/LCAppBanner.swift | 80 ++++++++++++++++++++++-- LiveContainerSwiftUI/LCAppListView.swift | 63 +++++++------------ LiveContainerSwiftUI/LCWebView.swift | 4 +- LiveContainerUI/LCAppInfo.m | 25 +++----- 4 files changed, 108 insertions(+), 64 deletions(-) diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 53ee66b..3435473 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -40,6 +40,12 @@ struct LCAppBanner : View { @State private var errorShow = false @State private var errorInfo = "" + @State private var isSingingInProgress = false + @State private var signProgress = 0.0 + @State private var isAppRunning = false + + @State private var observer : NSKeyValueObservation? + init(appInfo: LCAppInfo, delegate: LCAppBannerDelegate, appDataFolders: [String], tweakFolders: [String]) { _appInfo = State(initialValue: appInfo) _appDataFolders = State(initialValue: appDataFolders) @@ -71,11 +77,34 @@ struct LCAppBanner : View { Button { runApp() } label: { - Text("Run").bold().foregroundColor(.white) + if !isSingingInProgress { + Text("Run").bold().foregroundColor(.white) + } else { + ProgressView().progressViewStyle(.circular) + } + } .padding() + .frame(idealWidth: 70) .frame(height: 32) - .background(Capsule().fill(Color("FontColor"))) + .fixedSize() + .background(GeometryReader { g in + if !isSingingInProgress { + Capsule().fill(Color("FontColor")) + } else { + let w = g.size.width + let h = g.size.height + Capsule() + .fill(Color("FontColor")).opacity(0.2) + Circle() + .fill(Color("FontColor")) + .frame(width: w * 2, height: w * 2) + .offset(x: (signProgress - 2) * g.size.width, y: h/2-w) + } + + }) + .clipShape(Capsule()) + .disabled(isAppRunning) } .padding() @@ -105,7 +134,7 @@ struct LCAppBanner : View { Button { renameDataFolder() } label: { - Label("Rename data folder", systemImage: "pencel") + Label("Rename data folder", systemImage: "pencil") } } @@ -195,8 +224,46 @@ struct LCAppBanner : View { } func runApp() { - UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") - LCUtils.launchToGuestApp() + isAppRunning = true + DispatchQueue.global().async { + let patchInfo = appInfo.patchExec() + if patchInfo == "SignNeeded" { + let bundlePath = URL(fileURLWithPath: appInfo.bundlePath()) + let signProgress = LCUtils.signAppBundle(bundlePath) { success, error in + self.appInfo.signCleanUp(withSuccessStatus: success) + self.isSingingInProgress = false + if success { + self.isSingingInProgress = false + UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") + LCUtils.launchToGuestApp() + } else { + errorInfo = error != nil ? error!.localizedDescription : "Signing failed with unknown error" + errorShow = true + } + } + guard let signProgress = signProgress else { + errorInfo = "Failed to initiate signing!" + errorShow = true + self.isAppRunning = false + return + } + self.isSingingInProgress = true + self.observer = signProgress.observe(\.fractionCompleted) { p, v in + self.signProgress = signProgress.fractionCompleted + } + } else if patchInfo != nil { + errorInfo = patchInfo! + errorShow = true + self.isAppRunning = false + return + } else { + UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") + LCUtils.launchToGuestApp() + } + self.isAppRunning = false + } + + } func setDataFolder(folderName: String?) { @@ -268,7 +335,8 @@ struct LCAppBanner : View { } func uninstall() { - DispatchQueue.global().async { + // Add delay because of https://stackoverflow.com/questions/60358948/swiftui-delete-row-in-list-with-context-menu-ui-glitch + DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { do { self.confirmAppRemovalShow = true; self.appRemovalSemaphore.wait() diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 50cd706..8f92486 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -14,29 +14,6 @@ struct AppReplaceOption : Hashable { var appToReplace: LCAppInfo? } -class ProgressObserver : NSObject { - var delegate : (_ fraction: Double) -> Void; - - init(delegate: @escaping (_: Double) -> Void) { - self.delegate = delegate - } - - override func observeValue(forKeyPath keyPath: String?, - of object: Any?, - change: [NSKeyValueChangeKey : Any]?, - context: UnsafeMutableRawPointer?) { - - if let theKeyPath = keyPath { - if theKeyPath == "fractionCompleted" { - let progress = object as! Progress - self.delegate(progress.fractionCompleted) - } - } - - - } -} - struct LCAppListView : View, LCAppBannerDelegate { private var docPath: URL var bundlePath: URL @@ -56,6 +33,7 @@ struct LCAppListView : View, LCAppBannerDelegate { @State var installprogressVisible = false @State var installProgressPercentage = 0.0 @State var uiInstallProgressPercentage = 0.0 + @State var installObserver : NSKeyValueObservation? @State var installReplaceComfirmVisible = false @State var installOptions: [AppReplaceOption] @@ -166,18 +144,23 @@ struct LCAppListView : View, LCAppBannerDelegate { .navigationTitle("My Apps") .toolbar { ToolbarItem(placement: .topBarLeading) { - Button("Add", systemImage: "plus", action: { - if choosingIPA { - choosingIPA = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + if !installprogressVisible { + Button("Add", systemImage: "plus", action: { + if choosingIPA { + choosingIPA = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + choosingIPA = true + }) + } else { choosingIPA = true - }) - } else { - choosingIPA = true - } + } + + + }) + } else { + ProgressView().progressViewStyle(.circular) + } - - }) } ToolbarItem(placement: .topBarTrailing) { Button("Open Link", systemImage: "link", action: { @@ -252,6 +235,9 @@ struct LCAppListView : View, LCAppBannerDelegate { if LCObjcBridge.urlStrToOpen != nil { self.openWebView(urlString: LCObjcBridge.urlStrToOpen!) LCObjcBridge.urlStrToOpen = nil + } else if let urlStr = UserDefaults.standard.string(forKey: "webPageToOpen") { + UserDefaults.standard.removeObject(forKey: "webPageToOpen") + self.openWebView(urlString: urlStr) } } @@ -319,10 +305,6 @@ struct LCAppListView : View, LCAppBannerDelegate { } - func onInstallProgress(_ fraction : Double) { - self.installProgressPercentage = fraction - } - func installIpaFile(_ url:URL) throws { if(!url.startAccessingSecurityScopedResource()) { @@ -332,8 +314,9 @@ struct LCAppListView : View, LCAppBannerDelegate { let installProgress = Progress.discreteProgress(totalUnitCount: 100) self.installProgressPercentage = 0.0 - let progressObserver = ProgressObserver(delegate: onInstallProgress) - installProgress.addObserver(progressObserver, forKeyPath: "fractionCompleted", context: nil) + self.installObserver = installProgress.observe(\.fractionCompleted) { p, v in + self.installProgressPercentage = p.fractionCompleted + } let decompressProgress = Progress.discreteProgress(totalUnitCount: 100) installProgress.addChild(decompressProgress, withPendingUnitCount: 80) @@ -361,7 +344,7 @@ struct LCAppListView : View, LCAppBannerDelegate { return app.bundleIdentifier()! == newAppInfo.bundleIdentifier() } if fm.fileExists(atPath: outputFolder.path) || sameBundleIdApp.count > 0 { - appRelativePath = "\(newAppInfo.bundleIdentifier()!)_\(CFAbsoluteTimeGetCurrent()).app" + appRelativePath = "\(newAppInfo.bundleIdentifier()!)_\(Int(CFAbsoluteTimeGetCurrent())).app" self.installOptions = [AppReplaceOption(isReplace: false, nameOfFolderToInstall: appRelativePath)] diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index ff25d9e..7a08dfa 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -177,7 +177,7 @@ struct LCWebView: View { return } - runAppAlertMsg = "This web page is can be opened in \"\(appToLaunch.displayName()!)\" according to its Associated Domains, continue?" + runAppAlertMsg = "This web page can be opened in \"\(appToLaunch.displayName()!)\" according to its Associated Domains, continue?" runAppAlertShow = true self.doRunAppSemaphore.wait() if !doRunApp { @@ -264,7 +264,7 @@ class WebViewDelegate : NSObject,WKNavigationDelegate { return } - var loadSemaphore = DispatchSemaphore(value: 0) + let loadSemaphore = DispatchSemaphore(value: 0) // download and read apple-app-site-association let appleAppSiteAssociationURLs = [ URL(string: "https://\(host)/apple-app-site-association")!, diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index 047d4b7..4149872 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -163,16 +163,14 @@ - (void)save { [_info writeToFile:[NSString stringWithFormat:@"%@/Info.plist", _bundlePath] atomically:YES]; } -- (void)preprocessBundleBeforeSiging:(NSURL *)bundleURL completion:(dispatch_block_t)completion { - dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - // Remove faulty file - [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"LiveContainer"] error:nil]; - // Remove PlugIns folder - [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"PlugIns"] error:nil]; - // Remove code signature from all library files - [LCUtils removeCodeSignatureFromBundleURL:bundleURL]; - dispatch_async(dispatch_get_main_queue(), completion); - }); +- (void)preprocessBundleBeforeSiging:(NSURL *)bundleURL { + // Remove faulty file + [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"LiveContainer"] error:nil]; + // Remove PlugIns folder + [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"PlugIns"] error:nil]; + // Remove code signature from all library files + [LCUtils removeCodeSignatureFromBundleURL:bundleURL]; + } // return "SignNeeded" if sign is needed, other wise return an error @@ -213,16 +211,11 @@ - (NSString*)patchExec { } else { return @"Failed to find ALTCertificate.p12. Please refresh your store and try again."; } - dispatch_semaphore_t sema = dispatch_semaphore_create(0); // Sign app if JIT-less is set up if ([info[@"LCJITLessSignID"] unsignedLongValue] != signID) { NSURL *appPathURL = [NSURL fileURLWithPath:appPath]; - [self preprocessBundleBeforeSiging:appPathURL completion:^{ - dispatch_semaphore_signal(sema); - }]; - dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); - + [self preprocessBundleBeforeSiging:appPathURL]; // We need to temporarily fake bundle ID and main executable to sign properly NSString *tmpExecPath = [appPath stringByAppendingPathComponent:@"LiveContainer.tmp"]; if (!info[@"LCBundleIdentifier"]) { From 7065585578da1aabbeb070b973caebb9181b68e3 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Wed, 28 Aug 2024 10:44:19 +0800 Subject: [PATCH 08/36] Settings & JitLess setup --- LiveContainerSwiftUI/LCAppBanner.swift | 2 +- LiveContainerSwiftUI/LCSettingsView.swift | 125 ++++++++++++++++++ LiveContainerSwiftUI/LCTabView.swift | 21 ++- .../project.pbxproj | 4 +- .../LCJITLessSetupViewController.h | 9 ++ .../LCJITLessSetupViewController.m | 14 ++ LiveContainerUI/LCUtils.h | 2 +- LiveContainerUI/LCUtils.m | 5 + LiveContainerUI/LCVersionInfo.h | 5 + LiveContainerUI/LCVersionInfo.m | 9 ++ LiveContainerUI/Makefile | 3 +- Makefile | 4 +- main.m | 8 +- 13 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 LiveContainerUI/LCVersionInfo.h create mode 100644 LiveContainerUI/LCVersionInfo.m diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 3435473..a583ea7 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -99,7 +99,7 @@ struct LCAppBanner : View { Circle() .fill(Color("FontColor")) .frame(width: w * 2, height: w * 2) - .offset(x: (signProgress - 2) * g.size.width, y: h/2-w) + .offset(x: (signProgress - 2) * w, y: h/2-w) } }) diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index 2013506..bc891d5 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -6,3 +6,128 @@ // import Foundation +import SwiftUI + +struct LCSettingsView: View { + @State var errorShow = false + @State var errorInfo = "" + + @State var isJitLessEnabled = false + @State var isAltCertIgnored = false + @State var frameShortIcon = false + @State var silentSwitchApp = false + @State var injectToLCItelf = false + + init() { + _isJitLessEnabled = State(initialValue: LCUtils.certificatePassword() != nil) + _isAltCertIgnored = State(initialValue: UserDefaults.standard.bool(forKey: "LCIgnoreALTCertificate")) + _frameShortIcon = State(initialValue: UserDefaults.standard.bool(forKey: "LCFrameShortcutIcons")) + _silentSwitchApp = State(initialValue: UserDefaults.standard.bool(forKey: "LCSwitchAppWithoutAsking")) + _injectToLCItelf = State(initialValue: UserDefaults.standard.bool(forKey: "LCLoadTweaksToSelf")) + } + + var body: some View { + NavigationView { + Form { + + Section{ + Button { + setupJitLess() + } label: { + if isJitLessEnabled { + Text("Renew JIT-less certificate") + } else { + Text("Setup JIT-less certificate") + } + } + + } header: { + Text("JIT-Less") + } footer: { + Text("JIT-less allows you to use LiveContainer without having to enable JIT. Requires AltStore or SideStore.") + } + Section { + Toggle(isOn: $isAltCertIgnored) { + Text("Ignore ALTCertificate.p12") + } + } footer: { + Text("If you see frequent re-sign, enable this option.") + } + + Section{ + Toggle(isOn: $frameShortIcon) { + Text("Frame Short Icon") + } + } header: { + Text("Miscellaneous") + } footer: { + Text("Frame shortcut icons with LiveContainer icon.") + } + + Section { + Toggle(isOn: $silentSwitchApp) { + Text("Switch App Without Asking") + } + } footer: { + Text("By default, LiveContainer asks you before switching app. Enable this to switch app immediately. Any unsaved data will be lost.") + } + + Section { + Toggle(isOn: $injectToLCItelf) { + Text("Load Tewaks to LiveContainer Itself") + } + } footer: { + Text("Place your tweaks into the global “Tweaks” folder and LiveContainer will pick them up.") + } + + VStack{ + Text(LCUtils.getVersionInfo()) + .foregroundStyle(.gray) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .background(Color(UIColor.systemGroupedBackground)) + .listRowInsets(EdgeInsets()) + } + .navigationBarTitle("Settings") + .alert(isPresented: $errorShow){ + Alert(title: Text("Error"), message: Text(errorInfo)) + } + + .onChange(of: isAltCertIgnored) { newValue in + saveItem(key: "LCIgnoreALTCertificate", val: newValue) + } + .onChange(of: silentSwitchApp) { newValue in + saveItem(key: "LCSwitchAppWithoutAsking", val: newValue) + } + .onChange(of: frameShortIcon) { newValue in + saveItem(key: "LCFrameShortcutIcons", val: newValue) + } + .onChange(of: injectToLCItelf) { newValue in + saveItem(key: "LCLoadTweaksToSelf", val: newValue) + } + } + + } + + func saveItem(key: String, val: Bool) { + UserDefaults.standard.setValue(val, forKey: key) + } + + func setupJitLess() { + if !LCUtils.isAppGroupAltStoreLike() { + errorInfo = "Unsupported installation method. Please use AltStore or SideStore to setup this feature." + errorShow = true + return; + } + do { + let packedIpaUrl = try LCUtils.archiveIPA(withSetupMode: true) + let storeInstallUrl = String(format: LCUtils.storeInstallURLScheme(), packedIpaUrl.absoluteString) + UIApplication.shared.open(URL(string: storeInstallUrl)!) + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + + } + +} diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index 73898ec..d300a2f 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -9,15 +9,32 @@ import Foundation import SwiftUI struct LCTabView: View { + @State var errorShow = false + @State var errorInfo = "" + var body: some View { TabView { LCAppListView() .tabItem { Label("Apps", systemImage: "square.stack.3d.up.fill") } - - + LCSettingsView() + .tabItem { + Label("Settings", systemImage: "gearshape.fill") + } + } + .alert(isPresented: $errorShow){ + Alert(title: Text("Error"), message: Text(errorInfo)) + } + } + + func checkLastLaunchError() { + guard let errorStr = UserDefaults.standard.string(forKey: "error") else { + return } + UserDefaults.standard.removeObject(forKey: "error") + errorInfo = errorStr + errorShow = true } } diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj index 3f10d90..76bcb2a 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -60,8 +60,6 @@ 173564C72C76FE3500C6C918 /* LCAppBanner.swift */, 173564BC2C76FE3500C6C918 /* LCAppListView.swift */, 173564C12C76FE3500C6C918 /* LCSettingsView.swift */, - 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */, - 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */, 173564BD2C76FE3500C6C918 /* LCTabView.swift */, 173564C02C76FE3500C6C918 /* LCTweaksView.swift */, 173564C52C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift */, @@ -70,6 +68,8 @@ 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */, 173564BF2C76FE3500C6C918 /* Preview Content */, 178B4C3D2C77654400DD1F74 /* Shared.swift */, + 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */, + 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */, ); name = LiveContainerSwiftUI; sourceTree = ""; diff --git a/LiveContainerUI/LCJITLessSetupViewController.h b/LiveContainerUI/LCJITLessSetupViewController.h index 392b637..34f245c 100644 --- a/LiveContainerUI/LCJITLessSetupViewController.h +++ b/LiveContainerUI/LCJITLessSetupViewController.h @@ -1,5 +1,14 @@ #import +@interface LCJITLessAppDelegate : UIResponder + +@property (nonatomic, strong) UIWindow *window; +@property (nonatomic, strong) UIViewController *rootViewController; + +@end + + + @interface LCJITLessSetupViewController : UIViewController @end diff --git a/LiveContainerUI/LCJITLessSetupViewController.m b/LiveContainerUI/LCJITLessSetupViewController.m index bf5d945..136f581 100644 --- a/LiveContainerUI/LCJITLessSetupViewController.m +++ b/LiveContainerUI/LCJITLessSetupViewController.m @@ -3,6 +3,20 @@ #import "LCUtils.h" #import "UIKitPrivate.h" +@implementation LCJITLessAppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + UIViewController *viewController; + viewController = [LCJITLessSetupViewController new]; + _rootViewController = [[UINavigationController alloc] initWithRootViewController:viewController]; + _window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; + _window.rootViewController = _rootViewController; + [_window makeKeyAndVisible]; + return YES; +} + +@end + @implementation LCJITLessSetupViewController - (void)showDialogTitle:(NSString *)title message:(NSString *)message handler:(void(^)(UIAlertAction *))handler { diff --git a/LiveContainerUI/LCUtils.h b/LiveContainerUI/LCUtils.h index 77a4404..f1f6fa2 100644 --- a/LiveContainerUI/LCUtils.h +++ b/LiveContainerUI/LCUtils.h @@ -31,5 +31,5 @@ void LCPatchExecSlice(const char *path, struct mach_header_64 *header); + (BOOL)isAppGroupAltStoreLike; + (NSString *)appGroupPath; + (NSString *)storeInstallURLScheme; - ++ (NSString *)getVersionInfo; @end diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index 2d7644f..1193fa2 100644 --- a/LiveContainerUI/LCUtils.m +++ b/LiveContainerUI/LCUtils.m @@ -4,6 +4,7 @@ #import "AltStoreCore/ALTSigner.h" #import "LCUtils.h" +#import "LCVersionInfo.h" @implementation LCUtils @@ -282,4 +283,8 @@ + (NSURL *)archiveIPAWithSetupMode:(BOOL)setup error:(NSError **)error { return tmpIPAPath; } ++ (NSString *)getVersionInfo { + return [NSClassFromString(@"LCVersionInfo") getVersionStr]; +} + @end diff --git a/LiveContainerUI/LCVersionInfo.h b/LiveContainerUI/LCVersionInfo.h new file mode 100644 index 0000000..9c1ec09 --- /dev/null +++ b/LiveContainerUI/LCVersionInfo.h @@ -0,0 +1,5 @@ +#import + +@interface LCVersionInfo : NSObject ++ (NSString*)getVersionStr; +@end \ No newline at end of file diff --git a/LiveContainerUI/LCVersionInfo.m b/LiveContainerUI/LCVersionInfo.m new file mode 100644 index 0000000..dda3f48 --- /dev/null +++ b/LiveContainerUI/LCVersionInfo.m @@ -0,0 +1,9 @@ +#import "LCVersionInfo.h" + +@implementation LCVersionInfo ++ (NSString*)getVersionStr { + return [NSString stringWithFormat:@"Version %@-%s (%s/%s)", + NSBundle.mainBundle.infoDictionary[@"CFBundleShortVersionString"], + CONFIG_TYPE, CONFIG_BRANCH, CONFIG_COMMIT]; +} +@end \ No newline at end of file diff --git a/LiveContainerUI/Makefile b/LiveContainerUI/Makefile index 10556a2..feceda2 100644 --- a/LiveContainerUI/Makefile +++ b/LiveContainerUI/Makefile @@ -2,7 +2,8 @@ include $(THEOS)/makefiles/common.mk FRAMEWORK_NAME = LiveContainerUI -LiveContainerUI_FILES = LCAppDelegate.m LCJITLessSetupViewController.m LCMachOUtils.m LCAppListViewController.m LCSettingsListController.m LCTabBarController.m LCTweakListViewController.m LCUtils.m MBRoundProgressView.m UIViewController+LCAlert.m unarchive.m LCAppInfo.m LCWebView.m +#LiveContainerUI_FILES = LCAppDelegate.m LCJITLessSetupViewController.m LCMachOUtils.m LCAppListViewController.m LCSettingsListController.m LCTabBarController.m LCTweakListViewController.m LCUtils.m MBRoundProgressView.m UIViewController+LCAlert.m unarchive.m LCAppInfo.m LCWebView.m +LiveContainerUI_FILES = LCVersionInfo.m LCJITLessSetupViewController.m LCUtils.m LCMachOUtils.m LCAppDelegateSwiftUI.m LiveContainerUI_CFLAGS = \ -fobjc-arc \ -DCONFIG_TYPE=\"$(CONFIG_TYPE)\" \ diff --git a/Makefile b/Makefile index 7e89547..3dcfb50 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,7 @@ export CONFIG_COMMIT = $(shell git log --oneline | sed '2,10000000d' | cut -b 1- # Build the app APPLICATION_NAME = LiveContainer -$(APPLICATION_NAME)_FILES = dyld_bypass_validation.m main.m utils.m fishhook/fishhook.c LCSharedUtils.m LiveContainerUI/LCAppDelegateSwiftUI.m LiveContainerUI/LCUtils.m LiveContainerUI/LCMachOUtils.m +$(APPLICATION_NAME)_FILES = dyld_bypass_validation.m main.m utils.m fishhook/fishhook.c LCSharedUtils.m $(APPLICATION_NAME)_CODESIGN_FLAGS = -Sentitlements.xml $(APPLICATION_NAME)_CFLAGS = -fobjc-arc $(APPLICATION_NAME)_LDFLAGS = -e_LiveContainerMain -rpath @loader_path/Frameworks @@ -19,7 +19,7 @@ $(APPLICATION_NAME)_FRAMEWORKS = UIKit include $(THEOS_MAKE_PATH)/application.mk -SUBPROJECTS += TweakLoader TestJITLess LiveContainerSwiftUI +SUBPROJECTS += LiveContainerUI TweakLoader TestJITLess LiveContainerSwiftUI include $(THEOS_MAKE_PATH)/aggregate.mk # Make the executable name longer so we have space to overwrite it with the guest app's name diff --git a/main.m b/main.m index 3aa7ec4..258717a 100644 --- a/main.m +++ b/main.m @@ -345,9 +345,13 @@ int LiveContainerMain(int argc, char *argv[]) { return 1; } } - - void *LiveContainerUIHandle = dlopen("@executable_path/Frameworks/LiveContainerSwiftUI.framework/LiveContainerSwiftUI", RTLD_LAZY); + void *LiveContainerUIHandle = dlopen("@executable_path/Frameworks/LiveContainerUI.framework/LiveContainerUI", RTLD_LAZY); assert(LiveContainerUIHandle); + if([NSBundle.mainBundle.executablePath.lastPathComponent isEqualToString:@"JITLessSetup"]) { + return UIApplicationMain(argc, argv, nil, @"LCJITLessAppDelegate"); + } + void *LiveContainerSwiftUIHandle = dlopen("@executable_path/Frameworks/LiveContainerSwiftUI.framework/LiveContainerSwiftUI", RTLD_LAZY); + assert(LiveContainerSwiftUIHandle); @autoreleasepool { if ([lcUserDefaults boolForKey:@"LCLoadTweaksToSelf"]) { dlopen("@executable_path/Frameworks/TweakLoader.dylib", RTLD_LAZY); From bd2f05a00725b98ec44ba0b2e65e4f70577f2470 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Wed, 28 Aug 2024 21:32:07 +0800 Subject: [PATCH 09/36] substitute async for all semaphores --- LiveContainerSwiftUI/LCAppBanner.swift | 252 ++++++++++++---------- LiveContainerSwiftUI/LCAppListView.swift | 157 +++++--------- LiveContainerSwiftUI/LCSettingsView.swift | 85 +++++++- LiveContainerSwiftUI/LCTabView.swift | 58 ++++- LiveContainerSwiftUI/LCWebView.swift | 183 ++++++++-------- LiveContainerSwiftUI/Shared.swift | 10 + 6 files changed, 430 insertions(+), 315 deletions(-) diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index a583ea7..bd33f08 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -11,14 +11,13 @@ import UniformTypeIdentifiers protocol LCAppBannerDelegate { func removeApp(app: LCAppInfo) - func getDocPath() -> URL } struct LCAppBanner : View { @State var appInfo: LCAppInfo var delegate: LCAppBannerDelegate - @State var appDataFolders: [String] - @State var tweakFolders: [String] + @Binding var appDataFolders: [String] + @Binding var tweakFolders: [String] @State private var uiDataFolder : String? @State private var uiTweakFolder : String? @@ -30,12 +29,12 @@ struct LCAppBanner : View { @State private var confirmAppRemoval = false @State private var confirmAppFolderRemoval = false - @State private var appRemovalSemaphore = DispatchSemaphore(value: 0) - @State private var appFolderRemovalSemaphore = DispatchSemaphore(value: 0) + @State private var appRemovalContinuation : CheckedContinuation? = nil + @State private var appFolderRemovalContinuation : CheckedContinuation? = nil @State private var renameFolderShow = false @State private var renameFolderContent = "" - @State private var renameFolerSemaphore = DispatchSemaphore(value: 0) + @State private var renameFolerContinuation : CheckedContinuation? = nil @State private var errorShow = false @State private var errorInfo = "" @@ -46,10 +45,10 @@ struct LCAppBanner : View { @State private var observer : NSKeyValueObservation? - init(appInfo: LCAppInfo, delegate: LCAppBannerDelegate, appDataFolders: [String], tweakFolders: [String]) { + init(appInfo: LCAppInfo, delegate: LCAppBannerDelegate, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>) { _appInfo = State(initialValue: appInfo) - _appDataFolders = State(initialValue: appDataFolders) - _tweakFolders = State(initialValue: tweakFolders) + _appDataFolders = appDataFolders + _tweakFolders = tweakFolders self.delegate = delegate _uiDataFolder = State(initialValue: appInfo.getDataUUIDNoAssign()) _uiTweakFolder = State(initialValue: appInfo.tweakFolder()) @@ -75,7 +74,7 @@ struct LCAppBanner : View { } Spacer() Button { - runApp() + Task{ await runApp() } } label: { if !isSingingInProgress { Text("Run").bold().foregroundColor(.white) @@ -115,7 +114,7 @@ struct LCAppBanner : View { .contextMenu{ Text(appInfo.relativeBundlePath) Button(role: .destructive) { - uninstall() + Task{ await uninstall() } } label: { Label("Uninstall", systemImage: "trash") } @@ -126,13 +125,13 @@ struct LCAppBanner : View { } Menu(content: { Button { - createFolder() + Task{ await createFolder() } } label: { Label("New data folder", systemImage: "plus") } if uiDataFolder != nil { Button { - renameDataFolder() + Task{ await renameDataFolder() } } label: { Label("Rename data folder", systemImage: "pencil") } @@ -174,13 +173,13 @@ struct LCAppBanner : View { .alert("Confirm Uninstallation", isPresented: $confirmAppRemovalShow) { Button(role: .destructive) { self.confirmAppRemoval = true - self.appRemovalSemaphore.signal() + self.appRemovalContinuation?.resume() } label: { Text("Uninstall") } Button("Cancel", role: .cancel) { self.confirmAppRemoval = false - self.appRemovalSemaphore.signal() + self.appRemovalContinuation?.resume() } } message: { Text("Are you sure you want to uninstall \(appInfo.displayName()!)?") @@ -188,13 +187,13 @@ struct LCAppBanner : View { .alert("Delete Data Folder", isPresented: $confirmAppFolderRemovalShow) { Button(role: .destructive) { self.confirmAppFolderRemoval = true - self.appFolderRemovalSemaphore.signal() + self.appFolderRemovalContinuation?.resume() } label: { Text("Delete") } Button("Cancel", role: .cancel) { self.confirmAppFolderRemoval = false - self.appFolderRemovalSemaphore.signal() + self.appFolderRemovalContinuation?.resume() } } message: { Text("Do you also want to delete data folder of \(appInfo.displayName()!)? You can keep it for future use.") @@ -206,11 +205,11 @@ struct LCAppBanner : View { placeholder: "", action: { newText in self.renameFolderContent = newText! - renameFolerSemaphore.signal() + renameFolerContinuation?.resume() }, actionCancel: {_ in self.renameFolderContent = "" - renameFolerSemaphore.signal() + renameFolerContinuation?.resume() } ) .alert("Error", isPresented: $errorShow) { @@ -223,47 +222,45 @@ struct LCAppBanner : View { } - func runApp() { + func runApp() async { isAppRunning = true - DispatchQueue.global().async { - let patchInfo = appInfo.patchExec() - if patchInfo == "SignNeeded" { - let bundlePath = URL(fileURLWithPath: appInfo.bundlePath()) - let signProgress = LCUtils.signAppBundle(bundlePath) { success, error in - self.appInfo.signCleanUp(withSuccessStatus: success) + + let patchInfo = appInfo.patchExec() + if patchInfo == "SignNeeded" { + let bundlePath = URL(fileURLWithPath: appInfo.bundlePath()) + let signProgress = LCUtils.signAppBundle(bundlePath) { success, error in + self.appInfo.signCleanUp(withSuccessStatus: success) + self.isSingingInProgress = false + if success { self.isSingingInProgress = false - if success { - self.isSingingInProgress = false - UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") - LCUtils.launchToGuestApp() - } else { - errorInfo = error != nil ? error!.localizedDescription : "Signing failed with unknown error" - errorShow = true - } - } - guard let signProgress = signProgress else { - errorInfo = "Failed to initiate signing!" + UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") + LCUtils.launchToGuestApp() + } else { + errorInfo = error != nil ? error!.localizedDescription : "Signing failed with unknown error" errorShow = true - self.isAppRunning = false - return - } - self.isSingingInProgress = true - self.observer = signProgress.observe(\.fractionCompleted) { p, v in - self.signProgress = signProgress.fractionCompleted } - } else if patchInfo != nil { - errorInfo = patchInfo! + } + guard let signProgress = signProgress else { + errorInfo = "Failed to initiate signing!" errorShow = true self.isAppRunning = false return - } else { - UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") - LCUtils.launchToGuestApp() } + self.isSingingInProgress = true + self.observer = signProgress.observe(\.fractionCompleted) { p, v in + self.signProgress = signProgress.fractionCompleted + } + } else if patchInfo != nil { + errorInfo = patchInfo! + errorShow = true self.isAppRunning = false + return + } else { + UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") + LCUtils.launchToGuestApp() } + self.isAppRunning = false - } func setDataFolder(folderName: String?) { @@ -272,60 +269,65 @@ struct LCAppBanner : View { self.uiPickerDataFolder = folderName } - func createFolder() { - DispatchQueue.global().async { - self.renameFolderContent = NSUUID().uuidString + func createFolder() async { + + self.renameFolderContent = NSUUID().uuidString + + await withCheckedContinuation { c in + self.renameFolerContinuation = c self.renameFolderShow = true - self.renameFolerSemaphore.wait() - if self.renameFolderContent == "" { - return - } - let fm = FileManager() - let dest = self.delegate.getDocPath().appendingPathComponent("Data/Application").appendingPathComponent(self.renameFolderContent) - do { - try fm.createDirectory(at: dest, withIntermediateDirectories: false) - } catch { - errorShow = true - errorInfo = error.localizedDescription - return - } - - self.appDataFolders.append(self.renameFolderContent) - self.setDataFolder(folderName: self.renameFolderContent) } + + if self.renameFolderContent == "" { + return + } + let fm = FileManager() + let dest = LCPath.dataPath.appendingPathComponent(self.renameFolderContent) + do { + try fm.createDirectory(at: dest, withIntermediateDirectories: false) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + + self.appDataFolders.append(self.renameFolderContent) + self.setDataFolder(folderName: self.renameFolderContent) + } - func renameDataFolder() { + func renameDataFolder() async { if self.appInfo.getDataUUIDNoAssign() == nil { return } - DispatchQueue.global().async { - self.renameFolderContent = self.uiDataFolder == nil ? "" : self.uiDataFolder! + self.renameFolderContent = self.uiDataFolder == nil ? "" : self.uiDataFolder! + await withCheckedContinuation { c in + self.renameFolerContinuation = c self.renameFolderShow = true - self.renameFolerSemaphore.wait() - if self.renameFolderContent == "" { - return - } - let fm = FileManager() - let orig = self.delegate.getDocPath().appendingPathComponent("Data/Application").appendingPathComponent(appInfo.getDataUUIDNoAssign()) - let dest = self.delegate.getDocPath().appendingPathComponent("Data/Application").appendingPathComponent(self.renameFolderContent) - do { - try fm.moveItem(at: orig, to: dest) - } catch { - errorShow = true - errorInfo = error.localizedDescription - return - } - - let i = self.appDataFolders.firstIndex(of: self.appInfo.getDataUUIDNoAssign()) - guard let i = i else { - return - } - - self.appDataFolders[i] = self.renameFolderContent - self.setDataFolder(folderName: self.renameFolderContent) } + if self.renameFolderContent == "" { + return + } + let fm = FileManager() + let orig = LCPath.dataPath.appendingPathComponent(appInfo.getDataUUIDNoAssign()) + let dest = LCPath.dataPath.appendingPathComponent(self.renameFolderContent) + do { + try fm.moveItem(at: orig, to: dest) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + + let i = self.appDataFolders.firstIndex(of: self.appInfo.getDataUUIDNoAssign()) + guard let i = i else { + return + } + + self.appDataFolders[i] = self.renameFolderContent + self.setDataFolder(folderName: self.renameFolderContent) + } func setTweakFolder(folderName: String?) { @@ -334,39 +336,51 @@ struct LCAppBanner : View { self.uiPickerTweakFolder = folderName } - func uninstall() { - // Add delay because of https://stackoverflow.com/questions/60358948/swiftui-delete-row-in-list-with-context-menu-ui-glitch - DispatchQueue.global().asyncAfter(deadline: .now() + 0.1) { - do { + func uninstall() async { + do { + await withCheckedContinuation { c in + self.appRemovalContinuation = c self.confirmAppRemovalShow = true; - self.appRemovalSemaphore.wait() - if !self.confirmAppRemoval { - return - } - if self.appInfo.getDataUUIDNoAssign() != nil { + } + + if !self.confirmAppRemoval { + return + } + if self.appInfo.getDataUUIDNoAssign() != nil { + self.confirmAppFolderRemovalShow = true; + await withCheckedContinuation { c in + self.appFolderRemovalContinuation = c self.confirmAppFolderRemovalShow = true; - self.appFolderRemovalSemaphore.wait() - } else { - self.confirmAppFolderRemoval = false; } - - + } else { + self.confirmAppFolderRemoval = false; + } + + + let fm = FileManager() + try fm.removeItem(atPath: self.appInfo.bundlePath()!) + self.delegate.removeApp(app: self.appInfo) + if self.confirmAppFolderRemoval { let fm = FileManager() - try fm.removeItem(atPath: self.appInfo.bundlePath()!) - self.delegate.removeApp(app: self.appInfo) - if self.confirmAppFolderRemoval { - let fm = FileManager() - let dataFolderPath = self.delegate.getDocPath().appendingPathComponent("Data/Application").appendingPathComponent(appInfo.dataUUID()!) - try fm.removeItem(at: dataFolderPath) - } + let dataUUID = appInfo.dataUUID()! + let dataFolderPath = LCPath.dataPath.appendingPathComponent(dataUUID) +// try fm.removeItem(at: dataFolderPath) - } catch { - errorShow = true - errorInfo = error.localizedDescription - + DispatchQueue.main.async { + self.appDataFolders.removeAll(where: { f in + NSLog("[NMSL] \(f) vs \(dataUUID)") + return f == dataUUID + }) + } } + + } catch { + errorShow = true + errorInfo = error.localizedDescription + } + } - } + } diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 8f92486..6063906 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -15,13 +15,9 @@ struct AppReplaceOption : Hashable { } struct LCAppListView : View, LCAppBannerDelegate { - private var docPath: URL - var bundlePath: URL - var dataPath: URL - var tweakPath: URL - @State var apps: [LCAppInfo] - @State var appDataFolderNames: [String] - @State var tweakFolderNames: [String] + @Binding var apps: [LCAppInfo] + @Binding var appDataFolderNames: [String] + @Binding var tweakFolderNames: [String] @State var didAppear = false // ipa choosing stuff @@ -38,66 +34,20 @@ struct LCAppListView : View, LCAppBannerDelegate { @State var installReplaceComfirmVisible = false @State var installOptions: [AppReplaceOption] @State var installOptionChosen: AppReplaceOption? - @State var installOptionSemaphore = DispatchSemaphore(value: 0) + @State var installOptionContinuation : CheckedContinuation? = nil @State var webViewOpened = false @State var webViewURL : URL = URL(string: "https://www.google.com")! @State private var webViewUrlInputOpened = false @State private var webViewUrlInputContent = "" - @State private var webViewUrlInputSemaphore = DispatchSemaphore(value: 0) + @State private var webViewUrlInputContinuation : CheckedContinuation? = nil - init() { - let fm = FileManager() - self.docPath = fm.urls(for: .documentDirectory, in: .userDomainMask).last! - self.bundlePath = self.docPath.appendingPathComponent("Applications") - self.dataPath = self.docPath.appendingPathComponent("Data/Application") - self.tweakPath = self.docPath.appendingPathComponent("Tweaks") + init(apps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>, tweakFolderNames: Binding<[String]>) { _installOptions = State(initialValue: []) _installOptionChosen = State(initialValue: nil) - var tempAppDataFolderNames : [String] = [] - var tempTweakFolderNames : [String] = [] - - var tempApps: [LCAppInfo] = [] - - do { - // load apps - try fm.createDirectory(at: self.bundlePath, withIntermediateDirectories: true) - let appDirs = try fm.contentsOfDirectory(atPath: self.bundlePath.path) - for appDir in appDirs { - if !appDir.hasSuffix(".app") { - continue - } - let newApp = LCAppInfo(bundlePath: "\(self.bundlePath.path)/\(appDir)")! - newApp.relativeBundlePath = appDir - tempApps.append(newApp) - } - // load document folders - try fm.createDirectory(at: self.dataPath, withIntermediateDirectories: true) - let dataDirs = try fm.contentsOfDirectory(atPath: self.dataPath.path) - for dataDir in dataDirs { - let dataDirUrl = self.dataPath.appendingPathComponent(dataDir) - if !dataDirUrl.hasDirectoryPath { - continue - } - tempAppDataFolderNames.append(dataDir) - } - - // load tweak folders - try fm.createDirectory(at: self.tweakPath, withIntermediateDirectories: true) - let tweakDirs = try fm.contentsOfDirectory(atPath: self.tweakPath.path) - for tweakDir in tweakDirs { - let tweakDirUrl = self.tweakPath.appendingPathComponent(tweakDir) - if !tweakDirUrl.hasDirectoryPath { - continue - } - tempTweakFolderNames.append(tweakDir) - } - } catch { - NSLog("[LC] error:\(error)") - } - _apps = State(initialValue: tempApps) - _appDataFolderNames = State(initialValue: tempAppDataFolderNames) - _tweakFolderNames = State(initialValue: tempTweakFolderNames) + _apps = apps + _appDataFolderNames = appDataFolderNames + _tweakFolderNames = tweakFolderNames } var body: some View { @@ -107,7 +57,7 @@ struct LCAppListView : View, LCAppBannerDelegate { Section { LazyVStack { ForEach(apps, id: \.self) { app in - LCAppBanner(appInfo: app, delegate: self, appDataFolders: appDataFolderNames, tweakFolders: tweakFolderNames) + LCAppBanner(appInfo: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) } .transition(.scale) } @@ -164,26 +114,26 @@ struct LCAppListView : View, LCAppBannerDelegate { } ToolbarItem(placement: .topBarTrailing) { Button("Open Link", systemImage: "link", action: { - onOpenWebViewTapped() + Task { await onOpenWebViewTapped() } }) } } } - + .navigationViewStyle(StackNavigationViewStyle()) .alert(isPresented: $errorShow){ Alert(title: Text("Error"), message: Text(errorInfo)) } .fileImporter(isPresented: $choosingIPA, allowedContentTypes: [UTType(filenameExtension: "ipa")!]) { result in - startInstallApp(result) + Task { await startInstallApp(result) } } .alert("Installation", isPresented: $installReplaceComfirmVisible) { ForEach(installOptions, id: \.self) { installOption in Button(role: installOption.isReplace ? .destructive : nil, action: { self.installOptionChosen = installOption - self.installOptionSemaphore.signal() + self.installOptionContinuation?.resume() }, label: { Text(installOption.isReplace ? installOption.nameOfFolderToInstall : "Install as new") }) @@ -191,7 +141,7 @@ struct LCAppListView : View, LCAppBannerDelegate { } Button(role: .cancel, action: { self.installOptionChosen = nil - self.installOptionSemaphore.signal() + self.installOptionContinuation?.resume() }, label: { Text("Abort Installation") }) @@ -205,11 +155,11 @@ struct LCAppListView : View, LCAppBannerDelegate { placeholder: "scheme://", action: { newText in self.webViewUrlInputContent = newText! - webViewUrlInputSemaphore.signal() + webViewUrlInputContinuation?.resume() }, actionCancel: {_ in self.webViewUrlInputContent = "" - webViewUrlInputSemaphore.signal() + webViewUrlInputContinuation?.resume() } ) .fullScreenCover(isPresented: $webViewOpened) { @@ -218,16 +168,17 @@ struct LCAppListView : View, LCAppBannerDelegate { } - func onOpenWebViewTapped() { - DispatchQueue.global().async { + func onOpenWebViewTapped() async { + await withCheckedContinuation { c in webViewUrlInputOpened = true - webViewUrlInputSemaphore.wait() + webViewUrlInputContinuation = c + } if webViewUrlInputContent == "" { return } openWebView(urlString: webViewUrlInputContent) webViewUrlInputContent = "" - } + } func checkIfAppDelegateNeedOpenWebPage() { @@ -290,23 +241,20 @@ struct LCAppListView : View, LCAppBannerDelegate { - func startInstallApp(_ result:Result) { - DispatchQueue.global().async { - do { - let fileUrl = try result.get() - self.installprogressVisible = true - try installIpaFile(fileUrl) - } catch { - errorInfo = error.localizedDescription - errorShow = true - self.installprogressVisible = false - } + func startInstallApp(_ result:Result) async { + do { + let fileUrl = try result.get() + self.installprogressVisible = true + try await installIpaFile(fileUrl) + } catch { + errorInfo = error.localizedDescription + errorShow = true + self.installprogressVisible = false } - } - func installIpaFile(_ url:URL) throws { + func installIpaFile(_ url:URL) async throws { if(!url.startAccessingSecurityScopedResource()) { throw "Failed to access IPA"; } @@ -337,7 +285,7 @@ struct LCAppListView : View, LCAppBannerDelegate { } var appRelativePath = "\(newAppInfo.bundleIdentifier()!).app" - var outputFolder = self.bundlePath.appendingPathComponent(appRelativePath) + var outputFolder = LCPath.bundlePath.appendingPathComponent(appRelativePath) var appToReplace : LCAppInfo? = nil // Folder exist! show alert for user to choose which bundle to replace let sameBundleIdApp = self.apps.filter { app in @@ -351,8 +299,12 @@ struct LCAppListView : View, LCAppBannerDelegate { for app in sameBundleIdApp { self.installOptions.append(AppReplaceOption(isReplace: true, nameOfFolderToInstall: app.relativeBundlePath, appToReplace: app)) } - self.installReplaceComfirmVisible = true - self.installOptionSemaphore.wait() + + await withCheckedContinuation { c in + self.installOptionContinuation = c + self.installReplaceComfirmVisible = true + } + // user cancelled guard let installOptionChosen = self.installOptionChosen else { @@ -361,7 +313,7 @@ struct LCAppListView : View, LCAppBannerDelegate { return } - outputFolder = self.bundlePath.appendingPathComponent(installOptionChosen.nameOfFolderToInstall) + outputFolder = LCPath.bundlePath.appendingPathComponent(installOptionChosen.nameOfFolderToInstall) appToReplace = installOptionChosen.appToReplace if installOptionChosen.isReplace { try fm.removeItem(at: outputFolder) @@ -382,16 +334,16 @@ struct LCAppListView : View, LCAppBannerDelegate { } if patchResult == "SignNeeded" { // sign it - let signSemaphore = DispatchSemaphore(value: 0) var error : Error? = nil var success = false - let signProgress = LCUtils.signAppBundle(outputFolder) { success1, error1 in - error = error1 - success = success1 - signSemaphore.signal() + await withCheckedContinuation { c in + let signProgress = LCUtils.signAppBundle(outputFolder) { success1, error1 in + error = error1 + success = success1 + c.resume() + } + installProgress.addChild(signProgress!, withPendingUnitCount: 20) } - installProgress.addChild(signProgress!, withPendingUnitCount: 20) - signSemaphore.wait() if let error = error { finalNewApp?.signCleanUp(withSuccessStatus: false) @@ -413,17 +365,10 @@ struct LCAppListView : View, LCAppBannerDelegate { } func removeApp(app: LCAppInfo) { - self.apps.removeAll { now in - return app == now + DispatchQueue.main.async { + self.apps.removeAll { now in + return app == now + } } } - - func getDocPath() -> URL { - return self.docPath - } - -} - -#Preview { - LCAppListView() } diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index bc891d5..f1e0192 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -12,18 +12,29 @@ struct LCSettingsView: View { @State var errorShow = false @State var errorInfo = "" + @Binding var apps: [LCAppInfo] + @Binding var appDataFolderNames: [String] + + @State private var confirmAppFolderRemovalShow = false + @State private var confirmAppFolderRemoval = false + @State private var appFolderRemovalContinuation : CheckedContinuation? = nil + @State private var folderRemoveCount = 0 + @State var isJitLessEnabled = false @State var isAltCertIgnored = false @State var frameShortIcon = false @State var silentSwitchApp = false @State var injectToLCItelf = false - init() { + init(apps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>) { _isJitLessEnabled = State(initialValue: LCUtils.certificatePassword() != nil) _isAltCertIgnored = State(initialValue: UserDefaults.standard.bool(forKey: "LCIgnoreALTCertificate")) _frameShortIcon = State(initialValue: UserDefaults.standard.bool(forKey: "LCFrameShortcutIcons")) _silentSwitchApp = State(initialValue: UserDefaults.standard.bool(forKey: "LCSwitchAppWithoutAsking")) _injectToLCItelf = State(initialValue: UserDefaults.standard.bool(forKey: "LCLoadTweaksToSelf")) + + _apps = apps + _appDataFolderNames = appDataFolderNames } var body: some View { @@ -80,6 +91,14 @@ struct LCSettingsView: View { Text("Place your tweaks into the global “Tweaks” folder and LiveContainer will pick them up.") } + Section { + Button(role:.destructive) { + Task { await cleanUpUnusedFolders() } + } label: { + Text("Clean Unused Data Folders") + } + } + VStack{ Text(LCUtils.getVersionInfo()) .foregroundStyle(.gray) @@ -92,6 +111,28 @@ struct LCSettingsView: View { .alert(isPresented: $errorShow){ Alert(title: Text("Error"), message: Text(errorInfo)) } + .alert("Data Folder Clean Up", isPresented: $confirmAppFolderRemovalShow) { + if folderRemoveCount > 0 { + Button(role: .destructive) { + self.confirmAppFolderRemoval = true + self.appFolderRemovalContinuation?.resume() + } label: { + Text("Delete") + } + } + + Button("Cancel", role: .cancel) { + self.confirmAppFolderRemoval = false + self.appFolderRemovalContinuation?.resume() + } + } message: { + if folderRemoveCount > 0 { + Text("Do you want to delete \(folderRemoveCount) unused data folder(s)?") + } else { + Text("No data folder to remove. All data folders are in use.") + } + + } .onChange(of: isAltCertIgnored) { newValue in saveItem(key: "LCIgnoreALTCertificate", val: newValue) @@ -106,6 +147,7 @@ struct LCSettingsView: View { saveItem(key: "LCLoadTweaksToSelf", val: newValue) } } + .navigationViewStyle(StackNavigationViewStyle()) } @@ -129,5 +171,46 @@ struct LCSettingsView: View { } } + + func cleanUpUnusedFolders() async { + + var folderNameToAppDict : [String:LCAppInfo] = [:] + for app in apps { + guard let folderName = app.getDataUUIDNoAssign() else { + continue + } + folderNameToAppDict[folderName] = app + } + + var foldersToDelete : [String] = [] + for appDataFolderName in appDataFolderNames { + if folderNameToAppDict[appDataFolderName] == nil { + foldersToDelete.append(appDataFolderName) + } + } + folderRemoveCount = foldersToDelete.count + await withCheckedContinuation { c in + self.appFolderRemovalContinuation = c + DispatchQueue.main.async { + confirmAppFolderRemovalShow = true + } + } + if !confirmAppFolderRemoval { + return + } + do { + let fm = FileManager() + for folder in foldersToDelete { + try fm.removeItem(at: LCPath.dataPath.appendingPathComponent(folder)) + self.appDataFolderNames.removeAll(where: { s in + return s == folder + }) + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + + } } diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index d300a2f..615f282 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -9,22 +9,76 @@ import Foundation import SwiftUI struct LCTabView: View { + @State var apps: [LCAppInfo] + @State var appDataFolderNames: [String] + @State var tweakFolderNames: [String] + @State var errorShow = false @State var errorInfo = "" + init() { + let fm = FileManager() + var tempAppDataFolderNames : [String] = [] + var tempTweakFolderNames : [String] = [] + + var tempApps: [LCAppInfo] = [] + + do { + // load apps + try fm.createDirectory(at: LCPath.bundlePath, withIntermediateDirectories: true) + let appDirs = try fm.contentsOfDirectory(atPath: LCPath.bundlePath.path) + for appDir in appDirs { + if !appDir.hasSuffix(".app") { + continue + } + let newApp = LCAppInfo(bundlePath: "\(LCPath.bundlePath.path)/\(appDir)")! + newApp.relativeBundlePath = appDir + tempApps.append(newApp) + } + // load document folders + try fm.createDirectory(at: LCPath.dataPath, withIntermediateDirectories: true) + let dataDirs = try fm.contentsOfDirectory(atPath: LCPath.dataPath.path) + for dataDir in dataDirs { + let dataDirUrl = LCPath.dataPath.appendingPathComponent(dataDir) + if !dataDirUrl.hasDirectoryPath { + continue + } + tempAppDataFolderNames.append(dataDir) + } + + // load tweak folders + try fm.createDirectory(at: LCPath.tweakPath, withIntermediateDirectories: true) + let tweakDirs = try fm.contentsOfDirectory(atPath: LCPath.tweakPath.path) + for tweakDir in tweakDirs { + let tweakDirUrl = LCPath.tweakPath.appendingPathComponent(tweakDir) + if !tweakDirUrl.hasDirectoryPath { + continue + } + tempTweakFolderNames.append(tweakDir) + } + } catch { + NSLog("[LC] error:\(error)") + } + _apps = State(initialValue: tempApps) + _appDataFolderNames = State(initialValue: tempAppDataFolderNames) + _tweakFolderNames = State(initialValue: tempTweakFolderNames) + } + var body: some View { TabView { - LCAppListView() + LCAppListView(apps: $apps, appDataFolderNames: $appDataFolderNames, tweakFolderNames: $tweakFolderNames) .tabItem { Label("Apps", systemImage: "square.stack.3d.up.fill") } - LCSettingsView() + LCSettingsView(apps: $apps, appDataFolderNames: $appDataFolderNames) .tabItem { Label("Settings", systemImage: "gearshape.fill") } } .alert(isPresented: $errorShow){ Alert(title: Text("Error"), message: Text(errorInfo)) + }.onAppear() { + checkLastLaunchError() } } diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index 7a08dfa..893cc4c 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -24,7 +24,7 @@ struct LCWebView: View { @State private var runAppAlertMsg = "" @State private var doRunApp = false @State private var renameFolderContent = "" - @State private var doRunAppSemaphore = DispatchSemaphore(value: 0) + @State private var doRunAppContinuation : CheckedContinuation? = nil @State private var errorShow = false @State private var errorInfo = "" @@ -101,11 +101,11 @@ struct LCWebView: View { .alert("Run App", isPresented: $runAppAlertShow) { Button("Run", action: { self.doRunApp = true - self.doRunAppSemaphore.signal() + self.doRunAppContinuation?.resume() }) Button("Cancel", role: .cancel, action: { self.doRunApp = false - self.doRunAppSemaphore.signal() + self.doRunAppContinuation?.resume() }) } message: { Text(runAppAlertMsg) @@ -126,68 +126,71 @@ struct LCWebView: View { webView.setObserver(observer: observer) } - public func onURLSchemeDetected(url: URL) { - DispatchQueue.global().async { - var appToLaunch : LCAppInfo? = nil - appLoop: for app in apps { - if let schemes = app.urlSchemes() { - for scheme in schemes { - if let scheme = scheme as? String, scheme == url.scheme { - appToLaunch = app - break appLoop - } - } + public func onURLSchemeDetected(url: URL) async { + var appToLaunch : LCAppInfo? = nil + appLoop: for app in apps { + if let schemes = app.urlSchemes() { + for scheme in schemes { + if let scheme = scheme as? String, scheme == url.scheme { + appToLaunch = app + break appLoop } } - - guard let appToLaunch = appToLaunch else { - errorInfo = "Scheme \"\(url.scheme!)\" cannot be opened by any app installed in LiveContainer." - errorShow = true - return - } - - runAppAlertMsg = "This web page is trying to launch \"\(appToLaunch.displayName()!)\", continue?" + } + } + + guard let appToLaunch = appToLaunch else { + errorInfo = "Scheme \"\(url.scheme!)\" cannot be opened by any app installed in LiveContainer." + errorShow = true + return + } + + runAppAlertMsg = "This web page is trying to launch \"\(appToLaunch.displayName()!)\", continue?" + + await withCheckedContinuation { c in + self.doRunAppContinuation = c runAppAlertShow = true - self.doRunAppSemaphore.wait() - if !doRunApp { - return - } - - UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") - UserDefaults.standard.setValue(url.absoluteString, forKey: "launchAppUrlScheme") - LCUtils.launchToGuestApp() } + + if !doRunApp { + return + } + + UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") + UserDefaults.standard.setValue(url.absoluteString, forKey: "launchAppUrlScheme") + LCUtils.launchToGuestApp() + } - public func onUniversalLinkDetected(url: URL, bundleIDs: [String]) { - DispatchQueue.global().async { - var bundleIDToAppDict: [String: LCAppInfo] = [:] - for app in apps { - bundleIDToAppDict[app.bundleIdentifier()!] = app - } - - var appToLaunch: LCAppInfo? = nil - for bundleID in bundleIDs { - if let app = bundleIDToAppDict[bundleID] { - appToLaunch = app - break - } - } - guard let appToLaunch = appToLaunch else { - return + public func onUniversalLinkDetected(url: URL, bundleIDs: [String]) async { + var bundleIDToAppDict: [String: LCAppInfo] = [:] + for app in apps { + bundleIDToAppDict[app.bundleIdentifier()!] = app + } + + var appToLaunch: LCAppInfo? = nil + for bundleID in bundleIDs { + if let app = bundleIDToAppDict[bundleID] { + appToLaunch = app + break } - - runAppAlertMsg = "This web page can be opened in \"\(appToLaunch.displayName()!)\" according to its Associated Domains, continue?" + } + guard let appToLaunch = appToLaunch else { + return + } + + runAppAlertMsg = "This web page can be opened in \"\(appToLaunch.displayName()!)\" according to its Associated Domains, continue?" + runAppAlertShow = true + await withCheckedContinuation { c in + self.doRunAppContinuation = c runAppAlertShow = true - self.doRunAppSemaphore.wait() - if !doRunApp { - return - } - UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") - UserDefaults.standard.setValue(url.absoluteString, forKey: "launchAppUrlScheme") - LCUtils.launchToGuestApp() } - + if !doRunApp { + return + } + UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") + UserDefaults.standard.setValue(url.absoluteString, forKey: "launchAppUrlScheme") + LCUtils.launchToGuestApp() } } @@ -213,11 +216,11 @@ class WebViewLoadObserver : NSObject { class WebViewDelegate : NSObject,WKNavigationDelegate { private var pageTitle: Binding - private var urlSchemeHandler: (URL) -> Void - private var universalLinkHandler: (URL , [String]) -> Void // url, [String] of all apps that can open this web page + private var urlSchemeHandler: (URL) async -> Void + private var universalLinkHandler: (URL , [String]) async -> Void // url, [String] of all apps that can open this web page var domainBundleIdDict : [String:[String]] = [:] - init(pageTitle: Binding, urlSchemeHandler: @escaping (URL) -> Void, universalLinkHandler: @escaping (URL , [String]) -> Void) { + init(pageTitle: Binding, urlSchemeHandler: @escaping (URL) async -> Void, universalLinkHandler: @escaping (URL , [String]) async -> Void) { self.pageTitle = pageTitle self.urlSchemeHandler = urlSchemeHandler self.universalLinkHandler = universalLinkHandler @@ -232,19 +235,18 @@ class WebViewDelegate : NSObject,WKNavigationDelegate { return } if(scheme == "https") { - DispatchQueue.global().async { - self.loadDomainAssociations(url: url) + Task { + await self.loadDomainAssociations(url: url) if let host = url.host, let appIDs = self.domainBundleIdDict[host] { - self.universalLinkHandler(url, appIDs) + Task{ await self.universalLinkHandler(url, appIDs) } } - } return } if(scheme == "http" || scheme == "about" || scheme == "itms-appss") { return; } - urlSchemeHandler(url) + Task{ await urlSchemeHandler(url) } } @@ -253,7 +255,7 @@ class WebViewDelegate : NSObject,WKNavigationDelegate { } - func loadDomainAssociations(url: URL) { + func loadDomainAssociations(url: URL) async { if url.scheme != "https" || url.host == nil { return } @@ -270,32 +272,39 @@ class WebViewDelegate : NSObject,WKNavigationDelegate { URL(string: "https://\(host)/apple-app-site-association")!, URL(string: "https://\(host)/.well-known/apple-app-site-association")! ] - for siteAssociationURL in appleAppSiteAssociationURLs { - let task = URLSession.shared.dataTask(with: siteAssociationURL) { data, response, error in - do { - guard let data = data else { - loadSemaphore.signal() - return - } - let siteAssociationObj = try JSONDecoder().decode(SiteAssociation.self, from: data) - guard let detailItems = siteAssociationObj.applinks?.details else { - loadSemaphore.signal() - return - } - self.domainBundleIdDict[host] = [] - for item in detailItems { - self.domainBundleIdDict[host]!.append(contentsOf: item.getBundleIds()) - } - } catch { + + let tasks : [String] = [] + await withTaskGroup(of: Void.self) { group in + for siteAssociationURL in appleAppSiteAssociationURLs { + group.addTask { + await withCheckedContinuation { c in + let task = URLSession.shared.dataTask(with: siteAssociationURL) { data, response, error in + do { + guard let data = data else { + c.resume() + return + } + let siteAssociationObj = try JSONDecoder().decode(SiteAssociation.self, from: data) + guard let detailItems = siteAssociationObj.applinks?.details else { + c.resume() + return + } + self.domainBundleIdDict[host] = [] + for item in detailItems { + self.domainBundleIdDict[host]!.append(contentsOf: item.getBundleIds()) + } + } catch { + + } + c.resume() + } + task.resume() + } } - loadSemaphore.signal() } - task.resume() - } - for siteAssociationURL in appleAppSiteAssociationURLs { - loadSemaphore.wait() } + } } diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 6149560..1377e46 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -7,6 +7,16 @@ import SwiftUI +struct LCPath { + public static let docPath = { + let fm = FileManager() + return fm.urls(for: .documentDirectory, in: .userDomainMask).last! + }() + public static let bundlePath = docPath.appendingPathComponent("Applications") + public static let dataPath = docPath.appendingPathComponent("Data/Application") + public static let tweakPath = docPath.appendingPathComponent("Tweaks") +} + extension String: LocalizedError { public var errorDescription: String? { return self } } From cbc1af0056df09af288e185ce518dc7b0791804c Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 29 Aug 2024 00:35:56 +0800 Subject: [PATCH 10/36] tweak view --- LiveContainerSwiftUI/LCAppBanner.swift | 4 +- LiveContainerSwiftUI/LCTabView.swift | 11 +- LiveContainerSwiftUI/LCTweaksView.swift | 191 ++++++++++++++++++++++++ LiveContainerSwiftUI/LCWebView.swift | 2 - 4 files changed, 197 insertions(+), 11 deletions(-) diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index bd33f08..0a5eba6 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -361,14 +361,12 @@ struct LCAppBanner : View { try fm.removeItem(atPath: self.appInfo.bundlePath()!) self.delegate.removeApp(app: self.appInfo) if self.confirmAppFolderRemoval { - let fm = FileManager() let dataUUID = appInfo.dataUUID()! let dataFolderPath = LCPath.dataPath.appendingPathComponent(dataUUID) -// try fm.removeItem(at: dataFolderPath) + try fm.removeItem(at: dataFolderPath) DispatchQueue.main.async { self.appDataFolders.removeAll(where: { f in - NSLog("[NMSL] \(f) vs \(dataUUID)") return f == dataUUID }) } diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index 615f282..583c5d7 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -70,6 +70,11 @@ struct LCTabView: View { .tabItem { Label("Apps", systemImage: "square.stack.3d.up.fill") } + LCTweaksView(tweakFolders: $tweakFolderNames) + .tabItem{ + Label("Tweaks", systemImage: "wrench.and.screwdriver") + } + LCSettingsView(apps: $apps, appDataFolderNames: $appDataFolderNames) .tabItem { Label("Settings", systemImage: "gearshape.fill") @@ -91,9 +96,3 @@ struct LCTabView: View { errorShow = true } } - - - -#Preview { - LCTabView() -} diff --git a/LiveContainerSwiftUI/LCTweaksView.swift b/LiveContainerSwiftUI/LCTweaksView.swift index c86b632..f32fe56 100644 --- a/LiveContainerSwiftUI/LCTweaksView.swift +++ b/LiveContainerSwiftUI/LCTweaksView.swift @@ -6,3 +6,194 @@ // import Foundation +import SwiftUI + +struct LCTweakItem : Hashable { + let fileUrl: URL + let isFolder: Bool + let isFramework: Bool + let isTweak: Bool +} + +struct LCTweakFolderView : View { + @State var baseUrl : URL + @State var tweakItems : [LCTweakItem] + private var isRoot : Bool + @Binding var tweakFolders : [String] + + @State private var errorShow = false + @State private var errorInfo = "" + + @State private var newFolderShow = false + @State private var newFolderContent = "" + @State private var newFolerContinuation : CheckedContinuation? = nil + + init(baseUrl: URL, isRoot: Bool = false, tweakFolders: Binding<[String]>) { + _baseUrl = State(initialValue: baseUrl) + _tweakFolders = tweakFolders + self.isRoot = isRoot + var tmpTweakItems : [LCTweakItem] = [] + let fm = FileManager() + do { + let files = try fm.contentsOfDirectory(atPath: baseUrl.path) + for fileName in files { + let fileUrl = baseUrl.appendingPathComponent(fileName) + var isFolder : ObjCBool = false + fm.fileExists(atPath: fileUrl.path, isDirectory: &isFolder) + let isFramework = isFolder.boolValue && fileUrl.lastPathComponent.hasSuffix(".framework") + let isTweak = !isFolder.boolValue && fileUrl.lastPathComponent.hasSuffix(".dylib") + tmpTweakItems.append(LCTweakItem(fileUrl: fileUrl, isFolder: isFolder.boolValue, isFramework: isFramework, isTweak: isTweak)) + } + _tweakItems = State(initialValue: tmpTweakItems) + } catch { + NSLog("[LC] failed to load tweaks \(error.localizedDescription)") + _errorShow = State(initialValue: true) + _errorInfo = State(initialValue: error.localizedDescription) + _tweakItems = State(initialValue: []) + } + + } + + var body: some View { + List { + ForEach($tweakItems, id:\.self) { tweakItem in + let tweakItem = tweakItem.wrappedValue + if tweakItem.isFramework { + if #available(iOS 17.0, *) { + Label(tweakItem.fileUrl.lastPathComponent, systemImage: "duffle.bag.fill") + } else { + Label(tweakItem.fileUrl.lastPathComponent, systemImage: "shippingbox.fill") + } + } else if tweakItem.isFolder { + NavigationLink { + LCTweakFolderView(baseUrl: tweakItem.fileUrl, isRoot: false, tweakFolders: $tweakFolders) + } label: { + Label(tweakItem.fileUrl.lastPathComponent, systemImage: "folder.fill") + } + } else if tweakItem.isTweak { + Label(tweakItem.fileUrl.lastPathComponent, systemImage: "building.columns.fill") + } else { + Label(tweakItem.fileUrl.lastPathComponent, systemImage: "document.fill") + } + }.onDelete { indexSet in + deleteTweakItem(indexSet: indexSet) + } + } + .navigationTitle(baseUrl.lastPathComponent) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + + } label: { + Label("sign", systemImage: "signature") + } + } + ToolbarItem(placement: .topBarTrailing) { + Menu { + Button { + Task { await createNewFolder() } + } label: { + Label("New folder", systemImage: "folder.badge.plus") + } + + Button { + + } label: { + Label("Import Tweak", systemImage: "square.and.arrow.down") + } + } label: { + Label("add", systemImage: "plus") + } + } + } + .alert("Error", isPresented: $errorShow) { + Button("OK", action: { + }) + } message: { + Text(errorInfo) + } + .textFieldAlert( + isPresented: $newFolderShow, + title: "Enter the name of new folder", + text: $newFolderContent, + placeholder: "", + action: { newText in + self.newFolderContent = newText! + newFolerContinuation?.resume() + }, + actionCancel: {_ in + self.newFolderContent = "" + newFolerContinuation?.resume() + } + ) + .alert("Error", isPresented: $errorShow) { + Button("OK", action: { + }) + } message: { + Text(errorInfo) + } + } + + func deleteTweakItem(indexSet: IndexSet) { + var indexToRemove : [Int] = [] + let fm = FileManager() + do { + for i in indexSet { + let tweakItem = tweakItems[i] + try fm.removeItem(at: tweakItem.fileUrl) + indexToRemove.append(i) + } + } catch { + errorShow = true + errorInfo = error.localizedDescription + } + if isRoot { + for iToRemove in indexToRemove { + tweakFolders.removeAll(where: { s in + return s == tweakItems[iToRemove].fileUrl.lastPathComponent + }) + } + } + + tweakItems.remove(atOffsets: IndexSet(indexToRemove)) + } + + func createNewFolder() async { + self.newFolderContent = "" + + await withCheckedContinuation { c in + self.newFolerContinuation = c + self.newFolderShow = true + } + + if self.newFolderContent == "" { + return + } + let fm = FileManager() + let dest = baseUrl.appendingPathComponent(self.newFolderContent) + do { + try fm.createDirectory(at: dest, withIntermediateDirectories: false) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + tweakItems.append(LCTweakItem(fileUrl: dest, isFolder: true, isFramework: false, isTweak: false)) + if isRoot { + tweakFolders.append(self.newFolderContent) + } + + + } +} + +struct LCTweaksView: View { + @Binding var tweakFolders : [String] + + var body: some View { + NavigationView { + LCTweakFolderView(baseUrl: LCPath.tweakPath, isRoot: true, tweakFolders: $tweakFolders) + } + + } +} diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index 893cc4c..f5b23cd 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -266,14 +266,12 @@ class WebViewDelegate : NSObject,WKNavigationDelegate { return } - let loadSemaphore = DispatchSemaphore(value: 0) // download and read apple-app-site-association let appleAppSiteAssociationURLs = [ URL(string: "https://\(host)/apple-app-site-association")!, URL(string: "https://\(host)/.well-known/apple-app-site-association")! ] - let tasks : [String] = [] await withTaskGroup(of: Void.self) { group in for siteAssociationURL in appleAppSiteAssociationURLs { group.addTask { From 624c2d6e770a660f166940cb68b080e5b2c21717 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 29 Aug 2024 13:47:27 +0800 Subject: [PATCH 11/36] install tweak --- LiveContainerSwiftUI/LCAppListView.swift | 3 +- LiveContainerSwiftUI/LCTweaksView.swift | 250 ++++++++++++++++++++--- LiveContainerSwiftUI/Shared.swift | 60 ++++++ 3 files changed, 284 insertions(+), 29 deletions(-) diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 6063906..1880e5b 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -125,9 +125,8 @@ struct LCAppListView : View, LCAppBannerDelegate { .alert(isPresented: $errorShow){ Alert(title: Text("Error"), message: Text(errorInfo)) } - .fileImporter(isPresented: $choosingIPA, allowedContentTypes: [UTType(filenameExtension: "ipa")!]) { result in + .fileImporter(isPresented: $choosingIPA, allowedContentTypes: [.ipa]) { result in Task { await startInstallApp(result) } - } .alert("Installation", isPresented: $installReplaceComfirmVisible) { ForEach(installOptions, id: \.self) { installOption in diff --git a/LiveContainerSwiftUI/LCTweaksView.swift b/LiveContainerSwiftUI/LCTweaksView.swift index f32fe56..b601ddf 100644 --- a/LiveContainerSwiftUI/LCTweaksView.swift +++ b/LiveContainerSwiftUI/LCTweaksView.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import UniformTypeIdentifiers struct LCTweakItem : Hashable { let fileUrl: URL @@ -28,6 +29,12 @@ struct LCTweakFolderView : View { @State private var newFolderContent = "" @State private var newFolerContinuation : CheckedContinuation? = nil + @State private var renameFileShow = false + @State private var renameFileContent = "" + @State private var renameFileContinuation : CheckedContinuation? = nil + + @State private var choosingTweak = false + init(baseUrl: URL, isRoot: Bool = false, tweakFolders: Binding<[String]>) { _baseUrl = State(initialValue: baseUrl) _tweakFolders = tweakFolders @@ -56,50 +63,91 @@ struct LCTweakFolderView : View { var body: some View { List { - ForEach($tweakItems, id:\.self) { tweakItem in - let tweakItem = tweakItem.wrappedValue - if tweakItem.isFramework { - if #available(iOS 17.0, *) { - Label(tweakItem.fileUrl.lastPathComponent, systemImage: "duffle.bag.fill") - } else { - Label(tweakItem.fileUrl.lastPathComponent, systemImage: "shippingbox.fill") + Section { + ForEach($tweakItems, id:\.self) { tweakItem in + let tweakItem = tweakItem.wrappedValue + VStack { + if tweakItem.isFramework { + Label(tweakItem.fileUrl.lastPathComponent, systemImage: "shippingbox.fill") + } else if tweakItem.isFolder { + NavigationLink { + LCTweakFolderView(baseUrl: tweakItem.fileUrl, isRoot: false, tweakFolders: $tweakFolders) + } label: { + Label(tweakItem.fileUrl.lastPathComponent, systemImage: "folder.fill") + } + } else if tweakItem.isTweak { + Label(tweakItem.fileUrl.lastPathComponent, systemImage: "building.columns.fill") + } else { + Label(tweakItem.fileUrl.lastPathComponent, systemImage: "document.fill") + } } - } else if tweakItem.isFolder { - NavigationLink { - LCTweakFolderView(baseUrl: tweakItem.fileUrl, isRoot: false, tweakFolders: $tweakFolders) - } label: { - Label(tweakItem.fileUrl.lastPathComponent, systemImage: "folder.fill") + .contextMenu { + Button { + Task { await renameTweakItem(tweakItem: tweakItem)} + } label: { + Label("Rename", systemImage: "pencil") + } + + Button(role: .destructive) { + deleteTweakItem(tweakItem: tweakItem) + } label: { + Label("Delete", systemImage: "trash") + } } - } else if tweakItem.isTweak { - Label(tweakItem.fileUrl.lastPathComponent, systemImage: "building.columns.fill") - } else { - Label(tweakItem.fileUrl.lastPathComponent, systemImage: "document.fill") + + }.onDelete { indexSet in + deleteTweakItem(indexSet: indexSet) } - }.onDelete { indexSet in - deleteTweakItem(indexSet: indexSet) } + Section { + VStack{ + if isRoot { + Text("This is the global folder. All tweaks put here will be injected to all guest apps. Create a new folder if you use app-specific tweaks.") + .foregroundStyle(.gray) + .font(.system(size: 12)) + } else { + Text("This is the app-specific folder. Set the tweak folder and the guest app will pick them up recursively.") + .foregroundStyle(.gray) + .font(.system(size: 12)) + } + + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .background(Color(UIColor.systemGroupedBackground)) + .listRowInsets(EdgeInsets()) + } + } .navigationTitle(baseUrl.lastPathComponent) .toolbar { ToolbarItem(placement: .topBarTrailing) { - Button { - - } label: { - Label("sign", systemImage: "signature") + if LCUtils.certificatePassword() != nil { + Button { + + } label: { + Label("sign", systemImage: "signature") + } } } ToolbarItem(placement: .topBarTrailing) { Menu { Button { - Task { await createNewFolder() } + if choosingTweak { + choosingTweak = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + choosingTweak = true + }) + } else { + choosingTweak = true + } } label: { - Label("New folder", systemImage: "folder.badge.plus") + Label("Import Tweak", systemImage: "square.and.arrow.down") } - + Button { - + Task { await createNewFolder() } } label: { - Label("Import Tweak", systemImage: "square.and.arrow.down") + Label("New folder", systemImage: "folder.badge.plus") } } label: { Label("add", systemImage: "plus") @@ -126,6 +174,23 @@ struct LCTweakFolderView : View { newFolerContinuation?.resume() } ) + .textFieldAlert( + isPresented: $renameFileShow, + title: "Enter New Name", + text: $renameFileContent, + placeholder: "", + action: { newText in + self.renameFileContent = newText! + renameFileContinuation?.resume() + }, + actionCancel: {_ in + self.renameFileContent = "" + renameFileContinuation?.resume() + } + ) + .fileImporter(isPresented: $choosingTweak, allowedContentTypes: [.dylib, .lcFramework, .deb], allowsMultipleSelection: true) { result in + Task { await startInstallTweak(result) } + } .alert("Error", isPresented: $errorShow) { Button("OK", action: { }) @@ -146,6 +211,7 @@ struct LCTweakFolderView : View { } catch { errorShow = true errorInfo = error.localizedDescription + return } if isRoot { for iToRemove in indexToRemove { @@ -158,6 +224,75 @@ struct LCTweakFolderView : View { tweakItems.remove(atOffsets: IndexSet(indexToRemove)) } + func deleteTweakItem(tweakItem: LCTweakItem) { + var indexToRemove : Int? + let fm = FileManager() + do { + + try fm.removeItem(at: tweakItem.fileUrl) + indexToRemove = tweakItems.firstIndex(where: { s in + return s == tweakItem + }) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + + guard let indexToRemove = indexToRemove else { + return + } + tweakItems.remove(at: indexToRemove) + if isRoot { + tweakFolders.removeAll(where: { s in + return s == tweakItem.fileUrl.lastPathComponent + }) + } + } + + func renameTweakItem(tweakItem: LCTweakItem) async { + self.renameFileContent = tweakItem.fileUrl.lastPathComponent + + await withCheckedContinuation { c in + self.renameFileContinuation = c + self.renameFileShow = true + } + + if self.renameFileContent == "" { + return + } + + let indexToRename = tweakItems.firstIndex(where: { s in + return s == tweakItem + }) + guard let indexToRename = indexToRename else { + return + } + let newUrl = self.baseUrl.appendingPathComponent(self.renameFileContent) + + let fm = FileManager() + do { + try fm.moveItem(at: tweakItem.fileUrl, to: newUrl) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + tweakItems.remove(at: indexToRename) + let newTweakItem = LCTweakItem(fileUrl: newUrl, isFolder: tweakItem.isFolder, isFramework: tweakItem.isFramework, isTweak: tweakItem.isTweak) + tweakItems.insert(newTweakItem, at: indexToRename) + + if isRoot { + let indexToRename2 = tweakFolders.firstIndex(of: tweakItem.fileUrl.lastPathComponent) + guard let indexToRename2 = indexToRename2 else { + return + } + tweakFolders.remove(at: indexToRename2) + tweakFolders.insert(self.renameFileContent, at: indexToRename2) + + } + } + func createNewFolder() async { self.newFolderContent = "" @@ -184,6 +319,67 @@ struct LCTweakFolderView : View { } + } + + func startInstallTweak(_ result: Result<[URL], any Error>) async { + do { + let fm = FileManager() + let urls = try result.get() + var tmpPaths : [URL] = [] + // copy selected tweaks to tmp dir first + let tmpDir = fm.temporaryDirectory.appendingPathComponent("TweakTmp") + if fm.fileExists(atPath: tmpDir.path) { + try fm.removeItem(at: tmpDir) + } + try fm.createDirectory(at: tmpDir, withIntermediateDirectories: true) + for fileUrl in urls { + // handle deb file + if(!fileUrl.startAccessingSecurityScopedResource()) { + throw "Cannot open \(fileUrl.lastPathComponent), permission denied." + } + if(!fileUrl.isFileURL) { + throw "\(fileUrl.absoluteString), is not a file." + } + let toPath = tmpDir.appendingPathComponent(fileUrl.lastPathComponent) + try fm.copyItem(at: fileUrl, to: toPath) + tmpPaths.append(toPath) + LCParseMachO((toPath.path as NSString).utf8String) { path, header in + LCPatchAddRPath(path, header); + } + fileUrl.stopAccessingSecurityScopedResource() + } + + if (LCUtils.certificatePassword() != nil) { + // if in jit-less mode, we need to sign + let error = await LCUtils.signFilesInFolder(url: tmpDir) { p in + + } + if let error = error { + throw error + } + } + + + for tmpFile in tmpPaths { + let toPath = self.baseUrl.appendingPathComponent(tmpFile.lastPathComponent) + try fm.moveItem(at: tmpFile, to: toPath) + + let isFramework = toPath.lastPathComponent.hasSuffix(".framework") + let isTweak = toPath.lastPathComponent.hasSuffix(".dylib") + self.tweakItems.append(LCTweakItem(fileUrl: toPath, isFolder: false, isFramework: isFramework, isTweak: isTweak)) + } + + // clean up + try fm.removeItem(at: tmpDir) + + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + + + } } diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 1377e46..9188f54 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -6,6 +6,7 @@ // import SwiftUI +import UniformTypeIdentifiers struct LCPath { public static let docPath = { @@ -21,6 +22,13 @@ extension String: LocalizedError { public var errorDescription: String? { return self } } +extension UTType { + static let ipa = UTType(filenameExtension: "ipa")! + static let dylib = UTType(filenameExtension: "dylib")! + static let deb = UTType(filenameExtension: "deb")! + static let lcFramework = UTType(filenameExtension: "framework", conformingTo: .package)! +} + // https://stackoverflow.com/questions/56726663/how-to-add-a-textfield-to-alert-in-swiftui extension View { @@ -122,3 +130,55 @@ struct AppLinks : Codable { struct SiteAssociation : Codable { var applinks: AppLinks? } + +extension LCUtils { + public static func signFilesInFolder(url: URL, onProgressCreated: (Progress) -> Void) async -> String? { + let fm = FileManager() + var ans : String? = nil + /* NSString *codesignPath = [path stringByAppendingPathComponent:@"_CodeSignature"]; + NSString *provisionPath = [path stringByAppendingPathComponent:@"embedded.mobileprovision"]; + NSString *tmpExecPath = [path stringByAppendingPathComponent:@"LiveContainer.tmp"]; + NSString *tmpInfoPath = [path stringByAppendingPathComponent:@"Info.plist"]; + [NSFileManager.defaultManager copyItemAtPath:NSBundle.mainBundle.executablePath toPath:tmpExecPath error:nil]; + NSMutableDictionary *info = NSBundle.mainBundle.infoDictionary.mutableCopy; + */ + let codesignPath = url.appendingPathComponent("_CodeSignature") + let provisionPath = url.appendingPathComponent("embedded.mobileprovision") + let tmpExecPath = url.appendingPathComponent("LiveContainer.tmp") + let tmpInfoPath = url.appendingPathComponent("Info.plist") + var info = Bundle.main.infoDictionary!; + info["CFBundleExecutable"] = "LiveContainer.tmp"; + let nsInfo = info as NSDictionary + nsInfo.write(to: tmpInfoPath, atomically: true) + do { + try fm.copyItem(at: Bundle.main.executableURL!, to: tmpExecPath) + } catch { + return nil + } + + await withCheckedContinuation { c in + let progress = LCUtils.signAppBundle(url) { success, error in + do { + if let error = error { + ans = error.localizedDescription + } + try fm.removeItem(at: codesignPath) + try fm.removeItem(at: provisionPath) + try fm.removeItem(at: tmpExecPath) + try fm.removeItem(at: tmpInfoPath) + } catch { + ans = error.localizedDescription + } + c.resume() + } + guard let progress = progress else { + ans = "Failed to initiate bundle signing." + c.resume() + return + } + onProgressCreated(progress) + } + return ans + + } +} From b56028e5e25e5eee2fe2e4a8c8ca32074b2ccb6c Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 29 Aug 2024 23:56:53 +0800 Subject: [PATCH 12/36] tweak signing --- LiveContainerSwiftUI/LCTweaksView.swift | 96 +++++++++++++++++++------ 1 file changed, 75 insertions(+), 21 deletions(-) diff --git a/LiveContainerSwiftUI/LCTweaksView.swift b/LiveContainerSwiftUI/LCTweaksView.swift index b601ddf..f528aad 100644 --- a/LiveContainerSwiftUI/LCTweaksView.swift +++ b/LiveContainerSwiftUI/LCTweaksView.swift @@ -35,6 +35,8 @@ struct LCTweakFolderView : View { @State private var choosingTweak = false + @State private var isTweakSigning = false + init(baseUrl: URL, isRoot: Bool = false, tweakFolders: Binding<[String]>) { _baseUrl = State(initialValue: baseUrl) _tweakFolders = tweakFolders @@ -121,37 +123,43 @@ struct LCTweakFolderView : View { .navigationTitle(baseUrl.lastPathComponent) .toolbar { ToolbarItem(placement: .topBarTrailing) { - if LCUtils.certificatePassword() != nil { + if !isTweakSigning && LCUtils.certificatePassword() != nil { Button { - + Task { await signAllTweaks() } } label: { Label("sign", systemImage: "signature") } } + } ToolbarItem(placement: .topBarTrailing) { - Menu { - Button { - if choosingTweak { - choosingTweak = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + if !isTweakSigning { + Menu { + Button { + if choosingTweak { + choosingTweak = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + choosingTweak = true + }) + } else { choosingTweak = true - }) - } else { - choosingTweak = true + } + } label: { + Label("Import Tweak", systemImage: "square.and.arrow.down") + } + + Button { + Task { await createNewFolder() } + } label: { + Label("New folder", systemImage: "folder.badge.plus") } } label: { - Label("Import Tweak", systemImage: "square.and.arrow.down") - } - - Button { - Task { await createNewFolder() } - } label: { - Label("New folder", systemImage: "folder.badge.plus") + Label("add", systemImage: "plus") } - } label: { - Label("add", systemImage: "plus") + } else { + ProgressView().progressViewStyle(.circular) } + } } .alert("Error", isPresented: $errorShow) { @@ -293,6 +301,51 @@ struct LCTweakFolderView : View { } } + func signAllTweaks() async { + do { + let fm = FileManager() + let tmpDir = fm.temporaryDirectory.appendingPathComponent("TweakTmp") + if fm.fileExists(atPath: tmpDir.path) { + try fm.removeItem(at: tmpDir) + } + try fm.createDirectory(at: tmpDir, withIntermediateDirectories: true) + + var tmpPaths : [URL] = [] + // copy items to tmp folders + for item in tweakItems { + let tmpPath = tmpDir.appendingPathComponent(item.fileUrl.lastPathComponent) + tmpPaths.append(tmpPath) + try fm.copyItem(at: item.fileUrl, to: tmpPath) + } + + if (LCUtils.certificatePassword() != nil) { + // if in jit-less mode, we need to sign + isTweakSigning = true + let error = await LCUtils.signFilesInFolder(url: tmpDir) { p in + + } + isTweakSigning = false + if let error = error { + throw error + } + } + + for tmpFile in tmpPaths { + let toPath = self.baseUrl.appendingPathComponent(tmpFile.lastPathComponent) + // remove original item and move the signed ones back + if fm.fileExists(atPath: toPath.path) { + try fm.removeItem(at: toPath) + try fm.moveItem(at: tmpFile, to: toPath) + } + } + + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } + func createNewFolder() async { self.newFolderContent = "" @@ -317,8 +370,6 @@ struct LCTweakFolderView : View { if isRoot { tweakFolders.append(self.newFolderContent) } - - } func startInstallTweak(_ result: Result<[URL], any Error>) async { @@ -332,6 +383,7 @@ struct LCTweakFolderView : View { try fm.removeItem(at: tmpDir) } try fm.createDirectory(at: tmpDir, withIntermediateDirectories: true) + for fileUrl in urls { // handle deb file if(!fileUrl.startAccessingSecurityScopedResource()) { @@ -351,9 +403,11 @@ struct LCTweakFolderView : View { if (LCUtils.certificatePassword() != nil) { // if in jit-less mode, we need to sign + isTweakSigning = true let error = await LCUtils.signFilesInFolder(url: tmpDir) { p in } + isTweakSigning = false if let error = error { throw error } From d50413b3363d946e9a0a9ca0b2a6a5dca3808160 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 3 Sep 2024 11:22:15 +0800 Subject: [PATCH 13/36] multi live containers proof of concept --- LCSharedUtils.m | 8 +- LiveContainerSwiftUI/LCAppBanner.swift | 194 ++++++++++++++++++---- LiveContainerSwiftUI/LCSettingsView.swift | 29 ++++ LiveContainerSwiftUI/LCTabView.swift | 13 ++ LiveContainerSwiftUI/Shared.swift | 36 +++- LiveContainerUI/LCAppInfo.h | 2 + LiveContainerUI/LCAppInfo.m | 6 +- LiveContainerUI/LCUtils.h | 2 + LiveContainerUI/LCUtils.m | 63 ++++++- Resources/AppIcon2_60x60@2x.png | Bin 0 -> 29066 bytes Resources/AppIcon2_76x76@2x~ipad.png | Bin 0 -> 42229 bytes TweakLoader/Makefile | 2 +- TweakLoader/NSUserDefaults.m | 25 +++ main.m | 34 +++- 14 files changed, 364 insertions(+), 50 deletions(-) create mode 100644 Resources/AppIcon2_60x60@2x.png create mode 100644 Resources/AppIcon2_76x76@2x~ipad.png create mode 100644 TweakLoader/NSUserDefaults.m diff --git a/LCSharedUtils.m b/LCSharedUtils.m index 9a4af1a..d3a8ff1 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -5,7 +5,13 @@ @implementation LCSharedUtils + (NSString *)certificatePassword { - return [lcUserDefaults objectForKey:@"LCCertificatePassword"]; + NSString* ans = [lcUserDefaults objectForKey:@"LCCertificatePassword"]; + if(ans) { + return ans; + } else { + NSString *appGroupID = [NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"] firstObject]; + return [[[NSUserDefaults alloc] initWithSuiteName:appGroupID] objectForKey:@"LCCertificatePassword"]; + } } + (BOOL)launchToGuestApp { diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 0a5eba6..9d06876 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -16,6 +16,9 @@ protocol LCAppBannerDelegate { struct LCAppBanner : View { @State var appInfo: LCAppInfo var delegate: LCAppBannerDelegate + + @State var uiIsShared : Bool + @Binding var appDataFolders: [String] @Binding var tweakFolders: [String] @@ -36,6 +39,14 @@ struct LCAppBanner : View { @State private var renameFolderContent = "" @State private var renameFolerContinuation : CheckedContinuation? = nil + @State private var confirmMoveToAppGroupShow = false + @State private var confirmMoveToAppGroup = false + @State private var confirmMoveToAppGroupContinuation : CheckedContinuation? = nil + + @State private var confirmMoveToPrivateDocShow = false + @State private var confirmMoveToPrivateDoc = false + @State private var confirmMoveToPrivateDocContinuation : CheckedContinuation? = nil + @State private var errorShow = false @State private var errorInfo = "" @@ -55,6 +66,8 @@ struct LCAppBanner : View { _uiPickerDataFolder = _uiDataFolder _uiPickerTweakFolder = _uiTweakFolder + _uiIsShared = State(initialValue: appInfo.isShared) + } var body: some View { @@ -113,51 +126,60 @@ struct LCAppBanner : View { .contextMenu{ Text(appInfo.relativeBundlePath) - Button(role: .destructive) { - Task{ await uninstall() } - } label: { - Label("Uninstall", systemImage: "trash") - } - Button { - // Add to home screen - } label: { - Label("Add to home screen", systemImage: "plus.app") - } - Menu(content: { + if !uiIsShared { + Button(role: .destructive) { + Task{ await uninstall() } + } label: { + Label("Uninstall", systemImage: "trash") + } Button { - Task{ await createFolder() } + Task { await moveToAppGroup()} } label: { - Label("New data folder", systemImage: "plus") + Label("Convert to Shared App", systemImage: "arrowshape.turn.up.left") } - if uiDataFolder != nil { + Menu(content: { Button { - Task{ await renameDataFolder() } + Task{ await createFolder() } } label: { - Label("Rename data folder", systemImage: "pencil") + Label("New data folder", systemImage: "plus") + } + if uiDataFolder != nil { + Button { + Task{ await renameDataFolder() } + } label: { + Label("Rename data folder", systemImage: "pencil") + } } - } - Picker(selection: $uiPickerDataFolder , label: Text("")) { - ForEach(appDataFolders, id:\.self) { folderName in - Button(folderName) { - setDataFolder(folderName: folderName) - }.tag(Optional(folderName)) + Picker(selection: $uiPickerDataFolder , label: Text("")) { + ForEach(appDataFolders, id:\.self) { folderName in + Button(folderName) { + setDataFolder(folderName: folderName) + }.tag(Optional(folderName)) + } } - } - }, label: { - Label("Change Data Folder", systemImage: "folder.badge.questionmark") - }) - - Menu(content: { - Picker(selection: $uiPickerTweakFolder , label: Text("")) { - Label("None", systemImage: "nosign").tag(Optional(nil)) - ForEach(tweakFolders, id:\.self) { folderName in - Text(folderName).tag(Optional(folderName)) + }, label: { + Label("Change Data Folder", systemImage: "folder.badge.questionmark") + }) + + Menu(content: { + Picker(selection: $uiPickerTweakFolder , label: Text("")) { + Label("None", systemImage: "nosign").tag(Optional(nil)) + ForEach(tweakFolders, id:\.self) { folderName in + Text(folderName).tag(Optional(folderName)) + } } + }, label: { + Label("Change Tweak Folder", systemImage: "gear") + }) + } else { + Button { + Task { await movePrivateDoc() } + } label: { + Label("Convert to Private App", systemImage: "arrowshape.turn.up.left") } - }, label: { - Label("Change Tweak Folder", systemImage: "gear") - }) + } + } .onChange(of: uiPickerDataFolder, perform: { newValue in if newValue != uiDataFolder { @@ -198,6 +220,34 @@ struct LCAppBanner : View { } message: { Text("Do you also want to delete data folder of \(appInfo.displayName()!)? You can keep it for future use.") } + .alert("Move to App Group", isPresented: $confirmMoveToAppGroupShow) { + Button { + self.confirmMoveToAppGroup = true + self.confirmMoveToAppGroupContinuation?.resume() + } label: { + Text("Move") + } + Button("Cancel", role: .cancel) { + self.confirmMoveToAppGroup = false + self.confirmMoveToAppGroupContinuation?.resume() + } + } message: { + Text("Moving this app to App Group allows other LiveContainers to run this app with all its data and tweak preserved. If you want to access its data and tweak again from the file app, move it back.") + } + .alert("Move to Private Document Folder", isPresented: $confirmMoveToPrivateDocShow) { + Button { + self.confirmMoveToPrivateDoc = true + self.confirmMoveToPrivateDocContinuation?.resume() + } label: { + Text("Move") + } + Button("Cancel", role: .cancel) { + self.confirmMoveToPrivateDoc = false + self.confirmMoveToPrivateDocContinuation?.resume() + } + } message: { + Text("Moving this app to Private Document Folder allows you to access its data and tweaks in the Files app, but it can not be run by other LiveContainers.") + } .textFieldAlert( isPresented: $renameFolderShow, title: "Enter the name of new folder", @@ -372,11 +422,83 @@ struct LCAppBanner : View { } } + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + } + + func moveToAppGroup() async { + await withCheckedContinuation { c in + confirmMoveToAppGroupContinuation = c + confirmMoveToAppGroupShow = true + } + if !confirmMoveToAppGroup { + return + } + + do { + try LCPath.ensureAppGroupPaths() + let fm = FileManager() + try fm.moveItem(atPath: appInfo.bundlePath(), toPath: LCPath.lcGroupBundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) + if let dataFolder = appInfo.getDataUUIDNoAssign(), dataFolder.count > 0 { + try fm.moveItem(at: LCPath.dataPath.appendingPathComponent(dataFolder), + to: LCPath.lcGroupDataPath.appendingPathComponent(dataFolder)) + appDataFolders.removeAll(where: { s in + return s == dataFolder + }) + } + if let tweakFolder = appInfo.tweakFolder(), tweakFolder.count > 0 { + try fm.moveItem(at: LCPath.tweakPath.appendingPathComponent(tweakFolder), + to: LCPath.lcGroupTweakPath.appendingPathComponent(tweakFolder)) + tweakFolders.removeAll(where: { s in + return s == tweakFolder + }) + } + appInfo.setBundlePath(LCPath.lcGroupBundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) + appInfo.isShared = true + uiIsShared = true + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + + } + + func movePrivateDoc() async { + await withCheckedContinuation { c in + confirmMoveToPrivateDocContinuation = c + confirmMoveToPrivateDocShow = true + } + if !confirmMoveToPrivateDoc { + return + } + + do { + let fm = FileManager() + try fm.moveItem(atPath: appInfo.bundlePath(), toPath: LCPath.bundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) + if let dataFolder = appInfo.getDataUUIDNoAssign(), dataFolder.count > 0 { + try fm.moveItem(at: LCPath.lcGroupDataPath.appendingPathComponent(dataFolder), + to: LCPath.dataPath.appendingPathComponent(dataFolder)) + appDataFolders.append(dataFolder) + uiDataFolder = dataFolder + uiPickerDataFolder = dataFolder + } + if let tweakFolder = appInfo.tweakFolder(), tweakFolder.count > 0 { + try fm.moveItem(at: LCPath.lcGroupTweakPath.appendingPathComponent(tweakFolder), + to: LCPath.tweakPath.appendingPathComponent(tweakFolder)) + tweakFolders.append(tweakFolder) + uiTweakFolder = tweakFolder + uiPickerTweakFolder = tweakFolder + } + appInfo.setBundlePath(LCPath.bundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) + appInfo.isShared = false + uiIsShared = false } catch { errorShow = true errorInfo = error.localizedDescription - } + } diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index f1e0192..9f8a8d7 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -51,6 +51,14 @@ struct LCSettingsView: View { Text("Setup JIT-less certificate") } } + if isJitLessEnabled { + Button { + installAnotherLC() + } label: { + Text("Install another LiveContainer") + } + } + } header: { Text("JIT-Less") @@ -172,6 +180,27 @@ struct LCSettingsView: View { } + func installAnotherLC() { + if !LCUtils.isAppGroupAltStoreLike() { + errorInfo = "Unsupported installation method. Please use AltStore or SideStore to setup this feature." + errorShow = true + return; + } + let password = LCUtils.certificatePassword() + let lcDomain = UserDefaults.init(suiteName: LCUtils.appGroupID()) + lcDomain?.setValue(password, forKey: "LCCertificatePassword") + + + do { + let packedIpaUrl = try LCUtils.archiveIPA(withBundleName: "LiveContainer2") + let storeInstallUrl = String(format: LCUtils.storeInstallURLScheme(), packedIpaUrl.absoluteString) + UIApplication.shared.open(URL(string: storeInstallUrl)!) + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + } + func cleanUpUnusedFolders() async { var folderNameToAppDict : [String:LCAppInfo] = [:] diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index 583c5d7..9a7150b 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -33,6 +33,19 @@ struct LCTabView: View { } let newApp = LCAppInfo(bundlePath: "\(LCPath.bundlePath.path)/\(appDir)")! newApp.relativeBundlePath = appDir + newApp.isShared = false + tempApps.append(newApp) + } + + try fm.createDirectory(at: LCPath.lcGroupBundlePath, withIntermediateDirectories: true) + let appDirsShared = try fm.contentsOfDirectory(atPath: LCPath.lcGroupBundlePath.path) + for appDir in appDirsShared { + if !appDir.hasSuffix(".app") { + continue + } + let newApp = LCAppInfo(bundlePath: "\(LCPath.lcGroupBundlePath.path)/\(appDir)")! + newApp.relativeBundlePath = appDir + newApp.isShared = true tempApps.append(newApp) } // load document folders diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 9188f54..c786002 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -16,6 +16,35 @@ struct LCPath { public static let bundlePath = docPath.appendingPathComponent("Applications") public static let dataPath = docPath.appendingPathComponent("Data/Application") public static let tweakPath = docPath.appendingPathComponent("Tweaks") + + public static let lcGroupDocPath = { + let fm = FileManager() + // it seems that Apple don't want to create one for us, so we just borrow our Store's + if let appGroupPath = LCUtils.appGroupPath() { + return URL(fileURLWithPath: appGroupPath + "/LiveContainer", isDirectory: true) + } else if let appGroupPathUrl = + FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.SideStore.SideStore") { + return appGroupPathUrl.appendingPathComponent("LiveContainer") + } else { + return docPath + } + }() + public static let lcGroupBundlePath = lcGroupDocPath.appendingPathComponent("Applications") + public static let lcGroupDataPath = lcGroupDocPath.appendingPathComponent("Data/Application") + public static let lcGroupTweakPath = lcGroupDocPath.appendingPathComponent("Tweaks") + + public static func ensureAppGroupPaths() throws { + let fm = FileManager() + if !fm.fileExists(atPath: LCPath.lcGroupBundlePath.path) { + try fm.createDirectory(at: LCPath.lcGroupBundlePath, withIntermediateDirectories: true) + } + if !fm.fileExists(atPath: LCPath.lcGroupDataPath.path) { + try fm.createDirectory(at: LCPath.lcGroupDataPath, withIntermediateDirectories: true) + } + if !fm.fileExists(atPath: LCPath.lcGroupTweakPath.path) { + try fm.createDirectory(at: LCPath.lcGroupTweakPath, withIntermediateDirectories: true) + } + } } extension String: LocalizedError { @@ -135,13 +164,6 @@ extension LCUtils { public static func signFilesInFolder(url: URL, onProgressCreated: (Progress) -> Void) async -> String? { let fm = FileManager() var ans : String? = nil - /* NSString *codesignPath = [path stringByAppendingPathComponent:@"_CodeSignature"]; - NSString *provisionPath = [path stringByAppendingPathComponent:@"embedded.mobileprovision"]; - NSString *tmpExecPath = [path stringByAppendingPathComponent:@"LiveContainer.tmp"]; - NSString *tmpInfoPath = [path stringByAppendingPathComponent:@"Info.plist"]; - [NSFileManager.defaultManager copyItemAtPath:NSBundle.mainBundle.executablePath toPath:tmpExecPath error:nil]; - NSMutableDictionary *info = NSBundle.mainBundle.infoDictionary.mutableCopy; - */ let codesignPath = url.appendingPathComponent("_CodeSignature") let provisionPath = url.appendingPathComponent("embedded.mobileprovision") let tmpExecPath = url.appendingPathComponent("LiveContainer.tmp") diff --git a/LiveContainerUI/LCAppInfo.h b/LiveContainerUI/LCAppInfo.h index 86018ad..be284d7 100644 --- a/LiveContainerUI/LCAppInfo.h +++ b/LiveContainerUI/LCAppInfo.h @@ -13,6 +13,8 @@ NSString* _bundlePath; } @property NSString* relativeBundlePath; +@property bool isShared; +- (void)setBundlePath:(NSString*)newBundlePath; - (NSMutableDictionary*)info; - (UIImage*)icon; - (NSString*)displayName; diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index 4149872..1df7017 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -11,7 +11,7 @@ @implementation SignTmpStatus @implementation LCAppInfo - (instancetype)initWithBundlePath:(NSString*)bundlePath { self = [super init]; - + self.isShared = false; if(self) { _bundlePath = bundlePath; _info = [NSMutableDictionary dictionaryWithContentsOfFile:[NSString stringWithFormat:@"%@/Info.plist", bundlePath]]; @@ -20,6 +20,10 @@ - (instancetype)initWithBundlePath:(NSString*)bundlePath { return self; } +- (void)setBundlePath:(NSString*)newBundlePath { + _bundlePath = newBundlePath; +} + - (NSMutableArray*)urlSchemes { // find all url schemes NSMutableArray* urlSchemes = [[NSMutableArray alloc] init]; diff --git a/LiveContainerUI/LCUtils.h b/LiveContainerUI/LCUtils.h index f1f6fa2..9f0d180 100644 --- a/LiveContainerUI/LCUtils.h +++ b/LiveContainerUI/LCUtils.h @@ -15,6 +15,7 @@ void LCPatchExecSlice(const char *path, struct mach_header_64 *header); @interface LCUtils : NSObject + (NSURL *)archiveIPAWithSetupMode:(BOOL)setup error:(NSError **)error; ++ (NSURL *)archiveIPAWithBundleName:(NSString*)newBundleName error:(NSError **)error; + (NSData *)certificateData; + (NSString *)certificatePassword; + (void)setCertificateData:(NSData *)data; @@ -29,6 +30,7 @@ void LCPatchExecSlice(const char *path, struct mach_header_64 *header); + (NSProgress *)signAppBundle:(NSURL *)path completionHandler:(void (^)(BOOL success, NSError *error))completionHandler; + (BOOL)isAppGroupAltStoreLike; ++ (NSString *)appGroupID; + (NSString *)appGroupPath; + (NSString *)storeInstallURLScheme; + (NSString *)getVersionInfo; diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index 1193fa2..e6d4cfb 100644 --- a/LiveContainerUI/LCUtils.m +++ b/LiveContainerUI/LCUtils.m @@ -57,7 +57,13 @@ + (NSData *)certificateDataFile { } + (NSData *)certificateDataProperty { - return [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificateData"]; + NSData* ans = [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificateData"]; + if(ans) { + return ans; + } else { + return [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificateData"]; + } + } + (NSData *)certificateData { @@ -67,7 +73,11 @@ + (NSData *)certificateData { + (NSString *)certificatePassword { if (self.certificateDataFile) { - return [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificatePassword"];; + NSString* ans = [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificatePassword"]; + if(ans) { + return ans; + } + return [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificatePassword"]; } else if (self.certificateDataProperty) { return @""; } else { @@ -77,6 +87,7 @@ + (NSString *)certificatePassword { + (void)setCertificatePassword:(NSString *)certPassword { [NSUserDefaults.standardUserDefaults setObject:certPassword forKey:@"LCCertificatePassword"]; + [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] setObject:certPassword forKey:@"LCCertificatePassword"]; } #pragma mark LCSharedUtils wrappers @@ -283,6 +294,54 @@ + (NSURL *)archiveIPAWithSetupMode:(BOOL)setup error:(NSError **)error { return tmpIPAPath; } + ++ (NSURL *)archiveIPAWithBundleName:(NSString*)newBundleName error:(NSError **)error { + if (*error) return nil; + + NSFileManager *manager = NSFileManager.defaultManager; + NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:self.appGroupID]; + NSURL *bundlePath = [appGroupPath URLByAppendingPathComponent:@"Apps/com.kdt.livecontainer"]; + + NSURL *tmpPath = [appGroupPath URLByAppendingPathComponent:@"tmp"]; + [manager removeItemAtURL:tmpPath error:nil]; + + NSURL *tmpPayloadPath = [tmpPath URLByAppendingPathComponent:@"Payload"]; + NSURL *tmpIPAPath = [appGroupPath URLByAppendingPathComponent:@"tmp.ipa"]; + + [manager createDirectoryAtURL:tmpPath withIntermediateDirectories:YES attributes:nil error:error]; + if (*error) return nil; + + [manager copyItemAtURL:bundlePath toURL:tmpPayloadPath error:error]; + if (*error) return nil; + + NSURL *infoPath = [tmpPayloadPath URLByAppendingPathComponent:@"App.app/Info.plist"]; + NSMutableDictionary *infoDict = [NSMutableDictionary dictionaryWithContentsOfURL:infoPath]; + if (!infoDict) return nil; + + infoDict[@"CFBundleDisplayName"] = newBundleName; + infoDict[@"CFBundleName"] = newBundleName; + infoDict[@"CFBundleIdentifier"] = [NSString stringWithFormat:@"com.kdt.%@", newBundleName]; + infoDict[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0] = newBundleName; + infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon2_60x60@2x"; + infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][1] = @"AppIcon2_76x76@2x~ipad"; + infoDict[@"CFBundleIcons"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon2_60x60@2x"; + + + [infoDict writeToURL:infoPath error:error]; + + dlopen("/System/Library/PrivateFrameworks/PassKitCore.framework/PassKitCore", RTLD_GLOBAL); + NSData *zipData = [[NSClassFromString(@"PKZipArchiver") new] zippedDataForURL:tmpPayloadPath.URLByDeletingLastPathComponent]; + if (!zipData) return nil; + + [manager removeItemAtURL:tmpPath error:error]; + if (*error) return nil; + + [zipData writeToURL:tmpIPAPath options:0 error:error]; + if (*error) return nil; + + return tmpIPAPath; +} + + (NSString *)getVersionInfo { return [NSClassFromString(@"LCVersionInfo") getVersionStr]; } diff --git a/Resources/AppIcon2_60x60@2x.png b/Resources/AppIcon2_60x60@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c8688e77829d891cc3065e4ba5eb918867b1bf7a GIT binary patch literal 29066 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 PmlVoN1Egvtj6?nhdV}y2 literal 0 HcmV?d00001 diff --git a/Resources/AppIcon2_76x76@2x~ipad.png b/Resources/AppIcon2_76x76@2x~ipad.png new file mode 100644 index 0000000000000000000000000000000000000000..f4d7e0090f49c0ff6289daf7ec2dd6910f544960 GIT binary patch literal 42229 zcmYIv18`+cuyAa1%CLerw3i9sxdu1 z-E$(96{V07@Daekz@Wq}#AKAgzQZuIdzgRqk!7SMRKIY>e*g#b_5SwzJLwmKvJh1e z1p{l0M|?Ae{_2Cfs7i@})lCtee09E?N~fq7Gbfdz(wfxUbU1s;Qexv_wO{WAgs zGqt3W*u0U9ojHGIl3C51*;a zVRqJFol}Fy&Lx4HS&gS-d|xyu)9GK!w~vJ`SE3D1ekA%m z!U}L5)A{E!Pl%77AmL5~Oy>9Ad|{^TO<{+1;1erER11V`bS?~tsADv4-B!vThwyT6 znpPeH0YuZ$@oXBVEwVx*2f|AG+vcFLx6~ryEzeUO-n=)_&HQb#nBI;bxul|rI2L)I zDhMX|DFfL&$a_9lV@b@TOX&}hJvWbk-TM05H)q5Je1?G|EX-5`5qVXvQ7=_hN{td6 zjmE+-+;{%t}uAHd(7@(>bsXnMPZpzv0p2)F6{xBZK#80^f&o z!W46QI(h|MLwk3TS+V;^h%sM>(MbqrIU8F1owi{4N^&aD&*NqY6IExKF~%Npvrkwp zM8D&}aoQgRVh&OpaGJ@c@RLTF#7GQ_4x_914ZoYA++%%XZK`_@M(!egm#ZP;1+Ve@ z7*+%SQgMiuwGUePw=sCYR+ZuhaJL|j?c`#{{@B=xcrjPNSv8_eK*qZQFvS;FcQ4P-E1sN1^zuWWRaJ3|5 zXfYLKcYIdRI*as6@Vm|ZV>8sKw*~Fx_taW#eEtzE{N1LgNJVBcD$4YyI|x+-`mFV{ zPp~e!;QJF!I2z0uopTyO;|5BRwoO3BWsckiGTr@q)inek*m!K;)FwJJM&pCe@4p}Y zY0Tj_Ds9}~-VRUSo(&yX+ zZDUlji>mG_)&v#jZF2KsI^Zcd1euhJWM***nF%USpWH^VqrSbeA{Jc%1t}ijvTo6s z8wg5aV$pN9;x^^?OJ^Qw;L=z8{U3?U=zRycMck9yb2qdTc$@K&`fFFfN6n{#`D%fW z%XF)Gw{I5A2E$O_EMb0lMXP%RkM2m)t!c_IddA0#wC24g&W5lAS$0$X;{w~(iT>$fa!4x}UxP{c} z)pob_P3-B#s@+#G3$?3fuzX{4dyTmMO5rj6UyTeBp9~qyF{gU}1 ziT(}T;-QkJcHz=qec!dS|7h7HSUQ*R>thLiue2@DtCO7WA)QCqeSgRC3!28=7uLcZgg=_fZB!3iwMQ%IjgzfqsxqWkCf4Twekz)rPqDp|E9hv|hJy0hE{ zSU#tS3&$`BJ#pgskNdkZ==+y?5?QzP19q7*q4U!Wgs;T;561)h1`gMzmt1dr1&rfc zcH_H2^|#vTnpThNh;O&E>7H+4OwFg9IijXitQX_?&re;vzAu03ecv_+sr>nqm3p%W zrMBKC+Nb@uH)ai4TxPwSqGeg(cDG8KSBa)vEix{suHh-((!p$?PLuivqKk%L;bI=p zzxfT38p1mEdW4@DLvp<_$Rh-`TZ1RhASDdJ+qbE`3w!U2i^IP7rbxNNROZ>>&zgysJ zXZ8WHeGx;})B-&*bKaAXq7O6RuUBkjdZc7}d(6wemG+I)I}!85m^qz6!P_E!2ugtrB$1$+ zw5L0=1ptA!Jt{LkD3W{&W#DqHjrK}UY_smiz7JEG(UOU?2Lr_

4y5`Wm>kIpO(9 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%0A+(xEv zguu{S6TyG)eq>lZOB$FemevrXM%Wq74MB?ZbnsIwtq?U%OC!%sS@zY{$Cv5~0p->& zwYZCZZrC4{;Zz=TSx`8+Dor23QJ9TSkhrhbnI{NwJTD8n9tilywY zI3~-Pl^rcR=g%Fmw?;^Hbe)m)Y?b6HJMaYkkA!K2rJUsi<0f~NliZ+VL~Tl%ak*qI zH5+Th@xA~CTa@$&&F?kg6mXIn)6G@_J=r>7<|!v%Hz+Q0kE_w*p7o)gf2vnu^Gw_2 zC+3tCZ=(_Ct7M#!M%RPwmm_L9=f7R*>j^$W--QX3RAq~rphh^s!&w=Ou!vi7d4q&j8_wCgD;d%yWoXiluBIIT!tvg8XH?y5Fxn*UU83crn1(3cOEhO zJAG|pGIOnYv;|!L^o?XugNkurI|L<(BERh82mNxx6`H}?bc01Lu2im)qje9z50(E! zQyX7Hp`S%s^XWkT#|NE7uo#IYv{yZgb}>697W0mnUSh>^5!EsTVP`=96ld3xB?-Uj z^y*if_oMHA5ouFZ&OR2qYj35R1;MwwD`@l7=D4>ktKSu!1vJ9|&XZeVvBHZzY;r#! zJ0XFaiPu(~hacN6*34^$TOejdF0eJg4ZH8Et2UV;egojmxWz_xV5~RkFVmsZn537_ zmk`v~)MOuPN$a*2yoYg_zh=cu$k7;S{z0wfAs>Fbf^gVXw%dG;BwO4;i_1=K|6q=@ zZuK*>lLyS0nJJ`~FC4hj5^5L(_@h7c$;7@k|i=PvR{%uL~`5zMR#Qf#NVf zP39iBj3;a<8b+p^Z(2-FlXnTS&c3AFe4c^13NJdh%g^>q1*=#>L`Pn}2u@=3l+(fn z(EfM#D1crq$2kDr>7Xe{=xYu;3u?c@z-@4D*Jcr`}99P12c ztyy*5uT8JKXt)zU+_ql4%ogTMJ}KzJ#^&2OK*~N<(KendI%tTHx zK5{spI-%zgYg#Ve$iw6<>rc7%pud-%YN6FWo1ZI14u>#JT3Udh9fA`!x~hKCt9ESb z)h=cVQY&XCx0y+zucapK%4x{{F#wdMtyM{;uBF^H=Sqy^EjdgzhGZJXVsktAf$+}` ziwh2mOC*tS>>iDfx%g2_v-3`W=r1-bZ{H=>=52aYjg&)L+v!M5Ywpvn={a1q+^>fO z7Z&marqn$WwSlnqUGtG$3aEjoPKSP-&ZUuLE|?CQldi%zCt~NS7I#){Y!N%0Ws=e( z`KBO=$}r~^qL6d<_6w-&Sr|hiP?XY$W4IS|qhOZJCfa9>RwdVBLOy6}xI?;FHgcOM6v zrY_vrl=*;}gZB!Rl#H)>yN9;G*anve`)A{OTF62Aas6dK_=ymmTjsqscZ=`Mw|Ltd zo#R~EgV$h2zOl5#?j7;Q)sGZd(v+5ebZId!L%S4hCm{>XWS;HBr!%62*rRp&PsypJ ziPrej0q}wxm&^l?_d|jz%D4$oOL^tZg{V)~ky2C8jLyP~#hX(j0iO!P<4YRC1)1}W z!wj!lGGHXw{rss?LF(hijJj@+LJ+H8K8{dlYkAcU7fylsNX(a9ulBwRjKELQD5Wh{ zv`+SgkEv;j%m~Lm;@IEJ0la7yhPxCH-Zg%*qaOe2rmwcFjt@GF{4GN-@ z>qnm7AF3*rRH7%JhLJ?^?*q&xF(8)5+_>MJ*N=-6Eyu2NvPWlZ_lc6hqdgv5y0T%{ zNqoA<1)qvY^|}s})7Cb2($Wb2cpA4`V>-*?6rc2=yN|R?ReovCyG5kY24P#-mu5gf z;JKv`R0C~fp%!zuJk$RT721o_8y_jHjKUCg!RmfOyE|QBIQl0;3#KlU<40Kz&<9b= zzaWOP%UP@W5Xq_o_=)*-4pspYp>u;GWydHr=P&@`ubysK2@QCFehH>w zC%Ozh*_=9Yg==q28&83M&wHk_1dShIm%j-Y!Z#QjP{G)$u`Fwg`u8AblgBsAoJq$9 z{G6jmFtjL^wHbCsGy>BVzjxJH61h}x_O?^KdH_H}w$UH=$i%%Oi_`ZHdC{G8s3`U^ zI=|QhSPpRd)1Z&PCa07=D6#(?Zke583kq_LJnt4@WX>&Dp_IX@o6|=FE-c=6Y>URu z2F4`9%#K$u_5O64vf*4zn0!>ROKeTpjAedg2Wrr@NKhT30=B6L+HUf65qMY-5y#dK>=B=X{5D>Uf;jDbiW>ecg8GF>56mBeZ0rI61G17BD5_4 zC4H=6=}lmJ{nZhzYEAy$vYTyfvc@UM?$_+wY&f{Kh7Dz&<>0~>Si5w0x5{AggaY07 zGV<(XA#`b1SFmJGX^+y+5cmsbocUPG(_lPS3=#9ON(-;VrUgRZUsURH(mA}HK9aE| ziK3;6{&&AmovgRLY6F)<>~Bjk`=+SZME63ge8=-La)zx`3H8d@Py~MX19uF1=%gwr zGD`C|434v#^d{?5=(yb2GF6~$mGL{bSjoC2DHUb@@xETSVwf{~@z3=pzFq^zwCGCf z;G|T-PAwU&aG1)hHL&E$A0t)G$__A_6w1};tEKlsHk^x<))YH`xIj1ha+0?AQ^Pa*q8<+j~j z;*){rvXbYjtizh)OHv#$SIXB@?v#G5G{>MHqJ6ap*Ix!Cy3z(|4()$EJ7)t*s-(u! zn*{8-)#fBlE#7SwnayL1&p&1mO%n$q#OAW*(Y?)0E_@xh)mPdRtPYFctv6Td%nG8D zNH1hJH|t84%%OW)X`UVPqb6jB!`oKES64fWf4eQ=9onN%7mdg|mx`E#<(9~*jA}qw zaY9(NII5Zdy7j~&*kD4-WGV(YSXIpk%a1g>2MsX5v(kFyAjbiZdPR_ebM0X58)oTa z$E|cAl2bhO>vpZH5GoN$JiuyHu<OageDu&m$^^jq{$WVkS`j+*2Mo#fB90TfUUI3Q4(e4;+wa z0ToWd$-b;V35HAg-WrMP5*VE%YEK7qseGGP9!~1hjO4C4>(SK&&ti)BI9Ahmy@`VT zp0jA%S+wIThP~EZ^sSFI`J)bBNU%nZ{QqZ)6&tmu)8 z(yy)h+kV;ZdoJAzZ!}3jgVNwG7_t>M43?w|$fw0}r4d4^hndw>1I^eAXU-Z*mF~Pm zJyN!|iIcW=RneU2$zT$AKxNqdwktzdQ?6=fTZJeXqlhG!prXF1KI^gS9@YHD_Ky?B zUu!o)NrEvbQkH35-I1?5&c=#PcHe!BlgzRW;;*U(A9^!eSgPDLux15&sDk#@O4t>% zXPBSeX_YjsD|FkNCz$+0#xqyWY`mAnJKn>%avZxvmVVKjMiX(;u@bt(I!Ig;7-~UB z>j1uaSsl_sqY)!y2OLm={DX4*?E+}DAVY5II}yWcV_cITHY;Slxlvfw2Z|~nYlFVL)#Egw?UTk!z{GNDBoyLdiML&4z6*{V4RJRiSbjAB2Y=HE zdk62h%sj9Z zp9L58t<7od8ytnYf0i<(KYI?TyePPq|9QHrkF3ffk#FvW&r)z2Zqfr+$DRsNz z&Cz#qO>lYp+7?Av=$sR9D*nx+uR9i%Y55z85iq7c?9tu&__Ao%B(IcKbF3D`J_)|W zZ;#)*j5htEuen0394ytG*KkGaorHArO;r;T+ z!x+talkK=mPo&6m}{_+TB2G;aCXQ@JZSk$Kd?R?POp0iZBF+l z0u6e)^1q6T`k)*9?`@lLTZ^`TZFxi1f~uh;a4-_Jf266))K0B8)4ur_vx7CkXs9)E za!`dV62z&LE`T|bv4mCHFzu1l@B8Ku_8r4aN&IyoYYwlbm6hG+qH}wrGHd${Fbz~K z&0SDMUP_{W@_lgfj`i5NOVyK-#?tvk=~knouO#>6w4s1-TBGpz{2sUgvGNp%L+vMK zf0B>d%>FMY^;cJO_g>Wy<8KMsuvkW8Ny)RnH?7s-K^HpyLV#F@l$c`-%yfjBvpy?h zkDN?FR=zTOgoEP6#5ZriB$5NK&6`a}Qnk2heL29UK#@9l!H7kro?ES)TkR4|)*ho! zHEWOw;1m@%V;!Sfy@IH5|Hd(#T)bpWlM<~Iyw1n(bfyrD`G~vlr$bWSy@;htI+dnZXw7?I5j{}xtbF#HAT+VnfUr7%yw;Oyk+FB3@2aw zgI3-!0Q&Nj^J$RgDzajjx+u?7)8-1Lmb6zyE-dX8rWQSetyClyo{3W0D`3vvw{Bm> z!Tww}_OW*EYoSn@T7+Y27L>|P+X+7gz5E(_`SsL~BQ>UD`TIxEtSF1{+|Y!J_e+;G zUbot~5h~;RMsxRh9q`GXF}uzZj(GuH&77)iK|XlmcdQ9w`5YsmbSr3)JN?psU2JAC zdtdqhN(x&UP+$1Vs8G?NgIr!VnkaQ}b05H1j<|XLltrxkZE`}lGo8pHnkGfuKO{F; zax`0`!V(~gh)oME%aNc}Vl>^zl2$Jqe)=Kzq(6Nyr+}xKY&Kx7nXFq_qzaS(Hgelx zZ@}{i7I0|)RG6fuB&7z1&6>gH)!zb9eI z_zFSKL#R46NyfP7&DuPAfx+hxc4(~%{Vwck(Pv2KHgtHIT$y#LSr<0LL0Me3wXNI` zqeJci^@vKM`+6)3qB00)C54dt?WS%yxxa*$c~NaTVlQ}kF~@2vB?y|XxKdoeq@2Z6 zvnH#X5DT6dn?xXxN#)uduMNf4g-_Me-aDISb`g}BsNFWvqn*{(Ad^P1P}M%KxP(Z4 z@IYc<1=rP6Y%+I%ct-xNvP_$_Myt9mCk9H{eKo;Ou_o=%8a2*``;yCVm^M%jlH!hu z7+yL?iCzpdC4;u4i6|qqhIJJul{(I=9m-OBlC5jwSK0``OH^3VU01m%1#H~&<4l*i z_0#TR&Ya(IRt#TsI`ihCgZa9)dg(gWL&7g;9coFW#KdnW=aT7-XjT{=NxyeUNOjQ0WPKD42#|&)lo{uJ{7W=jnPaKpT~zb z%uTmP`@$7#de0b20?U!IGA9~dHF!_9PbtSs+stNvlRMmi6g`qXHHcQ=y;zxtmk5F3{#w)2e+t(Y}Rt;DOsOA!qJEu^eiO+ql5}$oRLCP>q{I-I#%ShW0Kx2 zu`&1rnJw;Y)@No5e=d?$lc>m4BPSQRH}a0 zx85}hjEO(`(bzBe9_OW5iX)2!p{bSf*sJ%wcy0utJX!vIkSIUe zDwHZUS)Exgmgg2=hL=#G2#sd( zX9Ey$%E}N={F&{1xHXr(B4NQgT70GL%@1}JbP=C z^B(I%UU!h8C08MLw2U+}g-}nz@`CSIih9|DBI8|3tf3W5p801kDPOjTt39I>^AYRW zX%AB7e%P=!tQ>XRM4{}YoDzGFgreU+m+LBf?r(3?Ai|ACsP%v z7#qJmv2K6w>mKQDIhyJ?l7d^Kvs4^aBI21KGw|a9$=p`jtKZb-$Y>vWIR^zf?U2+V z7E>#joDD8+MAEJa!;?32r8?M&-%wn&R0L29S+#0jflowVzp3coBjO(-A{ru6sHdKZ z6a3rd**}Y2k;z|AE(tx4PTZ*T-~B;Z1l<9*%o` zOl)9Dq{fx4*GP~i7&056R?4nR7 z<4dU#PoPh~w&WZXND)FS8vYEXDk zOdwqUPC4DqFl12kUmuDK(S(akM8h|{nlYF8M&zQFULz#kxGvxEz?5J`*hwN~gU6RT znM6ZGuQZHVA;P3w5^O?Lvb1~^o6tBXm_iGBxPIP8_Pt(|E%WV*GsNu#O}=BXAGRlD zs4URQbA4Ig0uF)kiN@29^v0QIgbk@K)&L&}ABd_e9{N8UD)=^PxmrR>#qz8xG{ zGSQ|1Bu3=#Mq*%4seA@Mc2PJ>ziQ=y4eDfSQD3EK?aqI2(d%lwnAN29qG-_>j0hQw zHi|!x+d?#ou*4PiRF`ipziquY$G&f(=b$L=m;*+|Pv_^>pud*Q(7gn_229q&vQAKG z2fNv0@HwDAO1FkbS1hd_ZA3Fz0A7UtvM}Q(ZZ+`_d^zNB;PEB!k$2k0K2EV5C(^*J6c&$=3&2yjS zECLP2uQ$CvRQ3it!NATzRi}k05`&EW;4a5A$q$zTi@-x7;Ozzt!(cA zQ7sWhBZaANWooQMiM9n+K7P({-#!#S-|BMZiUq3_maM6IulyYZhRZYzG7Vr^q)KRk zh}uf31SQhGR(IaDG|(x zrlr;dx6&Zfv(hL+z(4Z^J>eTNbjw<&#_oTGS_c68F@_R#tx-EP$9r0%r}*?s)P}-d zmQy|~n*qIpO&6&juw8jt{euk>U%Bg{&SEzEG##20{(>qG^`V1F=t+|>kxGO)*;MKM zzh#SA>Ws>j=s&A*dFU{&q`7tofku(TZe@xd=3mk2`XjW8o{Gns_7JSL!q`6z2E)@I z{;@&Mj3fS-HGey)jTpb%bc`{mHoG?pgo)(osn_Whh=32$ja0NYUOEfXqolFkFTI=L zr0|Akazse$kAtG)$ecxEVLo|iH;u!5d$u{I-E@a$>XjOH9MK6-aT`BGM3VvMqo!dN zLWa!U5?k;WIVjAOn&6ch?v3LmbBgAIXQ*_=M-k;$(Y53qW0Ktq@_Lz>P?ec?)&$gI z^j@!WpFWU(c`W?>1UNFDClM{dtT|xb#ML_R(}yoYa5H+(2-p0oM=M)YXxh}4XCN3l zh$4S0HvU$OR23-;YoLG^MaNT!bPv<%{w|aX0gHInE`jeD$+eAPYW5uDSvKivQQbvC zkgG&mtHF^w_ECGi`dAg|MGQ9}%#mz8e;HC;n*9zCqOt`jep!&d=5g7uZwALAjR2Cg-F@*%BA_rd#Uw+ZJXM<9Gf zuzjh2&za5U-sD_~8e=z(oJVRxnyvXqQ@N7p?I@&cefci&=eZ@5)vd_O79`Xg6Dv%} zxq;@vNmvuzOLR@FnZgNfK5PNZ{d6gOQ-*S+SCV{knnXseYBaCRi{vV+wAPN&_EKbR zY77qHa%W?qMSSFlnm!2tRi#>ZI?k^E-964Cd&RXe>7bVl0Qxg;D^$lc+~P7;YwCucm_$iQWPJG4wUiffmcGfl;cz_2sall6gL$sZqAT(*4l{)G=uyxav@u z>coVe;8pH-{`AJV95s3IW%hsaXRPb;oz)riDjZ#*3>^l3e7sGVTV!{U%x0#le5*`H?88qWd#Esw@iu>;;-QO*g z<#&qPNACvpt?=+<^KZaGl+PrMjh->rH%2|# z6sMPPsJ>%Wa|fiu`?jUq>j@FS#gs#^O{5P_^hT3bCwT<)6khl1vx>oSV>o)U63E<; zIN~e2L8>zPF#jySh4p7qkFgM%Pwcii^uVS)t=bBCxdFuTDwOo>bHL168Af;?J+Nnl zw)MIDYp&-uBqY#oUx4M*0}AmoU;tI74)J7SJFT*LEz z5OeEM%fls$<+_r5USA01g(zXKo``K@o^0{c?<`|01OUmH1*k3MNq~2X_~=T=hlpO; zpniAhM;8w+M)an6SH1GqG84!yLb#GrcI>_nJROHZcBCTgPDi@_t7L>DT^%IAD^2K= z4UGHe-va%Rk8)@jFDR#_DnyB&h3Ok%bHMS_aS%s+zR?EN`<_7BJva8uJ80dTADvCB zgFBr4s8}bJ{=(v09=dXtt)2|i@T)iahm$Io&)|&R0@vpA6WVQCX``(LO7wzJW86FCMfDisiopD98cBb#06=P8Ic%- zsNfo1yODFktU=ZfO^8^^Q$_gIfqt!I=N}TAtbC|WZ7e9S8w~)rwEx=jOKYiuKXZE1 z-6>aqUCuc7W*tbr+F#zjA=-JqWTtx^c4&GYcBFg#%f$8Et;F>_8^!fHJ82cTS2Z*^ zyTRKSt~sbqB_)ikkt#wr^wBA6iqucjAz;e;c!C}TLvvz}7!|%W=^FMLvl$E`&#$&^nbnUT5m)x75y=r*4a8ZHQUQU@RvRb}tBp;^_jo+q zN9>m~5~Et;?bKDPh57@ZMm0zR(SONduGE5r*lQpCkYP(C5v9o*y6Ni(JJ%XtuG;A6#JlXg04)y7K=k)P>jQMuDHGQ=_Uw=J8-{aM| z=V@@spUh;s@$7AivB}+nRb42_{8b;U9Q*B#8o;Ed*pEW^h5r_lHl+a(k6pLI79Q*Za2C^Q>*nq_uKK-%$GMxZ@-PH`=Th?jQTixtQ@h5s3PA35S;5 zCjJaw-QA3i8^Q}8h>_G3(@t1R^KEZi;^Aq^&Bej5Oh>VLKfT7b1QnrdE1RhYX&+Dd zSB0}+mF6%N+nWd7GZ*|a{wPuEV=Dji1@+_ZsPATkJpQdr=*^gtly|%Nk*(`}t|JGL z6c1uJsg}Y0hMH5;-Oy`!{3u6_|0S39i*gW7g z99XjQPe~II93Gr?DNnxHT{n~Lj;)b+U7x050nVo^{tlT5W%)&(3v~#n)?nV&I+vy` z=dh{ctU*GWLrp;q?w)zYRkF9TIn28O2$Rq=tjOwzexF=<$3-lH{`947iuWF5;%@F| z9&K%pmK*LjQs*5wd_Hl1zAc#|op2|}m{MKcfIfE?sq=SR%JX*qgHr1J!~Z}yURb6} zbB#6NA4Hj7TZ;dm!XRE`Xg0>#MkdTAqnF`rXt z?9}lKR*_UPRjJ+^k^+Qg(}ds8X+IvOsh`KUJ_hqYZ@541%7ytwUgn8*UT*V0W!K{T zynL3A-a2;}hW{w!D4_jhY}`wS9oPE<*PG?O<&}|lcp_AWleBQcPTxp=|oHWu*q9V$mS7y+qVe`_Yy-gARx>AvXj*$iUh<>$?l-b zm;Zr3Mjp%9c~ZJ4VUb$Uy7D>W)th`ouHAP|v8A*zAaGy`O;peqk+ zB2HgT$mx5%2%metouqr+HCjB)8-9qEf8FnNdVrYf^qlK+;eHYNL^5pl3G*=U%_E)` z93NK_RLCyRFFT)Cx?SCiXRUDYsLMs6zLq7GLfzPZ)qwW}3FU62A7IfHh*66H3Nd+3 z@&hl!v~J|8wOTk6fUgua0_sCKlZ_Ml2NA;`(G(@vg&f)g^ZSh9(}U%y+b7tf9TyLb z#R!Z8nsA;LA@eJ;is1ld(_n;Uis=#6dvX#yNBVKQ=K*hm#gs z!z^{7kRv*I)w!DENix-uO3al#p8x|T!i@c+TwGwujh6uRV9#AAYVCy0JtwhgUh{0| zibr?q;x_fm(U1M46>=p!9POW0y1xH?Do_Od^@lI|2}Th}HY;vH7*5YRyB~dvfI-_Y zkXG2$>v8H4!3(-9#i@0_NCXqusVV3^=yHcr+}yNE#iaXonqe0-iV65 z-ifDO0)v3FE#I{r&hXwFl1_nFEWiAKU{PkF(V3+@;C?nUYv&)E?yW;;|F?bmf7P*; z|7`b5EqRH`_WMimzpe>Y%!qaXkR~-G+wnCAMqOA#hYuQ@*C?k3IkquN%}!68aXL#` zd=yv|r-GpLWkdnx1ItB99~nvvqTC3V-{|Dj6ZBD`k-6yZcWEdi*GJw;yR5eCA@sX^ zhpHXAhx0$L|A(Wi3~KXxx|9|uP%ISsqgZiwcZw4{xJz+&*A}O^dnpdV-CcsaYp~+( z0p9%Inam`ae97IrclVsLd-q`scpB~Ux+zWdD%E#bxhIh7%FAUl>ZjZwxIr~mqz+X*{7JcB((+_kT`f0)(yP&?h0b=s+Zf@<}RaWT@6g!dV*W}fN4it(7B zeTmH@gz{z}#OaOFyn=u9fzF4350dD#dG_NWwav@tr@$}ayRH0E1F(C`@HWS#FUR@k zUn189_q;sb(Qf&xGGE~QZKS#?cH5~jE8Al9624)~UCwvNYR>;mxHh(Lv!d0vzEthX z*Z<76pc-3p{Ds=}a2ZY#M^~@ck}hqlNU$P;khW&I-3_rBgF`|(csqWCG_p;Mrn4lX zGlQNyyf1LcOz?ToD95W19{E>eL|$G{g^6$9Ep{k6?UZ1B_i6ukGniTLbz~LV0sHjJ z&@&6CN@S>6v5F&48io4qY^+~cKW(X)f%NWhBs|7ptI;ircc|C0*nlC1ja~x|Rezdc zsuPlRs6o=VOk;8|EjA&RWxtidpPRK!21N|UC+Z3rJ)(>s_pvd3q!V)tBPQ&n?HZkz z1okWQYV{aW7|K4y>DoXuNqMd`Rxj%vcv38K4|@|QoAM`ztI}F-Kj&yh@+KDTn8b@p z&`G35ps@roggA1K4^EV$?fc07SHt`BovNSq8BWjT1+p*H?L%RD1XJBk=KIyj-MW=S zA`EZUSy?JVM~d&gsk0rHB91~oy4ELkVp(B*=(6+)iK?YE|Yk5s3fmcC%lFuiU9;+Rr4;?iUT2^F~|=2EBF5kk4uZ7uft|)0H~Xj z{q66^h=@OyM6Uco(40|)_fC$b6nZtT2e>lD$_gL|w2ltaHz0$t+Hq_utWG58Ycp9y6siq}8p_!pACd0t_(x}QJ_Uxq$j^D8{ma5OU+N@o`5nBJ7f zUfT^>Jb--&g^$+LW1n4(eVdy8TQ6n$!QTZ5&dE+&D(ArUb~lkful)SwOBBUqg~=C* zkiHhvjD@l}@$?nNf8!>Ntte33EMQ@Lkr&z?|F8pgI0HNOe^h&EhU`OrxZz4nRCU!~ zt7cJ?Xe);6o5VG&0>RcCxno$nw|9n9m8Xv0?e|n76#~PMe<0A&H$#kNj5v+=bn0=YQBkRX@9Wj z5&f~m1B=@Ae5`*%pJ2KML;CK_-n#|`r*h$Jp0*UZKnEu@3rIW!1Hgs&Vl^{6b>51p zDC3411TQWrdrD15w%Ty<4X1}tQ`MrHrip=@IC3u50lIjeCuWo3B_u|X|0Ws$gah7j ze3${da}knD{b)E~-gHs0+P5P4Z>{jxoT3`KWa2knGnY?oO5;1^ zVAOEpwy7`>EyA(?b-WOL?412kxkz4IvG6sfa_&PGg;lB*S%rjxN=M=0D?3I~*_K{= z$~jNb#q0`#*wydP{GA$q0Rq&{#{;rLkYq$2h7ZtD-R?aEnj_3e+8SJKXtTZvP1%xE z0sKO5Xm=Sg&RE8|0#Ff2AXe(iI+v?|Z?u%73Ay}I??(j4mXp>No0zdl@kDIg`kmGh zF+$WUMg;SIAh?8}R=Sc34?9DzJGo!bdGwgH8EN?=n=3zeO-M z;?ULL7?|#9l_i_$RT>toe(>>?9}_v-4ZRJX%yL zT2!ZjSDm(WW?~#oz-f=%vRQjNqUL$O@2wJHSovQ{rP(3DeKPuFBjFH36{g7nfTlM#hwKi_atpGGil{&{6%gL- zPaw#;UhAo^6rvrc6LaAFJAE_-znBZ=wtqlk&C{m|ZLk@=z_e@mZwo81_Okieultr& zz2J}4JGgwOUva6XV(?4Uyh0S1JVkz9i_6e}t7=ZOj~OV*o21UbFBkm2k~SjwI79<; zle1=uF4z$N!Lgqv$T-)B)#2K#)`n3n{m&rnM3E&?@!UTH1;_aa5rMi!g31Ck!kR*j z$}efCiiDUaTCS*9KH@#1HumV|E3p9Q-F_;DJlFYa!No zI>B<@HIC)y%~~w&Fj9Lpl__uO#$V4bB$XJ_Ti{CVaX9|KoK5gFDCcR5tC>iTD>Dz9 ze3xh?xk&HQtq$cS)u#N?_XECT&gw3&cr10eGxA z=h-~wk^FNG`o5ogI!DcH3D6R$()z9baq8XeK7BcTa8v$5?dqQxyJ_8b zb4M(T+8PWom#*S4nj&P=wN)$FV=F%prN;WNKlsjWso9kc0Zp3cdfO~+z*eY4_n;srH$i?vHK>!9707G5~_no9wTEkdw z?w6cCGbLVkQHutrHIewqBI3WR)e7y?0ZgaJc7KK}EjFHgdCvVCGr#Aa{5^jlzWLge zXt3ix?Kh^#kKs``-aX#FW47rx#PAzq9ea$|m)&|><(x>Q;j4I}Qz*-yAt zAs0XH7a!^^e5l=RCjW6)UD>s{d-~mT;OFDj4TLV!PT(z*cm9Yl9EQvjVab^9XSn=^nUgvMH2-?V^|-hT6Zj9d%)IgD z%mWv?hErYO6pJiwzO8Dx3-uFoK{RE=fTT>1ahD~J0Jod)#YH|NOa0x zy|RjJdML1v9&B*vD~KW1ny(OnWD2q_&{yl8XxZ%Qr8fqK2zl4K^;>G(o`((9nwcbV zt&tkBM=vp}{lQMWdZRaLXhK-{XA&L96*`LlhpR`!oTjy(x@-wp-{0FHM%dQ73*$X! zbm5k&9j5zG2$^|et^DCr;xt^P5onzq0`(S3XUB7Tbkmb7r(`u4SuO!6hJD%g>xfG< zM)C~9b3BqF6W*45kOJ}&s{b~0MYYI}$(2|V)X@Q#FFe~pzLdOKO$o9j8^AFl!Q8iRQFKxpN1c6B{%wwR;N$JI<@Qkd~{D!n}S zP4sm};X}X`MwnDU=w#|_3v3io(eIVYUqI+xiSr4;20ClHlwfCo(5AdrQHph6UzptA z&qUI<{Sy2Yz;A^h$4_57k!qcBYNe*mKIqCvM5mb$viHJV#op@;vj-GaER5z5^2m!rGn%!@iZqTpxH)-mV6YumyJZE@9 zB#JQFo;$k@8STKxgb~4^wzcIYfHPB*$-?AWGi|Pz;riL*09O3Nkx)1Z;~ZnJ8^v(@ z`SBC1Gedd~Y`4q2EWQD?3-;$p?Vob|&~L@omAo}tAE%8XT{*YylYbYpD$>Ps-txaIcXX?sr^1G57e%)Ht^vI0h_{FxnwNHJrRLF15fHVYcIN8~QVW80i9Uua1C<~;0bs#yR-*DIQ=Vc<87AGM1xl$^ z2PB#hAd1$Fwdp&+u8X^owLQQRFSmq)^L1i3u(tn!) z(Djf^nDCvSSzI+YsV?=;BdK%i7c^b6DoNmvO%ag*&kW}8s+-DRp9?q*lANFZCaHFS zj(iH{+~i6R6F~EHgG? zbC;gk&)q$nTTtUVUIq3vVVRCSEhuWqJnl9tO&ayM)QL%95H-K`!Zhcu7Re9lD7}Tn zgN8^UZm=A+p>v2^nTW;+sjOQGBxr9wdxzZvg|VqN;^i`g2uQp8s2Q+(dmgFX)DYp( z$X#ncN$wWNxQQbsxFDqC`xqC88N*C+?fK0&f<|F-?~2jOI-!A9_H_}3ak zt&tDB9ew>Cn{U!jY+3aj^nCN2<-P4P(Ow(0zZg)kC6cQWN54PwC$PLl$y--+VkB_R z-DRj-YG9sb@AoGy@v5XJ^s;Vx{(_`NNFW^rAw~uoZv?_AIQYaIl`r}iQE5A*gN4L+ z`p8h*_sxOP&yk_<)^1H1A@OxG5HII!%TeQfSnUVXXVCE@huO!JtNSnt)u?fJm-3&K zzp52_+xD;6paf?dzBuLno@T)upFNZfSZS6UyB^#zp!1~k+OVlASlUcq3}{%G0lrEE zjdeQTt8#uSHFuek-dtRiv2ij>(t+<$ESn!oQnb|=|)=|pTI##yOdtcbub2}?X zusBwy{9qz!!s}8PQOCK0zrn2_;pW57xtUqcyPkBCZAtr9+T`zT*Nr^X%y??O=ORQY zM2n>)1l9I*4f=h7ZZdRMsUYxgTTV;h7Mc#_ZqvtZ=Lz7jO^^c?7tYttboE&KA1|6= z;r_M^>e1P6@N&}xyv$_(e#lwTiMKrHJ;5z3*v(Z~8o=aF8rqq`GuHs1 zPGM3#kk&q4d5Jvl0vrq-%nWC!sy?og-|%)9$`yIy&lP?`-VAtUke(SW`In-+iGI7@ zi|*5^846Ycs%{2E0r|~C`#^(kWzxXD5Hh2AeGq+Hfto9j?l=jwA{-3<2UFf1PNEP^ zRQ~gUu^1`mnLIPDFmtJaCnl}m^)KnFn1ajV8$mVhV=-^MPb7sl2GJP!;QU7C6^I*m zDp#ReYB@45j|>idYRnNpcE_U?aLb{9Q1ut$IbTTd#|l3Z#J1lMlAZ~VKAnyOyT5w9 z6zinCS&(M-zbC3p29fot&(Ie(s{>V{f_q&6XcMx-A`E12S;63<%zpJd)g@%@aZrD> zCd9fGXvL%EDlSQ*=7#2?_=2=OAmpMFBX4Qr@!?pbgc);h?7!x(rp;fI4yw+`jP|m{ z@CebDFCwW;KWZ67CG8|@YTtAlq^!3^!YS7QWh`P>tL57GPqX*weVEzxb!Fiw92$M7t&T`@Agv`{AYNl%>a*f@lM%;cS4ZsF3F95HqUmj- zuH{jC3F1kBTf$hSfRgRDl?j@X3wcuzg|-`f8_fc|nF5M2#JN9ZKtFpm7Qe*=>LW96 z{70#wC+ls@M3dZ-O=B1&J@hINYMJm;|1)rd|ZHHiIkT` zVQ*!zwwQb36yR!77MlnDDi=$9Kk9&E5SYNd9a4S|(8qFV_knR^M11n~yR$d7Rl2tA8%5}l!LscAo{e7oJHohLv%`Qz<|+jE zfFK!0d&-*n_y;IQ|K}{Igrbmie*@82iE$TLJtjn+iMuJZ;IB7-m0nxE(31Kem@6UD z{P=Flf+IU!n8+Q?#d^_8fAJn}qDFt%=84(1km1$BzP0ee3jtA-2 zvCv?9fTFlcR8o7L7~xpTJDcWjg|+%X^L+Sf7^P9Qf&kUdh1|=IQN{tg8Q<|1FX-Kl z{w5*s-2kn=26og4`F7@|c45iMOeS3IRB388b0-#*2_LbLZza4>#CnWGqs{}1|3%PP zDT)N|!j$n6n}%Q-s8AQsnGaJ?`eYP+UGYqVcjlWvTISdT!qfO?VQfE-VPjvEhMF#=-kmUXa6K zw_aeL3P{WvJr5YD^IA2A&R+4RXNlYD;5hi!M}=V7n!m(I39qjXJ_~&HdWu3s{M+RG z<|BJj^}VKHB3_e~fYvLDD2CxaYl?Zd8#sjEz7M_l?neox9{5zq3BJetMYU?fbt7YS z6xIc8^ZMb3lIbC{RDVj{br#$tY;U}u(NVByP@sru%Uh`k!LG!F$BJJ`^%x*$e^0fb zy(Ssjvp~|=>^_ekjg6-V>_csB%0jaQ>IrD3!t>+5Hu1iFmcsxHTTwJGw*y&vE6SGo zh~{3^6TTcJd4HjyT5G;6?Y^GrxI#=a)hI|Qb;k+k7YED@?&?v|^IJkwJdJ7hn%t!% zK@(Ny$Cq=@#&f>!zBfV>+Q-qUziVi?y1dA=_LAP%u=rs z!TSPkvG1c(thxDu9uOzGaf7QnttI-rhj!QgKZXI{MgIy0$EH<Qo}?`8Pb zQI;DtJ)wov!kfIqgW~}I4eGrCkBMqnX+9B)C2m=&ZFTAnDC?VmY4-|MQVg1$%;n?v z0hsy}ecnZT(wr)f*4;lca*`!UTN8fBVLD><7^CpRlZXE!g{)arRYqH8S8L_kw+3)} z(hN0dB!e$OWgw<+xgU?)QLrtA0{Rt`j*PHEy=-7tjiS=Fz>vHn|H!SeJT)PMqyA10 zDq+H1L)QZH`W6pA57?>n2vM>V)bO#;OG$dKFIV{ZLQg29C)WEro4TGBs|126_S>b= zR77^752RvE(okddUGg8wjAf#k5@84oIIx)6{XZ}EFTfOrvtBWm2 zpauamWy*O+bACL+a8yNojo;JM+VEyjiF5yr#AuLfNXxQzw$3>Bd{f_;l6SFS!8lmq z!_Sqpb!HAA2JUy)sKLD=9Bu<{zNqFTMkvUIH$F+SpJJ2aK4=}hIX!IgyrOkAhYHht zg|I9y4hy4)RDiB@L;ui~B{L!&1dqGtebRKwRPGoN_+T{eY$POh8&4r1WLq({h83Nv z9HcHnq)`=ER;dg0!{Ws3-tVxJSYI9frPSoIS-0m~Tk*TTl|=DY+>S+@Bh~g#)k%^v znVeT~wHmV>VLMed)VLccV5|h?gW=ol?0H}%6#ZmKT!PaYt$TDyE(LnIg*W6SGlJ^B zv&59C-0`3V37to6|Bb|vym~Y!+7WNO>SzsECnhpPgO4cP*DueVy)EPClR(?lI!kE&l!Lz`AteuPCAbD#SJ zKFM2S!h;lTydi>pydimfkTtNFnh1i(g|ws;Szu^d zS8V_F$dg_DjltK(zs=8go=E>X{1d@P`0!&Q%Va}c^N*9Tu^%^6Q`**_iN@KQFNaB5 zgTsU*@qBnr(Ogr-*ZT&2J-=amRMg@P4`@S35GNxGq2HD;!DN%=BZ?m1l`u*9D>PVa zij$imDOQ&DpvFd*4llLRAiwPqUh38)9&|C$7~O7qRtb)!fEkkN^PB~ufJyjq1GVe% zf4w35EJ)Spwe;L4A3WjN0+x~q^K1@D6)A__PL?fK*QIwQv_&wqN`RKl63QzRG<>pe zLce3;-CQqs&bD{9ghr#RMpU$amlfc=LB%U?)djjW1(#LksnHjTal0gY&~EdKa5I}3 zNy2Gu{imyQ9w@3oWAf&kBB<2h>hbts6Z7)WBY@*u{OcsLua}QJvlTf!h}2)z5h<2# zwQWLW8TlmT4VY+Ltoh=Z#HmSQz@hKiO|5C_%4uzE3E!Q}{7H%lDyN`d#oJP|p&8E> zay9G(+5$<~AT9lrdyedExdi*y?1!0m%UZ=Dv zu>%Lm_BQc-{37*fjCQ1D@PAe=wLretedH554iXY01fM2@HrQlLAXIn7?scb@UkLBr zuEt4;ZPykDa(0CQHYj``!EVTme};F#oIzkDGdwl5N)4$`(`}Xg`_JzT^p5*$r3R{5 z_Qk}#m*?bj2u9|Q;kfLt$%z?c`!6i+mj}~hx}P3Acz*3IHpPSJ5#*|PM^pR=Y?o6N zeT{`^T+F(6hv69|MZHhO2}9cS^vXyI17+6cgHf`DfEpZM^0OG`J0*=urv{D8GTp2b z?aKS`&?GoV$fl6VO%S}3C(tx0OY2uNW>tV-U(d#9X$aS0Ef)GX#qv+Q{`4e- zQmX;31V5LmIYL(Q*;$d;wYhM~^Ui(->v(xn&KV}nOwp(njM&rKhmmUoR|?hKzq>U# z`qke3q%Rwpp5X&Cx&IdSh(?z8x-0t^dj1L?^m(lUzg>MigvK`~%yLA?%Ip++G%D+@ z3hu+;jo*r~#Hm$@^fu9@X5*Bfv!SE8iWi!`Y&DN={kseFaoK88)cA44c^f`&`23u?-hke2ZNG&JS92=$7l>o0Ue=EUIB`K3`(`1z|Jk~jgNnJ{zInluyx zp`hFLKHo=a3a$=X*5pRHq)w4wxs9(#AJ~obd#N&3&f-hs^?gb37Ay~+Q1tTij^dOz zgi8X|xuy2#i3_<>vQm{dkOWLJQ1x1~Q#XV}t&6nGN=PwJ@~Z+Yh73#J+K#9~Ix_+N z^GpRvX!8t(;0Pw4+2v-goQI#@>PCHeW{ZyGZBp+}o3)U^cfL)CY3)MPyXr2LO#vUO z2V+R!B&XoKD0mKLZ88%XAm#OQ*n1JG{VXI}CgMWWTKtbgz1TW5|7)u@2ya0v*@jcE@PKN! zspm^THkr|(dydbs1BiWaFgYs+a@nkApwkpR^OW+o>{a*b5#66nqO z;nlwv`%XSoUIv40nJa6oa@#cJ(^6@S0TNtO8E;WEsbk>V^w36HD*TQ*zTT_1;Pve7;@D5mEYS+RcTyNQ?DSl&j-KlfYBM%c)6wHBdGE< z*=Z44s=AZUmo4h_q+Or%n!U3pg39%fCT{h}HT1YYFnUo|2eSnZP*dZkCb2_tQ{$=j z*Bx-H^&OL@q-XfFePv5bN4i%+12LqziB^~>d^YUVD^!%Dk;+eCr|Ojn3*Uh&1^wJI zUU#b|zWBD=$@iFX3Cx{Tl?CJ)T@{u@JH`1j~67aF&j ztA2iyI1;OwUVe`?A>qO)9Ic7S_%pz?owJ`e6bCF`S=w(ew{<7@l9OlW$8=uo=4o0g|yCZGb0a@v-w2Os@GOOq)5n%LH}KcfD`<# zegK$9V|VbA`xl=l=qnGC8&HSorM?%HnEwcT9{SV=ckfkwH6|*(!iwHl)eK|R6{_A` z{+M%^q38WIMM2aXfhI_Y8Xm>kJNoT|PEYex8d{Ru(t;juG)FHdm(G8XSPs9b&^HLkFf3Zi?LP)nV3a%R`j! zT<-d`tImT{j>vd^7eqzrM9%!0k~<6^aJqR{RC@x^I!W!n%w;K)scHj`BW*h?X=j4} z4&&6Lml7dY9_4n^y=0+P-mP}K6{{bweH&8jIT#Yqy6E0~7+W1ntU94M<#sAGi(H0x zJvjI`$HvHMeohUoe0OWn#=9xtsOcbdk}3m9WYXA^WfUG5BE+g=sSIz1>1tLz{M3PK z_efeuT$FT@6^61-;;TqdXZnaqIB_hA**MPMq*&mgCF>{%;&mdZIUMNC5Nkw_QkWnU z=7NJ{+vPYB&M4IG{(eeICqeF*e)!~2iqX%C*`%*=^4%G{=7N;_&*BwHjToSHCiV8I zJ-c7@bt;n2-;z%e6LdA2rXAfz92?%mb=!Ywm*2p?9YzE0)cbq_I00Wfq5?xicDgR4 zJC|Ke=a^nBX@-{iT@Ac_c$^NCBL!$G5wlY(m6e@R%00Z+FXb1h1|TX19qz>A+oK9Y zLxagQ2)OjAfefX}GR{59pSzI@K+a`fBStI8;kzqKHy#%QD`}g+cEif>+08n5=k-nUrFnD7f1Wfh+iB`+;>^pAMNn3HD3Fb zbYtVFCt53xZf~)|9xlrBZ}huLn_X@rB_f-*IQ8Ezo;_qgrUvC4|1#CcrYNk}F7)=R zy~OtUfo|XBO}7oCBZ}1Nf?aiYs{^jBWroYC;p?e~KB+i}ea1w5-@m5@pkbz=)^9|S z!{L6qEM2CA1ktVRvKmDoRyX&X#tcv;#${Z_=58hZmS^uzKM_cn2w0%OY37c=tHQ6@ zGZjFxa8UMnSJoP8O!!Wb{evQbew+2#heb=mM<|?=O``M2l7K_T`82(5UOu#21dP<8 zG+y=@y1~*{2B0NMORXy!>)&5NWeK<$RA)`h9pkQvoPK!{=^D5%NeDcEF*gwth!tq-K}kkncN?Q|l+ z_=zY|8k{RpLG+vv=;=5H(Zpe4!R_ph_8X==@%`u8LJd0_OB zw(q_qcMH8=X!x~Y7aHwii`P@5&&!E3<3U;P?kL@=J$qbWH?z#_F4r=UVR9;psNGLB zKoG}3KaIu375sq{EiAf0$JEMdZZT=QBW=BINW6CbT+#Z8hGcvPbj&zf}Hyh zHgiuOwK6?tEvpbQQzj75eUAX@QZA)Yt@3JQX0Z}JUipo=&ek$f1fR9g*sw*Cgx5U@ z*XgpKs<39K>PHjTZcK@#c6ye~@Kz=hWbN97=)291)|EW<`HRkI4a@+zid=e1P*~fr zDfPLUve&GXxug|gzC(HbJ)XX@vw_$~x<5Pps_d}9cuf-ft8h-=U5Bji!>p`vvVcZ8@pAzEP<;o{96u44&3|zuK=)7{ESvZVN(h+~=LJcgKcr zSy+p9yNOu-+lfl<7b~sn?T@eit<0zb!%(xYYeDg-Ax=tqxF z2l?%nugskrz9wVi9nL=Tj3szwn%l~^K-I{VKwE~~G?sd4zUmtD>UVUen%sa?eL4g= z7HTPfA$G3ZephuPC!ih3KjF6=(Cm6*lNTRX4rI;Wc1L;U%4c?KOM2%20Y3RK_gTs8XorDpPub|84$8%quKuEtCXM zTej5nMk(R|i%>G~iC+9)`AKg7a!cOu?9!clWFW?jTo-?Jxn^o1XaIA95x=E&Z9eLw z2PyfQpVt0K56D6(rNy&uY&=%xJe~0#%{XG{6TZ)wX^kfBC~O~>q&c`GJ)RLGX1CKv z1RBEgTJR)~!V^R&>OM`d@pkg@)`DeC-zG`S>cWd>G~Tm^WGaRzz{-cDTBsb)+piVGwTNb>)oIV6Delzd|V)%D~i zGiVXA4Byx&!U}#f0%qzW6vhb}6sQX$9_>dbPXt`!9(L6fsS9eoCch_~BQ6c!(e0ax zA&b~5ZCy6SU^ic3t)-k9FDFVrZlxl|>^G|V8i=`J!AXf%8$wx;W{L8n#Z$Nf%XU*m z+vCMvs+ad8;)16@^_VvRJ-Q)XL?IJt5SfuPcLHF;kg_aLtG7D{ibshe68z|KOS9sF zjDFIg#du}b*}-#t(vDhpYf>3aW87#k)_4^+kZq)j)2UiW)6MJ~uUiN$g}qGtZ{y>^h?Ud{a+BDa%0noI=Vr5Kp4_$( z+t0+P`R@~(q?aA5^g2R!KnM(#Hk8c8KvXTRQc1T+jIJxvGgu)5 zfs(r-u;4dQ0(&i74Q$+wk&*Gw;f{4=Hh+!0W=)+4~JnD?Xa!) z;7gAmHHBo?#mCiC{c)1`)Qn9Y-+HHi$$Sy{yZ$TO5rce3|nLL#fK(*CkH`{Y6tpT^l3EAF<)I`Z2g_XFL4 zO)Y`3u$&jPWm5)XkMO7A86e`Iw6{|gqe}5i&+qZ8yyESv)t&{O9b3!IC%Qe02RB1B zmaev6(Yik#|E<1d8R~4p4vdgxq2}%gFSzk#A6PauHH}ZnkrcDWbYeF}wOXbuFry1R z74QvT6!r?EpixZ7Vy32Mi;iBs!%5BZAG%&*vS_%GXRoc5867cRf67f4y8ixi4If~1 zJVazc`uFEgZ6lkWwZl+G&ddpR0iVBFLJ{uhMj;3C3l9^<$%~8&PvcZ;e1RWRMai?@ zYoDDc1=oqlNeDB#T>VH-B$Xq6(K0$8@D)%>3HQ~qJRcX-RqC}BOtaX5_!3m}X;gkl zU)grB6_1NxdN+v3_;$GPAYWr-d_n(uJ)<~tt>}b1JS;HKMoJ33#-KU;*XFF*yz8Rr zu28PrVcMK9I^jrLkyLE>6dFzydolbY_oA0jciCSQjNr~*97=JBm!CtDT%O9#>^0|I zWk{ir_1)LL0Z~iLfD3{|!SkpYI5X(J&9AD6R|B^r2QUJD&`hOJ&-b&oD{j7SjPMPl zK4P56vXHTHh4pjtRoen)Yh{3wr<+mV+m&Uk?sXoXj%RkXGg)onaBad2e2d`$ZL3y8HM<;s#FF^ zv?1Z_eU*V55B?=s;2GWNYKJ6O8+?~e)$I(QX4=t&jpBg6>{GM0oNG4m%2B^Pm++#TR~ z-AZn{2Z20Z&m3iqMal6tsMKb7gUfF?*}S;guOIErJtqf@XI$6Di9D~Cs~%q@gqJ;Rq^@JAytqU1d|Y$eU=$q6oAU+D)XK`IzLOh;r~FKZCVTUj zSi#$JeH=rQ7?)J8l$giMltsS8#brO!|%>Yb>HN*&KjBg`Ufj;eG;jWh!4F{jd_1}N=xTE-= z4P!bPaUQFedNO4eMYYY)jcPi+$eDG+EnGL%llsVQ&scr?ZEPjltFbazivucv+UEHR-F47aZw%S_4H`C?DFlrlKb}RNPXTp zh>}WAwy+pZBKLMkU(F%CD2^E>LD#Mf?O;96?c3btTs5GrKQY|KCxTf6JxR4>MnBY8 z_$tQ$WCT?6B@KorWNcJUl-2ED`NP%~rd?jBOACv`%vqgUPP|R&E)NHMHRgMIMlTj6 z=?wMaoQ)8K&qFM1o|cld6?ME*PLk3U%6L=ld;$(q--mfQ>>&HEcVzxP5j07hd4L{l zJ|0`H5mU&Gk5DlQ&1V&Gh`&gs9dF3x2 z7omMX!#`OP?-Kz7?SOF7#-fY$SZU9Pji&VDmXDiO3uE^N!!(;&1s&%j`uY}X1V?h2 zGR$_AEjOG_NDaPMJBAWf{v-#7+`LM4gI*a_?wsHHRQsU;Q6Bk`r6F z=-aAl%2J6^1+AIZvneSD6XGSbKmV1^%&{!Nm_e%R z!JLcO;e^?N0<(j}g!q9oJ6%sRJKoXLVs?)V<|wA|2v=8&1|RHlA@k}PrZ~LH zcVI_&8Xr&!FYm(svh5UNg8(->+}LO$?pP)*`BweM{P4*+w;O>lXUvzG9=%HI^>(a2#&Za)h$X0^+v*7^E*3H05VL zs1~@oC!dPQGHgS4OgZ*HFz@GMgz=`U%E2A_IT5(@)O2D0ce{<{al?jD5B{*wl59H% zD&bdN)RtuWW;ialRB1KSX(l^mj|jP?2&6Jks%C16x#vgbo5KoY{tiQ+#OU69ar{I2 zN)kAvWcVYjIf;{?dTvZlcohaOO%D*sGwg0wRUA|(@x;N)S2B9+x%Ml4uRE9dubEx1zl-;u>xLzMRhRNJ*3?k-LNaN;1pL*5!6SlMi0-%P%6AfN!#Jqj8qE)N zz4N(nZ$@Od2>6@z67GKYBiRSrrd|A_#|>F8Bz2G6AtlOJi}}hteK@labA!?C!Z@Pi z%eJCFqsxu)|Ki6ECK z_+C}VQ8^pQyO+NHFXbuJL0^3qYThIsi39dz_jDaY6@T|u9-AuSo%^*#{o|V4Hn9%t z*mgakNjDO5qF3drPZv2S;kJ#UVD8Fk{^{L#s$}7y_wF*pO}y{La$XKKxmiobT$an) zLMJ04RzOTlkq*^Md~~5Xi$0_6SuK#u&5%4#6i+LwIji6|z@O22_nL&47R3#9hNZ8@ zpLYX`1|>Mxr=l`7ZOj+M?gB>!7SD!0kFS#*rWy^7+EUAM2>px)ER2G z!pO3Ubd7CT@uDIU(<>`tfyi097cB4X=nqx*R z^T%rU`yNt1v2uALY?AA$kB`RyUwz-uo8Pve{+9k?^NZIajQ(BCc%20q;GxyVtGz%T ze2T)3L%_b4FWTlQZ}+|sitN}h85}G}leQ1MymKE*XU|EW{r!=3-$;F#{oLt*s@aKF1}1s9jOYle`Fc{_O5unE<$;S zIriZR!!uui_VMQx^Je(3@Wayj;gdQGiqigSE9>Z5jQW|-v6D1obLqecjMu@1V4amZP9wT=Kj*Zfm`;I>xlsm9?*Z)rx53{d_^*nLTLZTu z*xU|C+{m-6)9J&Xne=~i!ms6&j#FYN$bX`_&Q0ywi%oo_{LEd=QWR}`nLCSrX% zQfm3Fsx}B*yD}YwrW49Q%m@b0%M*3~f4?gHsyndm;PpP+O{(EmMg)d-WJ7KPa&C^;&rZzgd`Pw{9nprb z8%o~G1b|4Tc)g7=C6vUpmu|V>3g6(|uIChm)%&YKIhIT1aE`oL>MenRkEFPSh+~pt z^{(V&gCQYs%j0^%!!yN}^BvU;BuplrC!X9PuMA=YIjByye+RzIk*zQ9LR4)|#EPBN zAEBQ#BG=!IgCzq*gQG-L;EA;bsdlL+(Q9F1tO3BbI1;~{f*Gs>ha;eVA_g3?efQwB zRgT%U7;Svau^MO7qE8yt=}bLEjc`;A;9XC6v?pta4Jt<3teknC0XFbBMRi(WIZt(_ zYB@ii^Eh)nZ(R`IA8@Fvexbl%`pww@w|ahf-qQ!YlQpmB9epBM2AaI;pxgi@=y^Xm z6#ZCAt?DL02dzv=RgHqO>aV}65%fguotAeIT6J?4kUnEYjb6*kg$6Q!QSf;_F-cPa zhJCp`EzHrjnNSEM3nru)uZsNMF5@fgF89T;(*^3B~h`?7WX=` zp{l@7CW-$+ht3LysMB>+j~i;BMhJPfUxe$;J_Ko75Wg`=Mw&Fwx#g*DhDp>i8lGIQ zuvd)h%USD$uz6GGH#kr|?x^6;+NEtmSpM064xbIsI5bjyO`Se+B~*orLj zc)YS43jR_HV0(X*5#48hg_ms=*v}}3V|-WnA#)YS)Li;cDN|5U87uM{5(wt3irFnL ze;=U56#s+WoOKDL?t{^-S_({MWcdZ=AGI3dP7=P>0?QzvGIQ(FQYXqmQaD3VXpr33 zP??(|_fZW(0Po5Z!gs+Ge;yui`p(w88_nixIm$zSL4gVEz=yxB(H`DLs-!Sil@Y93 zn70aW&}?Y|%YoBy-_rcu=H9{u$PbGphjce8>cT>V=xd^jxF^JHv(p_8M)U}2_O%26=EuE6HZDHXZAVRfs#+YL3r zN=S1|>QACfCKG{+#(K+d-;Qe;Log}|@OUn4zT>hSe!kyo?@8v+kr^&TNMX@MXx{peu3fwn6RertvJC0pR z;x89Fl5|}+GQ(fv65Mb#>;!(MyC9^@f~#|FBWKmn+vROPvU?acHgKSytZA8?PdLXT zFwQK8IZ9XMg$qWwUKvL(e$r?~8rA@m*+kN62E0oK6sa3aeETC{80NIcY>hLSQ1{Xb=z(gwZj&ySqE2Lvo}@OO7t3doa4YOQcJL?|%F1-aUKv zy!YJSIp=rkeSa}{?id$Y?i=yzV*+*uxEk8wu#;l8vF5P~W}fLwZM(Tr?9waua`SC} z9lklQ&gM}*R5p9ksjpwJ7ML;=*xN@-&$Uh&Ip_pDOPlqC9L-)UE6GC_DJy(>*R!fF zq#nxW)dn4k&aOUXIBLdfzwEqwT75MI(H|BZ>Dy2h9`_C@USJZQ6dic_BBX6hbU@XP z53b79;j^sb{|l@AqBBmEf9aj+A0}2@4%s)&4BJ}}Sd9U)u9O@mpA}r6i07UJb;`01 zYK$b{0ZWF?Hvw^ENFgDe1h&fs>5p{4(NEfdb^ldO`q=uYJ6$d=9 z6eq@`gCTEwl5IXJ{O4Bqaftk@3w3A}V(52c56|+y0?jO@`Xex0y@FPTV>`Rb9ptzA zG{QEEi?VpM08g9J>Ww-ZlkS;3Eng0m`1$Qx=Fco->3V!p^L@)ZE#;r2pGx(IKK`?8 z+ETZD0^5)!b!JyfU?Q6u^HXoi`#k~0JKcu1X$3^y3_F$>P;Rv!1-VhI<@w$%#*rFpVCM*$8Lz;Px8qSbm9G8Y5c$FFlSX?7`KHs{+N~n-`EwVFY zS|5l(iRc$b00Q`K<}Un=d5tMMyg93eY z@5<$ZIz&=(jX8IjcEn)m=3zy@=S{QXlMF<9sW%Q~OlDwW_*J!K?0xzXR`X6!+O3fV z7%!~&#{VxKVPOiXffApw4Zu-UBvSM=p!#Mnyi(ju(`LSr)3_+1r8>rh9!-$g)m(l* z>HX(Zxyo2}|GxDzswV%F#6n5M_b$$%H$dYB$k#-_J* zN6;vi5AJh^Lt_ItA5KgTuFqvkhein+VdEltJJiGUfhJi8v2%!x{E>a)NZKSq~qx&b1`%qJEG3|ZKQ z?83njj+d{OdgHfiLR)c&*U7~~edrU90n3bC(=(`}ks=*r_HOu1ihOhM;G$_s{p(J0 z_Zv!`HOO+X5=ub$Y|rApfiHUI>A%J>IsZB9qhpq`mXwPj@{Kc^o6~Eo9Pdq`F7nm* zlt?n1&FL_4e=H6r!_ViljBd~)q^;-a@0as5X{gmmsrx2&=|XQ-0~fVUb>SMI*=1Tu z%_ZvV!sM3b3_GUK=inyC9**)nXp>FN;Q@h)oMMu;8Y*pC&$j)|Q6u3+=ZfeJLUSlh zL}jI&em!MWJjas8RBD)BluQ$WI_z&fW~Q6wds5omIh-8=7-OXA%X+z{DyszC3|z0Z z@=!>z!-=gH?s`>u_liPj>H2#l3$)+(D;jcP0Bdouh+m$o-|Gic-pnZ2{e<{OdM%%v z6kYkr6Z)mW%$jKy_|Hy|1s)>{oo|`E}-A9fBP`` zy?dg*QyfSOa$n7oW3h6V7)BDmZnznf*AmWY!`k?vkPY(QWxu4yFiX(LNZI#d*#O`Z zHK&c6_t`KRiT^7?%9TJ#%bN`Ld62DwxwtL$h zn%cnGI6u58sEL00PDVf&4fkE=Xf#GOQ43SsjKDztu0zei5_E@exu^b~dK74{dDAYd zG)7kom3B; zSLE$>`hiW?$BV3SHvXT<(Fy}e<531&kDWm_nUWOFM`Yb|66@_ATN{d@vHV_^gRV}{gHBx-@cCje72-A z^l2In(<7{&WBBBf;B%=B1&79;SoW>+kx!oY+g0%Pi&wOEy72PS*v=cgnuCHul|t!g zZVrGV#9XqR&egs|xCOBN-FP~8v#1PE1cz3Lc|56^r?nH}%kvJ>$qDtq-d&AX93Mu^ zN*!5jR8Coqh%}TGjr9o(QtQsdhY8%T@D9U(Eu4JEU$0-riO+aE1Ju9kdN~9v>IO=> zw~T#&Ait)(bzWoKJ4w;x<#vVW+TAH!OSG`rS3hKtd&-Q*^VHZvCvQ1xkJS+ zqwk3fS9e|gy0JwdQ}b?Gn@<3r1(_crg4HL0+CxJT0ku~vX`Xkz zs9W33+#{hJsQ9Ip;9Zrop1a~d%Ri8mkgp$#4KUO#S_KkxhxpoiPtN}8!2X?NBGa2` zbwD=EHkRD7dLI$aN_Aq;--|f^(_H$;W+3Iv5ofA)vb_^>qzI-*l-N%#I;r9Ooe;uK zbyRa-RERzJmTS-*R>p^A70ZCA!2MU4C?DN^0sJj}%6}z^sv=^i+mT5Ibr_4`> zapuO~=50{Zwli*ztIkH2G>^V6t~`_MCe!D4Nx4?MJk`4uAT&++nwt92TACwrA81i9 zOH5~GhP=+qM?{wU-7y{6=~ZX(6WF{4*~Z8tGHt`dI-eI=A96u;()P8z(=88;iYnrp zM@8_EJcm27|ed5PpX(P%G@H>*&nJX3(MyuNjz* zkCK$h>xW-<%M9U{1Z@4UCwC0qqGFas~Kmm@0d%0y4WD5NaWB))M;- zC1dnlJHZ~+ssPl`Ujr7`QT!~sWzK0n)3X~qO6ZtjRO)DiJ73-E1Qlk=!HP}Bu_T@U zh92*~oQ~0axb2*Ka?eKiqJ1oI3YEO*8VfTm6Bny{j~c}txMzZN7 zm=cQ^vVxhl@oT{mKB1W=0#Y1=av7}_Esosni3N{ZZ$`TC-PUS;K0@m;0FLweDBy&B zMcC2!g75ZQK^SQNmXY`smdtrPC3NPbwn}|XnaaELV0$e4g@~YOk z!haMFyV{Aw)L(Lq&-6>oqREMTEH_0=K{|9T7>X9WVf$1YBFn*IE;_*@ztfGZP+$RW z8kTtlIWcB27oe6|7AUt>NsYVR1Q4c_FhYB<>y>ey53yV@IZ6Eu`f>XovmCE=7kz_v zIMJ_#?&8&dy%wvFBmFd2XDbw~Ufatf!dFMT<;{<7sc*f~wH}^2HoABA8R8dZ2vXWu znpE z%aEnfkFqg}mPI^1Z*W#Tr$2lrbK9&rbR4ZhsQcB~RcvnQ&%qZx%##r*&x*R(f+<^4 zm0I;*o_e3Xc@eT!!nMaBNjh=)i7B&;QOP39!>1HB^{&qcimdm!p*ln+b|OVK`&>{t z0J1HVHiyRF3!|HlC54MKSjn@+cdn2Ct?ZwHdtFt69UU$5212A1^ubP7=B3f^hV}kw z9q%YM`Jj=|1xb42knEF47Fcj9ukb`(;TeS)ykW;M2W``Ik$M=UHr>|DO=n7R35Akhq>0I>Wld%Hm1|RLy|0{8?~ulkKEy_ZbX|pvEG+zY$Tssl^7NA|@os4r=GhGda~fU5nIWDln%=Ow}78k}CFWyQNli>vUN@`NP}Pt~I2Sts=S?&bLq~yvn={ zui+q73H5kI@JPBhxDO1?M0#Bx=ALFXN60RCQq>~(rM8`HkuT}H8a%Bp8!z{AekHd!wBu%k!I;`M}-n+kCBq}^VrfB6)R2Nq< z`dvZsqtF8#%)5gY8QCu@pIkq9@yuupRi(@*T9px1lu*sE-1|phWcxn{z&C8_YUV* zg!H49f(+SNBL#)nF%U@rF-603664=85%r+XR6;EH;--Uf zH0nKj9zM;YVFh30KDA)_z&9$J18%5lT50}z#aIbhC-KCOfut@^_dvBAC3TgYfemYAg{>1|K4Q7$aHazYT_enqPI z>k=o^{pgiQe!@izhKh=dtjyrLsWUH2QVl;mSxWwEdXwp^Q&xCXf%#6g%c?TNOMfaI zpLfhQK-ez+^do^Cnc-yJWp(y@f_EUDdnE0XYGM-8?H;g>W*3nB+1u(i1~_i>E$Em; zrm5|>*9iTB4>P;(N3H!2nbYVlO)Q3l*EnHL24lL%L|MZ>d3M-;%>1k+CqjQp=Bzj% zp@EGuks8z0zzRb+_X%r^=yrshGr%A&L1=G1(Z|J&ByUETCmaZe{Un~aT$rb3v%AnP z;UV0Z$GUQQkNuH#?u-LnK~&@n7|`D*e27;ANV)(M8sC_T>M@AY8u!eUSxXVeWF3|% zz9o=zo1QWI{qNoRcF}9!yML=~kJEp8w$%ZD2z3^Ru!c<+kZUCBt4T>ak-??ZBcjWQ z)6^0#iAdr}>wU;aO@4pJX49$_&Va15lWDOB#K3xgCYEW@G-uSWmq~k#;=dZ+j@((< zR$giqj{4h`uO-Uz?1QG}Q7n`MuC=zl*AooSE7zwTT_{Xks$~rJNezu3piTi>jC|b| zt`-h1@rpiiIqfyC!RrInrSmFeTWwCkMg1}QHX)p|Q=J+X4DNzBbBEMx-z*|z97q*F zv5s8nF=d^mv~`zGQ}Bq)PE+3^%1xnZtathQHGeWU_@=QQOZ#r1IpMkEY8cFl{Q*&2 zza~dcA&8FIISEg-sQ5^1(LQodMZW(2IgAfH>}_Tru5#?-cGXndiZa(B%g<0&MV?Sd zugecsd+ABX9Lvyi$=6xT3SG(pqzob-XeNTeufqzDtcT`Om9A4!%(*M2r{^&&nv7O^(>x#3*w{-PAG?7#An+@mV5$&0JMJ5$Vu zL}4-(F)`gQsfP)2BammYG%hWU@2=awSrFtLp3YX{Q#5b(j*%((yoBfGW)YW(>#{6@ z=jbT$euP#|@8kc!=T>;Z<3@m-f|$iq!)z?qzZHh$H*hEjdh=T!ZhS9BDdzf1FRgcj zFE9FJRdHFMFWQ))ozPV@iWeffm&_aG716DojKa@i9jC_o2RokZ8ab&5D|A_HFakt- z!@)jmamy;=;+Z2iXP5Xanght?9$Dd-#V<>e2*?b?vi?noM95C>sLl;BJ<+QmM?nL9 zVOt+MXF2q}@Se-A98!Go{E#Osgn)%E3OH- z2`VhKAnWS##K1*u)d>O$mH&%_D+@gQT$csf+v^wwPC`siZ5N~33pRaes3K*X6yb5% zsdR}v5|)B~8FA;k%>m;oM*zrqv%vI|d_BsmXo)M5SO zFAEzyas_&ME@8yCKZA9=&E=V0R74+{T*5DARppwU>XOuGNeP0-QTW^D<$oC|VCBz* zty%02fHjHG6N^z#98JVCw-18iD_)N>Ys@KR#MM`#GuVmfx?VU=PWM9{7>;WWp4*y> zE4%Y9XJ-q0ptQkL552`zzly{tsE8wC7d+{7=t^(qo)U@1RHSmm-2tvHs{jkyvBPbJ z&zm7A+zI&M@?!8c27f;*moSf_<$6eaHrI3tv$Wncvj37c{U}oDCHkYu9uJHJHmqcyS_;VO4Q)zKHz_I% ziEO!h>jWRdJ;%#PR~+k6`S^4gPa6gu>(N%#vBVX;n>(N~7INZlz)`J+WQB(vGt$Mh zeEIcDG`ZdG)(C|AcrUkYvWv=jz106QF`WFDYO(6q%>Ir-IbkZQ*sz86n=P_DDMCWR z8uJy5a!8%m@h@EUKrI_fkf{lZ{n0;Ww80&i&UCeV5ieEIGrNRuLtyu?bd|ZW%Sei+ zxa#wgd=moSJW!DJ7tQz6p?eIAiqA7&mj zPPJb`mBxc-@s;k%W8h)CW$bz1Kzhf{LpN=aO0TcIUT<*rGGc-+nc&7mlhbRPJNH{; zFV_APc4#h%I^sT2x%Zho1^ zLb<4yx%WOF8Xl=p44NL5daXDeL@)hfN;5JrD8+ni*r&J2Ts>TWpw+6(8t1gKh%7Io z>C#bt%WFu@O59Cj+~!|KKnKy6?0$Z2PwFdPl9>Ex&SP8~h5 z&}J+3H>dmQ`o&R!N}njCq=dXq=u<)O9Wc|_xY?n0$Uemm+k336V)&-nw&b^gf4Pd$ zkI^&yZgk24U(C}6 zM8BG<4vtOm(4cbMFrcXH1V&z#6?tK?MZ68#g7~^u3oZP>tjNSnx2z7)C#apd_WanQ zpZJ`G-F%lT1fuVA!G#@Fe@<>##2Ys?$B)gKcr4kK9O(DA^NYMH1}{JVz`|{G^~}`H zEiRqPy*}#04JV;DsI=?{&f0hh+1&o#rty0+8|VvBug$k;Ulscd%PNX7jsl;}Rpw%c zE(YA=)ds|VDsTvlEh3Ee^-cU-c;V`?>0p+RW-eq~-AL8&9r*7~=U`^@O&0nL+u*c9 zP^SW2W9a20#;owmfT<;<-D!2TNyFir+Vn5GztLNvrY=AKIi(AeB>}eSha%-TIkv8L zmsnNlTU!JKTi0E?CF+3bD>`U@&!npK<2RM)`%O*sxw7s+t=LR4X$c0jdNSaoA{)|! zy{>6?%~1TS^?r=%*%e}iL_UWaVMmiLI=gDq(Cg6a@smY_`YxhE0gNx{T5!an& z;C}?KAH_8{RDOrV#mWJF(Pm!}fdYGbM#ZJjglXbxk@owpIwP{9mD{IWe`om#j~3o$ z`c>Afhs595^egnbtoYw)#Q^Jr;lVsl>MwMiYIbcH8!5wSx5mq@<>?}6 zfSP`TeG+akCgas^xk=9eAX|=+zTe)5nEH@#Skv7bB;pg1K{iCADB6oXziPC2y=o$= zpqJk2s+KtDq-8y@qek%d2#6FQ4V{Y%VOqy4kL_#PdoKsh)~LXE&osQ^GRjIJ&ZazB zS)UkMmj!HZNHqjLfB1mnBPM!k6VqR!%QkI8$&n|*_6d#L)=tjAp-QQm-ix)Krs9rYIkcy8`Y zr#-S#kd~!Yl!1`@hM8^D&q`4SKTOHHa8pL+S1F%wk1E{$7e-ZnybLNZxBWh2RwM=YIWq&pM)ZS;9%a7ni`q(zuq5~XYgAkj4Y2Gu?ohzfo;FsSZyz=DQz`TTIAIXMq;>PkNRdWO3ErRBo(})>(#QA$0VQ?0pvA6Lb zuOBgHDjsN_{=}Q;S>D z9D%ypS$BM4I607Xn6o@IEVhV!%(Uv{@7rSw=94?Ggdl2VVL0Uuc@ L1<6VY!{Gk_z8=3{ diff --git a/Resources/AppIcon60x60_2.png b/Resources/AppIcon60x60_2.png new file mode 100644 index 0000000000000000000000000000000000000000..078c34d810e030257b42d41a9372f57de562b3be GIT binary patch literal 4812 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 literal 0 HcmV?d00001 diff --git a/Resources/AppIcon76x76_2.png b/Resources/AppIcon76x76_2.png new file mode 100644 index 0000000000000000000000000000000000000000..fb80be3196ae6da6670f3cb5aeffab7fb799343d GIT binary patch literal 6059 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 literal 0 HcmV?d00001 diff --git a/main.m b/main.m index 5a08d44..f696fc2 100644 --- a/main.m +++ b/main.m @@ -19,7 +19,6 @@ static const char *dyldImageName; NSUserDefaults *lcUserDefaults; NSString* lcAppUrlScheme; -NSString* lcAppGroup; @implementation NSUserDefaults(LiveContainer) + (instancetype)lcUserDefaults { @@ -197,9 +196,9 @@ static void overwriteExecPath(NSString *bundlePath) { NSString *bundlePath = [NSString stringWithFormat:@"%@/Applications/%@", docPath, selectedApp]; NSBundle *appBundle = [[NSBundle alloc] initWithPath:bundlePath]; bool isSharedBundle = false; - if (!appBundle) { - NSString *appGroupID = [NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"] firstObject]; - NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:appGroupID]; + // not found locally, let's look for the app in shared folder + if (!appBundle) { + NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:[LCSharedUtils appGroupID]]; appGroupFolder = [appGroupPath URLByAppendingPathComponent:@"LiveContainer"]; NSString *bundlePath2 = [NSString stringWithFormat:@"%@/Applications/%@", appGroupFolder.path, selectedApp]; @@ -353,7 +352,6 @@ int LiveContainerMain(int argc, char *argv[]) { lcUserDefaults = NSUserDefaults.standardUserDefaults; lcAppUrlScheme = NSBundle.mainBundle.infoDictionary[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0]; - lcAppGroup = [NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"] firstObject]; NSString *selectedApp = [lcUserDefaults stringForKey:@"selected"]; if (selectedApp) { NSString *launchUrl = [lcUserDefaults stringForKey:@"launchAppUrlScheme"]; From 3ea4bb4a2dc035ee1cd66ac2bae1e1f8e26045ba Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 5 Sep 2024 01:11:13 +0800 Subject: [PATCH 18/36] symlink preferences & app share lock --- LCSharedUtils.h | 5 + LCSharedUtils.m | 133 +++++++++++++++++- .../BadgeColor.colorset/Contents.json | 20 +++ LiveContainerSwiftUI/LCAppBanner.swift | 19 ++- LiveContainerSwiftUI/LCSettingsView.swift | 2 + LiveContainerSwiftUI/Shared.swift | 20 +++ Resources/Info.plist | 1 + TweakLoader/NSUserDefaults.m | 12 +- TweakLoader/UIKit+GuestHooks.m | 18 ++- entitlements_setup.xml | 6 +- main.m | 55 +++++++- 11 files changed, 276 insertions(+), 15 deletions(-) create mode 100644 LiveContainerSwiftUI/Assets.xcassets/BadgeColor.colorset/Contents.json diff --git a/LCSharedUtils.h b/LCSharedUtils.h index 2a07a36..64f2e5f 100644 --- a/LCSharedUtils.h +++ b/LCSharedUtils.h @@ -6,4 +6,9 @@ + (BOOL)launchToGuestApp; + (BOOL)launchToGuestAppWithURL:(NSURL *)url; + (void)setWebPageUrlForNextLaunch:(NSString*)urlString; ++ (NSString*)getAppRunningLCSchemeWithBundleId:(NSString*)bundleId; ++ (void)setAppRunningByThisLC:(NSString*)bundleId; ++ (void)setupPreferences:(NSString*) newHomePath; ++ (void)moveSharedAppFolderBack; ++ (void)removeAppRunningByLC:(NSString*)LCScheme; @end diff --git a/LCSharedUtils.m b/LCSharedUtils.m index d941daa..d447a1d 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -8,7 +8,7 @@ @implementation LCSharedUtils + (NSString *)appGroupID { static dispatch_once_t once; - static NSString *appGroupID; + static NSString *appGroupID = @"group.com.SideStore.SideStore"; dispatch_once(&once, ^{ for (NSString *group in NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"]) { NSURL *path = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:group]; @@ -77,4 +77,135 @@ + (void)setWebPageUrlForNextLaunch:(NSString*) urlString { [lcUserDefaults setObject:urlString forKey:@"webPageToOpen"]; } ++ (NSURL*)appLockPath { + static dispatch_once_t once; + static NSURL *infoPath; + + dispatch_once(&once, ^{ + NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:[LCSharedUtils appGroupID]]; + infoPath = [appGroupPath URLByAppendingPathComponent:@"LiveContainer/appLock.plist"]; + }); + return infoPath; +} + ++ (NSString*)getAppRunningLCSchemeWithBundleId:(NSString*)bundleId { + NSURL* infoPath = [self appLockPath]; + NSMutableDictionary *info = [NSMutableDictionary dictionaryWithContentsOfFile:infoPath.path]; + if (!info) { + return nil; + } + + for (NSString* key in info) { + if([bundleId isEqualToString:info[key]]) { + if([key isEqualToString:lcAppUrlScheme]) { + return nil; + } + return key; + } + } + + return nil; +} + +// if you pass null then remove this lc from appLock ++ (void)setAppRunningByThisLC:(NSString*)bundleId { + NSURL* infoPath = [self appLockPath]; + + NSMutableDictionary *info = [NSMutableDictionary dictionaryWithContentsOfFile:infoPath.path]; + if (!info) { + info = [NSMutableDictionary new]; + } + if(bundleId == nil) { + [info removeObjectForKey:lcAppUrlScheme]; + } else { + info[lcAppUrlScheme] = bundleId; + } + [info writeToFile:infoPath.path atomically:YES]; + +} + ++ (void)removeAppRunningByLC:(NSString*)LCScheme { + NSURL* infoPath = [self appLockPath]; + + NSMutableDictionary *info = [NSMutableDictionary dictionaryWithContentsOfFile:infoPath.path]; + if (!info) { + return; + } + [info removeObjectForKey:LCScheme]; + [info writeToFile:infoPath.path atomically:YES]; + +} + ++ (void)setupPreferences:(NSString*) newHomePath { + NSFileManager* fm = [[NSFileManager alloc] init]; + NSError* error1; + NSString* plistLocationFrom = [NSString stringWithFormat:@"%@/Library/Preferences/", newHomePath]; + NSArray * plists = [fm contentsOfDirectoryAtPath:plistLocationFrom error:&error1]; + + NSString* plistLocationTo = [NSString stringWithFormat:@"%s/Library/Preferences/", getenv("LC_HOME_PATH")]; + // remove all symbolic links first + NSArray *directoryContents = [fm contentsOfDirectoryAtPath:plistLocationTo error:&error1]; + for (NSString *item in directoryContents) { + NSString *itemPath = [plistLocationTo stringByAppendingPathComponent:item]; + + // Get the attributes of the item + NSDictionary *attributes = [fm attributesOfItemAtPath:itemPath error:&error1]; + if (error1) { + NSLog(@"Error reading attributes of item: %@, %@", item, error1.localizedDescription); + continue; + } + + // Check if the item is a symbolic link + if ([attributes[NSFileType] isEqualToString:NSFileTypeSymbolicLink]) { + // Attempt to delete the symbolic link + [fm removeItemAtPath:itemPath error:&error1]; + } + } + + // link all plists + for(int i =0; i < [plists count]; ++i) { + NSString* linkPath = [NSString stringWithFormat:@"%@/%@", plistLocationTo, plists[i]]; + if([fm fileExistsAtPath:linkPath] && ![linkPath containsString:@"livecontainer"]) { + [fm removeItemAtPath:linkPath error:&error1]; + } + symlink([NSString stringWithFormat:@"%@/%@", plistLocationFrom, plists[i]].UTF8String, linkPath.UTF8String); + } +} + ++ (void)moveSharedAppFolderBack { + NSFileManager *fm = NSFileManager.defaultManager; + NSURL *libraryPathUrl = [fm URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] + .lastObject; + NSURL *docPathUrl = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] + .lastObject; + NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:[LCSharedUtils appGroupID]]; + NSURL *appGroupFolder = [appGroupPath URLByAppendingPathComponent:@"LiveContainer"]; + + NSError *error; + NSString *sharedAppDataFolderPath = [libraryPathUrl.path stringByAppendingPathComponent:@"SharedDocuments"]; + if(![fm fileExistsAtPath:sharedAppDataFolderPath]){ + [fm createDirectoryAtPath:sharedAppDataFolderPath withIntermediateDirectories:YES attributes:@{} error:&error]; + } + // move all apps in shared folder back + NSArray * sharedDataFoldersToMove = [fm contentsOfDirectoryAtPath:sharedAppDataFolderPath error:&error]; + for(int i = 0; i < [sharedDataFoldersToMove count]; ++i) { + NSString* destPath = [appGroupFolder.path stringByAppendingPathComponent:[NSString stringWithFormat:@"Data/Application/%@", sharedDataFoldersToMove[i]]]; + if([fm fileExistsAtPath:destPath]) { + [fm + moveItemAtPath:[sharedAppDataFolderPath stringByAppendingPathComponent:sharedDataFoldersToMove[i]] + toPath:[docPathUrl.path stringByAppendingPathComponent:[NSString stringWithFormat:@"FOLDER_EXISTS_AT_APP_GROUP_%@", sharedDataFoldersToMove[i]]] + error:&error + ]; + + } else { + [fm + moveItemAtPath:[sharedAppDataFolderPath stringByAppendingPathComponent:sharedDataFoldersToMove[i]] + toPath:destPath + error:&error + ]; + } + } + +} + @end diff --git a/LiveContainerSwiftUI/Assets.xcassets/BadgeColor.colorset/Contents.json b/LiveContainerSwiftUI/Assets.xcassets/BadgeColor.colorset/Contents.json new file mode 100644 index 0000000..5b2f605 --- /dev/null +++ b/LiveContainerSwiftUI/Assets.xcassets/BadgeColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "16", + "green" : "153", + "red" : "242" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 9d06876..84addfd 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -80,7 +80,17 @@ struct LCAppBanner : View { VStack (alignment: .leading, content: { - Text(appInfo.displayName()).font(.system(size: 16)).bold() + HStack { + Text(appInfo.displayName()).font(.system(size: 16)).bold() + if uiIsShared { + Text("SHARED").font(.system(size: 8)).bold().padding(2) + .frame(width: 50, height:16) + .background( + Capsule().fill(Color("BadgeColor")) + ) + } + } + Text("\(appInfo.version()) - \(appInfo.bundleIdentifier())").font(.system(size: 12)).foregroundColor(Color("FontColor")) Text(uiDataFolder == nil ? "Data folder not created yet" : uiDataFolder!).font(.system(size: 8)).foregroundColor(Color("FontColor")) }) @@ -273,6 +283,13 @@ struct LCAppBanner : View { } func runApp() async { + if let runningLC = LCUtils.getAppRunningLCScheme(bundleId: self.appInfo.relativeBundlePath) { + let openURL = URL(string: "\(runningLC)://livecontainer-launch?bundle-name=\(self.appInfo.relativeBundlePath!)")! + if await UIApplication.shared.canOpenURL(openURL) { + await UIApplication.shared.open(openURL) + return + } + } isAppRunning = true let patchInfo = appInfo.patchExec() diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index db7fd02..6b7b8df 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -307,6 +307,8 @@ struct LCSettingsView: View { ] as CFDictionary) if status != errSecSuccess && status != errSecItemNotFound { //Error while removing class $0 + errorInfo = status.description + errorShow = true } } } diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 8464687..4f583cc 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -203,4 +203,24 @@ extension LCUtils { return ans } + + public static func getAppRunningLCScheme(bundleId: String) -> String? { + // Retrieve the app group path using the app group ID + let infoPath = LCPath.lcGroupDocPath.appendingPathComponent("appLock.plist") + // Read the plist file into a dictionary + guard let info = NSDictionary(contentsOf: infoPath) as? [String: String] else { + return nil + } + // Iterate over the dictionary to find the matching bundle ID + for (key, value) in info { + if value == bundleId { + if key == LCUtils.appUrlScheme() { + return nil + } + return key + } + } + + return nil + } } diff --git a/Resources/Info.plist b/Resources/Info.plist index fbdc47a..d8d4c04 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -67,6 +67,7 @@ LSApplicationQueriesSchemes sidestore + livecontainer livecontainer2 livecontainer3 diff --git a/TweakLoader/NSUserDefaults.m b/TweakLoader/NSUserDefaults.m index db7680d..fa1091a 100644 --- a/TweakLoader/NSUserDefaults.m +++ b/TweakLoader/NSUserDefaults.m @@ -17,18 +17,18 @@ @interface NSUserDefaults(Private) - (CFStringRef)_identifier; @end -// NSFileManager simulate app group @implementation NSUserDefaults(LiveContainerHooks) - (CFStringRef)hook__container { - const char *homeDir = getenv("HOME"); - CFStringRef cfHomeDir = CFStringCreateWithCString(NULL, homeDir, kCFStringEncodingUTF8); // let LiveContainer it self bypass - CFComparisonResult r = CFStringCompare([self _identifier], kCFPreferencesCurrentApplication, 0); - if(r == kCFCompareEqualTo) { - return nil; + + if(self == NSUserDefaults.lcUserDefaults || CFStringHasPrefix([self _identifier], CFSTR("com.apple"))) { + return [self hook__container]; } + const char *homeDir = getenv("HOME"); + CFStringRef cfHomeDir = CFStringCreateWithCString(NULL, homeDir, kCFStringEncodingUTF8); return cfHomeDir; + } @end diff --git a/TweakLoader/UIKit+GuestHooks.m b/TweakLoader/UIKit+GuestHooks.m index 6c4d9d7..908debb 100644 --- a/TweakLoader/UIKit+GuestHooks.m +++ b/TweakLoader/UIKit+GuestHooks.m @@ -198,9 +198,25 @@ - (void)hook_scene:(id)scene didReceiveActions:(NSSet *)actions fromTransitionCo } } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?bundle-name=", NSUserDefaults.lcAppUrlScheme]]){ - + // If it's not current app, then switch if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { + // check if there are other LCs is running this app + NSURLComponents* components = [NSURLComponents componentsWithString:url]; + for (NSURLQueryItem* queryItem in components.queryItems) { + if ([queryItem.name isEqualToString:@"bundle-name"]) { + NSString* runningLC = [NSClassFromString(@"LCSharedUtils") getAppRunningLCSchemeWithBundleId:queryItem.value]; + if(runningLC) { + NSString* urlStr = [NSString stringWithFormat:@"%@://livecontainer-launch?bundle-name=%@", runningLC, queryItem.value]; + [UIApplication.sharedApplication openURL:[NSURL URLWithString:urlStr] options:@{} completionHandler:nil]; + return; + } + break; + } + } + + + LCShowSwitchAppConfirmation(urlAction.url); } diff --git a/entitlements_setup.xml b/entitlements_setup.xml index 5217de4..d55fdf4 100644 --- a/entitlements_setup.xml +++ b/entitlements_setup.xml @@ -13,12 +13,8 @@ keychain-access-groups group.* - KeychainAccessGroupWillBeWrittenByLiveContainerAAAAAAAAAAAAAAAAAAAA - - - keychain-access-groups - $(AppIdentifierPrefix)com.kdt.livecontainer + KeychainAccessGroupWillBeWrittenByLiveContainerAAAAAAAAAAAAAAAAAAAA diff --git a/main.m b/main.m index f696fc2..3a483cf 100644 --- a/main.m +++ b/main.m @@ -209,6 +209,9 @@ static void overwriteExecPath(NSString *bundlePath) { if(!appBundle) { return @"App not found"; } + if(isSharedBundle) { + [LCSharedUtils setAppRunningByThisLC:selectedApp]; + } NSError *error; @@ -279,6 +282,12 @@ static void overwriteExecPath(NSString *bundlePath) { NSString *newHomePath = nil; if(isSharedBundle) { newHomePath = [NSString stringWithFormat:@"%@/Data/Application/%@", appGroupFolder.path, dataUUID]; + // move data folder to private library + NSURL *libraryPathUrl = [fm URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask].lastObject; + NSString *sharedAppDataFolderPath = [libraryPathUrl.path stringByAppendingPathComponent:@"SharedDocuments"]; + NSString* dataFolderPath = [appGroupFolder.path stringByAppendingPathComponent:[NSString stringWithFormat:@"Data/Application/%@", dataUUID]]; + newHomePath = [sharedAppDataFolderPath stringByAppendingPathComponent: dataUUID]; + [fm moveItemAtPath:dataFolderPath toPath:newHomePath error:&error]; } else { newHomePath = [NSString stringWithFormat:@"%@/Data/Application/%@", docPath, dataUUID]; } @@ -298,6 +307,7 @@ static void overwriteExecPath(NSString *bundlePath) { NSString *dirPath = [newHomePath stringByAppendingPathComponent:dir]; [fm createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil]; } + [LCSharedUtils setupPreferences:newHomePath]; // Preload executable to bypass RT_NOLOAD uint32_t appIndex = _dyld_image_count(); @@ -352,14 +362,55 @@ int LiveContainerMain(int argc, char *argv[]) { lcUserDefaults = NSUserDefaults.standardUserDefaults; lcAppUrlScheme = NSBundle.mainBundle.infoDictionary[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0]; + + [LCSharedUtils moveSharedAppFolderBack]; + NSString *selectedApp = [lcUserDefaults stringForKey:@"selected"]; + NSString* runningLC = [LCSharedUtils getAppRunningLCSchemeWithBundleId:selectedApp]; + NSLog(@"[NMSL] running lc = %@, selectedApp= %@", runningLC, selectedApp); + // if another instance is running, we just switch to that one, these should be called after uiapplication initialized + if(selectedApp && runningLC) { + [lcUserDefaults removeObjectForKey:@"selected"]; + NSString* selectedAppBackUp = selectedApp; + selectedApp = nil; + dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)); + dispatch_after(delay, dispatch_get_main_queue(), ^{ + // Base64 encode the data + NSString* urlStr = [NSString stringWithFormat:@"%@://livecontainer-launch?bundle-name=%@", runningLC, selectedAppBackUp]; + NSURL* url = [NSURL URLWithString:urlStr]; + if([[UIApplication sharedApplication] canOpenURL:url]){ + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; + + NSString *launchUrl = [lcUserDefaults stringForKey:@"launchAppUrlScheme"]; + // also pass url scheme to another lc + if(launchUrl) { + [lcUserDefaults removeObjectForKey:@"launchAppUrlScheme"]; + + // Base64 encode the data + NSData *data = [launchUrl dataUsingEncoding:NSUTF8StringEncoding]; + NSString *encodedUrl = [data base64EncodedStringWithOptions:0]; + + NSString* finalUrl = [NSString stringWithFormat:@"%@://open-url?url=%@", runningLC, encodedUrl]; + NSURL* url = [NSURL URLWithString: finalUrl]; + + [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; + + } + } else { + [LCSharedUtils removeAppRunningByLC: runningLC]; + } + }); + + } + if (selectedApp) { + NSString *launchUrl = [lcUserDefaults stringForKey:@"launchAppUrlScheme"]; [lcUserDefaults removeObjectForKey:@"selected"]; // wait for app to launch so that it can receive the url if(launchUrl) { [lcUserDefaults removeObjectForKey:@"launchAppUrlScheme"]; - dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)); + dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)); dispatch_after(delay, dispatch_get_main_queue(), ^{ // Base64 encode the data NSData *data = [launchUrl dataUsingEncoding:NSUTF8StringEncoding]; @@ -376,10 +427,12 @@ int LiveContainerMain(int argc, char *argv[]) { NSString *appError = invokeAppMain(selectedApp, argc, argv); if (appError) { [lcUserDefaults setObject:appError forKey:@"error"]; + [LCSharedUtils setAppRunningByThisLC:nil]; // potentially unrecovable state, exit now return 1; } } + [LCSharedUtils setAppRunningByThisLC:nil]; void *LiveContainerUIHandle = dlopen("@executable_path/Frameworks/LiveContainerUI.framework/LiveContainerUI", RTLD_LAZY); assert(LiveContainerUIHandle); if([NSBundle.mainBundle.executablePath.lastPathComponent isEqualToString:@"JITLessSetup"]) { From a276bde7ba2bf8d7e3eb0024b7e574835a235cca Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 5 Sep 2024 12:53:31 +0800 Subject: [PATCH 19/36] launch redirect & lc2 restriction & bug fixes --- LCSharedUtils.m | 22 +++++++-- LiveContainerSwiftUI/LCAppBanner.swift | 9 +++- LiveContainerSwiftUI/LCAppListView.swift | 33 +++++++------ LiveContainerSwiftUI/LCSettingsView.swift | 22 +++------ LiveContainerSwiftUI/LCTabView.swift | 12 +++-- LiveContainerSwiftUI/LCTweaksView.swift | 2 +- LiveContainerSwiftUI/LCWebView.swift | 26 ++++++++--- LiveContainerSwiftUI/Shared.swift | 11 +++++ LiveContainerUI/LCAppDelegateSwiftUI.m | 15 ++++++ TweakLoader/UIKit+GuestHooks.m | 57 +++++++++++++---------- main.m | 1 - 11 files changed, 137 insertions(+), 73 deletions(-) diff --git a/LCSharedUtils.m b/LCSharedUtils.m index d447a1d..595ec01 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -61,15 +61,26 @@ + (BOOL)launchToGuestAppWithURL:(NSURL *)url { NSURLComponents* components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; if(![components.host isEqualToString:@"livecontainer-launch"]) return NO; + NSString* launchBundleId = nil; + NSString* openUrl = nil; for (NSURLQueryItem* queryItem in components.queryItems) { if ([queryItem.name isEqualToString:@"bundle-name"]) { - [lcUserDefaults setObject:queryItem.value forKey:@"selected"]; - - // Attempt to restart LiveContainer with the selected guest app - return [self launchToGuestApp]; - break; + launchBundleId = queryItem.value; + } else if ([queryItem.name isEqualToString:@"open-url"]){ + NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:queryItem.value options:0]; + openUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + } + } + if(launchBundleId) { + if (openUrl) { + [lcUserDefaults setObject:openUrl forKey:@"launchAppUrlScheme"]; } + + // Attempt to restart LiveContainer with the selected guest app + [lcUserDefaults setObject:launchBundleId forKey:@"selected"]; + return [self launchToGuestApp]; } + return NO; } @@ -172,6 +183,7 @@ + (void)setupPreferences:(NSString*) newHomePath { } } +// move app data to private folder to prevent 0xdead10cc https://forums.developer.apple.com/forums/thread/126438 + (void)moveSharedAppFolderBack { NSFileManager *fm = NSFileManager.defaultManager; NSURL *libraryPathUrl = [fm URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask] diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 84addfd..2d6a1a7 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -182,7 +182,7 @@ struct LCAppBanner : View { }, label: { Label("Change Tweak Folder", systemImage: "gear") }) - } else { + } else if LCUtils.multiLCStatus != 2 { Button { Task { await movePrivateDoc() } } label: { @@ -483,6 +483,13 @@ struct LCAppBanner : View { } func movePrivateDoc() async { + let runningLC = LCUtils.getAppRunningLCScheme(bundleId: appInfo.relativeBundlePath!) + if runningLC != nil { + errorInfo = "Data of this app is currently in \(runningLC!). Open \(runningLC!) and launch it to 'My Apps' screen and try again." + errorShow = true + return + } + await withCheckedContinuation { c in confirmMoveToPrivateDocContinuation = c confirmMoveToPrivateDocShow = true diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 1880e5b..d401126 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -60,6 +60,10 @@ struct LCAppListView : View, LCAppBannerDelegate { LCAppBanner(appInfo: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) } .transition(.scale) + + if LCUtils.multiLCStatus == 2 { + Text("Manage apps in the primary LiveContainer").foregroundStyle(.gray).padding() + } } .padding() } header: { @@ -94,23 +98,24 @@ struct LCAppListView : View, LCAppBannerDelegate { .navigationTitle("My Apps") .toolbar { ToolbarItem(placement: .topBarLeading) { - if !installprogressVisible { - Button("Add", systemImage: "plus", action: { - if choosingIPA { - choosingIPA = false - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + if LCUtils.multiLCStatus != 2 { + if !installprogressVisible { + Button("Add", systemImage: "plus", action: { + if choosingIPA { + choosingIPA = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + choosingIPA = true + }) + } else { choosingIPA = true - }) - } else { - choosingIPA = true - } + } - - }) - } else { - ProgressView().progressViewStyle(.circular) + + }) + } else { + ProgressView().progressViewStyle(.circular) + } } - } ToolbarItem(placement: .topBarTrailing) { Button("Open Link", systemImage: "link", action: { diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index 6b7b8df..1c30270 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -25,8 +25,6 @@ struct LCSettingsView: View { @State private var confirmKeyChainContinuation : CheckedContinuation? = nil @State var isJitLessEnabled = false - // 0= not installed, 1= is installed, 2=current liveContainer is the second one - @State var multipleLiveContainerStatus = 0 @State var isAltCertIgnored = false @State var frameShortIcon = false @@ -42,19 +40,13 @@ struct LCSettingsView: View { _apps = apps _appDataFolderNames = appDataFolderNames - if LCUtils.appUrlScheme()?.lowercased() != "livecontainer" { - _multipleLiveContainerStatus = State(initialValue: 2) - } else if UIApplication.shared.canOpenURL(URL(string: "livecontainer2://")!) { - _multipleLiveContainerStatus = State(initialValue: 1) - } else { - _multipleLiveContainerStatus = State(initialValue: 0) - } + } var body: some View { NavigationView { Form { - if multipleLiveContainerStatus != 2 { + if LCUtils.multiLCStatus != 2 { Section{ Button { setupJitLess() @@ -76,16 +68,16 @@ struct LCSettingsView: View { Button { installAnotherLC() } label: { - if multipleLiveContainerStatus == 0 { + if LCUtils.multiLCStatus == 0 { Text("Install another LiveContainer") - } else if multipleLiveContainerStatus == 1 { - Text("Second LiveContainer Already Installed") - } else if multipleLiveContainerStatus == 2 { + } else if LCUtils.multiLCStatus == 1 { + Text("Reinstall another LiveContainer") + } else if LCUtils.multiLCStatus == 2 { Text("This is the second LiveContainer") } } - .disabled(multipleLiveContainerStatus > 0) + .disabled(LCUtils.multiLCStatus == 2) } header: { Text("Multiple LiveContainers") } footer: { diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index 9a7150b..bae0d10 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -83,11 +83,13 @@ struct LCTabView: View { .tabItem { Label("Apps", systemImage: "square.stack.3d.up.fill") } - LCTweaksView(tweakFolders: $tweakFolderNames) - .tabItem{ - Label("Tweaks", systemImage: "wrench.and.screwdriver") - } - + if LCUtils.multiLCStatus != 2 { + LCTweaksView(tweakFolders: $tweakFolderNames) + .tabItem{ + Label("Tweaks", systemImage: "wrench.and.screwdriver") + } + } + LCSettingsView(apps: $apps, appDataFolderNames: $appDataFolderNames) .tabItem { Label("Settings", systemImage: "gearshape.fill") diff --git a/LiveContainerSwiftUI/LCTweaksView.swift b/LiveContainerSwiftUI/LCTweaksView.swift index c63e76d..f6ab917 100644 --- a/LiveContainerSwiftUI/LCTweaksView.swift +++ b/LiveContainerSwiftUI/LCTweaksView.swift @@ -121,7 +121,6 @@ struct LCTweakFolderView : View { } .navigationTitle(baseUrl.lastPathComponent) - .navigationViewStyle(StackNavigationViewStyle()) .toolbar { ToolbarItem(placement: .topBarTrailing) { if !isTweakSigning && LCUtils.certificatePassword() != nil { @@ -445,6 +444,7 @@ struct LCTweaksView: View { NavigationView { LCTweakFolderView(baseUrl: LCPath.tweakPath, isRoot: true, tweakFolders: $tweakFolders) } + .navigationViewStyle(StackNavigationViewStyle()) } } diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index f5b23cd..205850f 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -126,6 +126,24 @@ struct LCWebView: View { webView.setObserver(observer: observer) } + func launchToApp(bundleId: String, url: URL) { + if let runningLC = LCUtils.getAppRunningLCScheme(bundleId: bundleId) { + + let encodedUrl = Data(url.absoluteString.utf8).base64EncodedString() + if let urlToOpen = URL(string: "\(runningLC)://livecontainer-launch?bundle-name=\(bundleId)&open-url=\(encodedUrl)"), UIApplication.shared.canOpenURL(urlToOpen) { + NSLog("[NMSL] urlToOpen = \(urlToOpen.absoluteString)") + UIApplication.shared.open(urlToOpen) + isPresent = false + return + } + + } + + UserDefaults.standard.setValue(bundleId, forKey: "selected") + UserDefaults.standard.setValue(url.absoluteString, forKey: "launchAppUrlScheme") + LCUtils.launchToGuestApp() + } + public func onURLSchemeDetected(url: URL) async { var appToLaunch : LCAppInfo? = nil appLoop: for app in apps { @@ -156,9 +174,7 @@ struct LCWebView: View { return } - UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") - UserDefaults.standard.setValue(url.absoluteString, forKey: "launchAppUrlScheme") - LCUtils.launchToGuestApp() + launchToApp(bundleId: appToLaunch.relativeBundlePath!, url: url) } @@ -188,9 +204,7 @@ struct LCWebView: View { if !doRunApp { return } - UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") - UserDefaults.standard.setValue(url.absoluteString, forKey: "launchAppUrlScheme") - LCUtils.launchToGuestApp() + launchToApp(bundleId: appToLaunch.relativeBundlePath!, url: url) } } diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 4f583cc..aaba0c1 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -161,6 +161,17 @@ struct SiteAssociation : Codable { } extension LCUtils { + // 0= not installed, 1= is installed, 2=current liveContainer is the second one + public static let multiLCStatus = { + if LCUtils.appUrlScheme()?.lowercased() != "livecontainer" { + return 2 + } else if UIApplication.shared.canOpenURL(URL(string: "livecontainer2://")!) { + return 1 + } else { + return 0 + } + }() + public static func signFilesInFolder(url: URL, onProgressCreated: (Progress) -> Void) async -> String? { let fm = FileManager() var ans : String? = nil diff --git a/LiveContainerUI/LCAppDelegateSwiftUI.m b/LiveContainerUI/LCAppDelegateSwiftUI.m index 638ad39..f3da20c 100644 --- a/LiveContainerUI/LCAppDelegateSwiftUI.m +++ b/LiveContainerUI/LCAppDelegateSwiftUI.m @@ -1,6 +1,7 @@ #import "LCAppDelegateSwiftUI.h" #import #import "LCUtils.h" +#import "LCSharedUtils.h" @implementation LCAppDelegateSwiftUI @@ -23,7 +24,21 @@ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(N NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:urlComponent.queryItems[0].value options:0]; NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; [NSClassFromString(@"LCSwiftBridge") openWebPageWithUrlStr:decodedUrl]; + } else if([url.host isEqualToString:@"livecontainer-launch"]) { + NSURLComponents* components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + for (NSURLQueryItem* queryItem in components.queryItems) { + if ([queryItem.name isEqualToString:@"bundle-name"]) { + NSString* runningLC = [NSClassFromString(@"LCSharedUtils") getAppRunningLCSchemeWithBundleId:queryItem.value]; + if(runningLC) { + NSString* urlStr = [NSString stringWithFormat:@"%@://livecontainer-launch?bundle-name=%@", runningLC, queryItem.value]; + [UIApplication.sharedApplication openURL:[NSURL URLWithString:urlStr] options:@{} completionHandler:nil]; + return YES; + } + break; + } + } } + return [LCUtils launchToGuestAppWithURL:url]; } diff --git a/TweakLoader/UIKit+GuestHooks.m b/TweakLoader/UIKit+GuestHooks.m index 908debb..620de6b 100644 --- a/TweakLoader/UIKit+GuestHooks.m +++ b/TweakLoader/UIKit+GuestHooks.m @@ -101,6 +101,36 @@ void LCOpenWebPage(NSString* webPageUrlString, NSString* originalUrl) { } +void handleLiveContainerLaunch(NSURL* url) { + // If it's not current app, then switch + // check if there are other LCs is running this app + NSString* bundleName = nil; + NSString* openUrl = nil; + NSURLComponents* components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + for (NSURLQueryItem* queryItem in components.queryItems) { + if ([queryItem.name isEqualToString:@"bundle-name"]) { + bundleName = queryItem.value; + } else if ([queryItem.name isEqualToString:@"open-url"]) { + NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:queryItem.value options:0]; + openUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + } + } + + if ([bundleName isEqualToString:NSBundle.mainBundle.bundlePath.lastPathComponent]) { + if(openUrl) { + openUniversalLink(openUrl); + } + } else { + NSString* runningLC = [NSClassFromString(@"LCSharedUtils") getAppRunningLCSchemeWithBundleId:bundleName]; + if(runningLC) { + NSString* urlStr = [NSString stringWithFormat:@"%@://livecontainer-launch?bundle-name=%@", runningLC, bundleName]; + [UIApplication.sharedApplication openURL:[NSURL URLWithString:urlStr] options:@{} completionHandler:nil]; + return; + } + LCShowSwitchAppConfirmation(url); + } +} + // Handler for AppDelegate @implementation UIApplication(LiveContainerHook) - (void)hook__applicationOpenURLAction:(id)action payload:(NSDictionary *)payload origin:(id)origin { @@ -137,10 +167,7 @@ - (void)hook__applicationOpenURLAction:(id)action payload:(NSDictionary *)payloa return; } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?bundle-name=", NSUserDefaults.lcAppUrlScheme]]) { - if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { - LCShowSwitchAppConfirmation([NSURL URLWithString:url]); - } - return; + handleLiveContainerLaunch([NSURL URLWithString:url]); // Not what we're looking for, pass it } @@ -198,27 +225,7 @@ - (void)hook_scene:(id)scene didReceiveActions:(NSSet *)actions fromTransitionCo } } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?bundle-name=", NSUserDefaults.lcAppUrlScheme]]){ - - // If it's not current app, then switch - if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { - // check if there are other LCs is running this app - NSURLComponents* components = [NSURLComponents componentsWithString:url]; - for (NSURLQueryItem* queryItem in components.queryItems) { - if ([queryItem.name isEqualToString:@"bundle-name"]) { - NSString* runningLC = [NSClassFromString(@"LCSharedUtils") getAppRunningLCSchemeWithBundleId:queryItem.value]; - if(runningLC) { - NSString* urlStr = [NSString stringWithFormat:@"%@://livecontainer-launch?bundle-name=%@", runningLC, queryItem.value]; - [UIApplication.sharedApplication openURL:[NSURL URLWithString:urlStr] options:@{} completionHandler:nil]; - return; - } - break; - } - } - - - - LCShowSwitchAppConfirmation(urlAction.url); - } + handleLiveContainerLaunch(urlAction.url); } diff --git a/main.m b/main.m index 3a483cf..8ffa68f 100644 --- a/main.m +++ b/main.m @@ -367,7 +367,6 @@ int LiveContainerMain(int argc, char *argv[]) { NSString *selectedApp = [lcUserDefaults stringForKey:@"selected"]; NSString* runningLC = [LCSharedUtils getAppRunningLCSchemeWithBundleId:selectedApp]; - NSLog(@"[NMSL] running lc = %@, selectedApp= %@", runningLC, selectedApp); // if another instance is running, we just switch to that one, these should be called after uiapplication initialized if(selectedApp && runningLC) { [lcUserDefaults removeObjectForKey:@"selected"]; From 48a12a8da77d3349164c9d99b61e63da6d458655 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Fri, 6 Sep 2024 01:11:57 +0800 Subject: [PATCH 20/36] fix preferences & installation issues --- LCSharedUtils.h | 3 +- LCSharedUtils.m | 65 ++++++++++++++++-------- LiveContainerSwiftUI/LCAppListView.swift | 16 ++++-- TweakLoader/Makefile | 2 +- TweakLoader/NSUserDefaults.m | 34 ------------- main.m | 57 +++++++++++++++------ 6 files changed, 102 insertions(+), 75 deletions(-) delete mode 100644 TweakLoader/NSUserDefaults.m diff --git a/LCSharedUtils.h b/LCSharedUtils.h index 64f2e5f..df5e552 100644 --- a/LCSharedUtils.h +++ b/LCSharedUtils.h @@ -8,7 +8,8 @@ + (void)setWebPageUrlForNextLaunch:(NSString*)urlString; + (NSString*)getAppRunningLCSchemeWithBundleId:(NSString*)bundleId; + (void)setAppRunningByThisLC:(NSString*)bundleId; -+ (void)setupPreferences:(NSString*) newHomePath; ++ (void)movePreferencesFromPath:(NSString*) plistLocationFrom toPath:(NSString*)plistLocationTo; ++ (void)loadPreferencesFromPath:(NSString*) plistLocationFrom; + (void)moveSharedAppFolderBack; + (void)removeAppRunningByLC:(NSString*)LCScheme; @end diff --git a/LCSharedUtils.m b/LCSharedUtils.m index 595ec01..015cd49 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -147,40 +147,65 @@ + (void)removeAppRunningByLC:(NSString*)LCScheme { } -+ (void)setupPreferences:(NSString*) newHomePath { +// move all plists file from fromPath to toPath ++ (void)movePreferencesFromPath:(NSString*) plistLocationFrom toPath:(NSString*)plistLocationTo { NSFileManager* fm = [[NSFileManager alloc] init]; NSError* error1; - NSString* plistLocationFrom = [NSString stringWithFormat:@"%@/Library/Preferences/", newHomePath]; NSArray * plists = [fm contentsOfDirectoryAtPath:plistLocationFrom error:&error1]; - NSString* plistLocationTo = [NSString stringWithFormat:@"%s/Library/Preferences/", getenv("LC_HOME_PATH")]; - // remove all symbolic links first + // remove all plists in toPath first NSArray *directoryContents = [fm contentsOfDirectoryAtPath:plistLocationTo error:&error1]; for (NSString *item in directoryContents) { + // Check if the item is a plist and does not contain "LiveContainer" + if(![item hasSuffix:@".plist"] || [item containsString:@"livecontainer"]) { + continue; + } NSString *itemPath = [plistLocationTo stringByAppendingPathComponent:item]; - - // Get the attributes of the item - NSDictionary *attributes = [fm attributesOfItemAtPath:itemPath error:&error1]; - if (error1) { - NSLog(@"Error reading attributes of item: %@, %@", item, error1.localizedDescription); + // Attempt to delete the file + [fm removeItemAtPath:itemPath error:&error1]; + } + + [fm createDirectoryAtPath:plistLocationTo withIntermediateDirectories:YES attributes:@{} error:&error1]; + // move all plists in fromPath to toPath + for (NSString* item in plists) { + if(![item hasSuffix:@".plist"] || [item containsString:@"livecontainer"]) { continue; } - - // Check if the item is a symbolic link - if ([attributes[NSFileType] isEqualToString:NSFileTypeSymbolicLink]) { - // Attempt to delete the symbolic link - [fm removeItemAtPath:itemPath error:&error1]; + NSString* toPlistPath = [NSString stringWithFormat:@"%@/%@", plistLocationTo, item]; + NSString* fromPlistPath = [NSString stringWithFormat:@"%@/%@", plistLocationFrom, item]; + + [fm moveItemAtPath:fromPlistPath toPath:toPlistPath error:&error1]; + if(error1) { + NSLog(@"[NMSL] error1 = %@", error1.description); } + } + +} + +// to make apple happy and prevent, we have to load all preferences into NSUserDefault so that guest app can read them ++ (void)loadPreferencesFromPath:(NSString*) plistLocationFrom { + NSFileManager* fm = [[NSFileManager alloc] init]; + NSError* error1; + NSArray * plists = [fm contentsOfDirectoryAtPath:plistLocationFrom error:&error1]; - // link all plists - for(int i =0; i < [plists count]; ++i) { - NSString* linkPath = [NSString stringWithFormat:@"%@/%@", plistLocationTo, plists[i]]; - if([fm fileExistsAtPath:linkPath] && ![linkPath containsString:@"livecontainer"]) { - [fm removeItemAtPath:linkPath error:&error1]; + // move all plists in fromPath to toPath + for (NSString* item in plists) { + if(![item hasSuffix:@".plist"] || [item containsString:@"livecontainer"]) { + continue; } - symlink([NSString stringWithFormat:@"%@/%@", plistLocationFrom, plists[i]].UTF8String, linkPath.UTF8String); + NSString* fromPlistPath = [NSString stringWithFormat:@"%@/%@", plistLocationFrom, item]; + // load, the file and sync + NSMutableDictionary* dict = [NSMutableDictionary dictionaryWithContentsOfFile:fromPlistPath]; + NSUserDefaults* nud = [[NSUserDefaults alloc] initWithSuiteName: [item substringToIndex:[item length]-6]]; + for(NSString* key in dict) { + [nud setObject:dict[key] forKey:key]; + } + + [nud synchronize]; + } + } // move app data to private folder to prevent 0xdead10cc https://forums.developer.apple.com/forums/thread/126438 diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index d401126..b67172d 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -271,17 +271,27 @@ struct LCAppListView : View, LCAppBannerDelegate { } let decompressProgress = Progress.discreteProgress(totalUnitCount: 100) installProgress.addChild(decompressProgress, withPendingUnitCount: 80) + let payloadPath = fm.temporaryDirectory.appendingPathComponent("Payload") + if fm.fileExists(atPath: payloadPath.path) { + try fm.removeItem(at: payloadPath) + } // decompress extract(url.path, fm.temporaryDirectory.path, decompressProgress) url.stopAccessingSecurityScopedResource() - let payloadPath = fm.temporaryDirectory.appendingPathComponent("Payload") let payloadContents = try fm.contentsOfDirectory(atPath: payloadPath.path) - if payloadContents.count < 1 || !payloadContents[0].hasSuffix(".app") { + var appBundleName : String? = nil + for fileName in payloadContents { + if fileName.hasSuffix(".app") { + appBundleName = fileName + break + } + } + guard let appBundleName = appBundleName else { throw "App bundle not found" } - let appBundleName = payloadContents[0] + let appFolderPath = payloadPath.appendingPathComponent(appBundleName) guard let newAppInfo = LCAppInfo(bundlePath: appFolderPath.path) else { diff --git a/TweakLoader/Makefile b/TweakLoader/Makefile index 6a9f22e..1105f0f 100644 --- a/TweakLoader/Makefile +++ b/TweakLoader/Makefile @@ -6,7 +6,7 @@ include $(THEOS)/makefiles/common.mk LIBRARY_NAME = TweakLoader -TweakLoader_FILES = TweakLoader.m NSBundle+FixCydiaSubstrate.m NSFileManager+GuestHooks.m UIKit+GuestHooks.m utils.m NSUserDefaults.m +TweakLoader_FILES = TweakLoader.m NSBundle+FixCydiaSubstrate.m NSFileManager+GuestHooks.m UIKit+GuestHooks.m utils.m TweakLoader_CFLAGS = -objc-arc TweakLoader_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks diff --git a/TweakLoader/NSUserDefaults.m b/TweakLoader/NSUserDefaults.m deleted file mode 100644 index fa1091a..0000000 --- a/TweakLoader/NSUserDefaults.m +++ /dev/null @@ -1,34 +0,0 @@ -// -// NSUserDefaults.m -// jump -// -// Created by s s on 2024/9/2. -// - -@import Foundation; -#import "utils.h" - -__attribute__((constructor)) -static void NSUDGuestHooksInit() { - swizzle(NSUserDefaults.class, @selector(_container), @selector(hook__container)); -} - -@interface NSUserDefaults(Private) -- (CFStringRef)_identifier; -@end - -@implementation NSUserDefaults(LiveContainerHooks) - -- (CFStringRef)hook__container { - // let LiveContainer it self bypass - - if(self == NSUserDefaults.lcUserDefaults || CFStringHasPrefix([self _identifier], CFSTR("com.apple"))) { - return [self hook__container]; - } - const char *homeDir = getenv("HOME"); - CFStringRef cfHomeDir = CFStringCreateWithCString(NULL, homeDir, kCFStringEncodingUTF8); - return cfHomeDir; - -} - -@end diff --git a/main.m b/main.m index 8ffa68f..950a017 100644 --- a/main.m +++ b/main.m @@ -201,8 +201,8 @@ static void overwriteExecPath(NSString *bundlePath) { NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:[LCSharedUtils appGroupID]]; appGroupFolder = [appGroupPath URLByAppendingPathComponent:@"LiveContainer"]; - NSString *bundlePath2 = [NSString stringWithFormat:@"%@/Applications/%@", appGroupFolder.path, selectedApp]; - appBundle = [[NSBundle alloc] initWithPath:bundlePath2]; + bundlePath = [NSString stringWithFormat:@"%@/Applications/%@", appGroupFolder.path, selectedApp]; + appBundle = [[NSBundle alloc] initWithPath:bundlePath]; isSharedBundle = true; } @@ -255,19 +255,6 @@ static void overwriteExecPath(NSString *bundlePath) { // Overwrite NSUserDefaults NSUserDefaults.standardUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:appBundle.bundleIdentifier]; - - // Overwrite NSBundle - overwriteMainNSBundle(appBundle); - - // Overwrite CFBundle - overwriteMainCFBundle(); - - // Overwrite executable info - NSMutableArray *objcArgv = NSProcessInfo.processInfo.arguments.mutableCopy; - objcArgv[0] = appBundle.executablePath; - [NSProcessInfo.processInfo performSelector:@selector(setArguments:) withObject:objcArgv]; - NSProcessInfo.processInfo.processName = appBundle.infoDictionary[@"CFBundleExecutable"]; - *_CFGetProgname() = NSProcessInfo.processInfo.processName.UTF8String; // Set & save the folder it it does not exist in Info.plist NSString* dataUUID = appBundle.infoDictionary[@"LCDataUUID"]; @@ -307,8 +294,27 @@ static void overwriteExecPath(NSString *bundlePath) { NSString *dirPath = [newHomePath stringByAppendingPathComponent:dir]; [fm createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil]; } - [LCSharedUtils setupPreferences:newHomePath]; + [LCSharedUtils loadPreferencesFromPath:[newHomePath stringByAppendingPathComponent:@"Library/Preferences"]]; + [lcUserDefaults setObject:dataUUID forKey:@"lastLaunchDataUUID"]; + if(isSharedBundle) { + [lcUserDefaults setObject:@"Shared" forKey:@"lastLaunchType"]; + } else { + [lcUserDefaults setObject:@"Private" forKey:@"lastLaunchType"]; + } + + // Overwrite NSBundle + overwriteMainNSBundle(appBundle); + + // Overwrite CFBundle + overwriteMainCFBundle(); + + // Overwrite executable info + NSMutableArray *objcArgv = NSProcessInfo.processInfo.arguments.mutableCopy; + objcArgv[0] = appBundle.executablePath; + [NSProcessInfo.processInfo performSelector:@selector(setArguments:) withObject:objcArgv]; + NSProcessInfo.processInfo.processName = appBundle.infoDictionary[@"CFBundleExecutable"]; + *_CFGetProgname() = NSProcessInfo.processInfo.processName.UTF8String; // Preload executable to bypass RT_NOLOAD uint32_t appIndex = _dyld_image_count(); void *appHandle = dlopen(*path, RTLD_LAZY|RTLD_GLOBAL|RTLD_FIRST); @@ -363,6 +369,25 @@ int LiveContainerMain(int argc, char *argv[]) { lcUserDefaults = NSUserDefaults.standardUserDefaults; lcAppUrlScheme = NSBundle.mainBundle.infoDictionary[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0]; + // move preferences first then the entire folder + + + NSString* lastLaunchDataUUID = [lcUserDefaults objectForKey:@"lastLaunchDataUUID"]; + if(lastLaunchDataUUID) { + NSString* lastLaunchType = [lcUserDefaults objectForKey:@"lastLaunchType"]; + NSString* preferencesTo; + NSURL *libraryPathUrl = [NSFileManager.defaultManager URLsForDirectory:NSLibraryDirectory inDomains:NSUserDomainMask].lastObject; + NSURL *docPathUrl = [NSFileManager.defaultManager URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask].lastObject; + if([lastLaunchType isEqualToString:@"Shared"]) { + preferencesTo = [libraryPathUrl.path stringByAppendingPathComponent:[NSString stringWithFormat:@"SharedDocuments/%@/Library/Preferences", lastLaunchDataUUID]]; + } else { + preferencesTo = [docPathUrl.path stringByAppendingPathComponent:[NSString stringWithFormat:@"Data/Application/%@/Library/Preferences", lastLaunchDataUUID]]; + } + [LCSharedUtils movePreferencesFromPath:[NSString stringWithFormat:@"%@/Preferences", libraryPathUrl.path] toPath:preferencesTo]; + [lcUserDefaults removeObjectForKey:@"lastLaunchDataUUID"]; + [lcUserDefaults removeObjectForKey:@"lastLaunchType"]; + } + [LCSharedUtils moveSharedAppFolderBack]; NSString *selectedApp = [lcUserDefaults stringForKey:@"selected"]; From cb2dd17990a748cc13b01cbcb7083df7655ded6b Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Fri, 6 Sep 2024 10:01:47 +0800 Subject: [PATCH 21/36] being able to move dangling folders out of app group --- LiveContainerSwiftUI/LCSettingsView.swift | 56 +++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index 1c30270..8b4f531 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -11,6 +11,8 @@ import SwiftUI struct LCSettingsView: View { @State var errorShow = false @State var errorInfo = "" + @State var successShow = false + @State var successInfo = "" @Binding var apps: [LCAppInfo] @Binding var appDataFolderNames: [String] @@ -120,6 +122,11 @@ struct LCSettingsView: View { } Section { + Button { + Task { await moveDanglingFolders() } + } label: { + Text("Move Dangling Folders Out of App Group") + } Button(role:.destructive) { Task { await cleanUpUnusedFolders() } } label: { @@ -144,6 +151,9 @@ struct LCSettingsView: View { .alert(isPresented: $errorShow){ Alert(title: Text("Error"), message: Text(errorInfo)) } + .alert(isPresented: $successShow){ + Alert(title: Text("Success"), message: Text(successInfo)) + } .alert("Data Folder Clean Up", isPresented: $confirmAppFolderRemovalShow) { if folderRemoveCount > 0 { Button(role: .destructive) { @@ -304,5 +314,51 @@ struct LCSettingsView: View { } } } + + func moveDanglingFolders() async { + let fm = FileManager() + do { + var appDataFoldersInUse : Set = Set(); + var tweakFoldersInUse : Set = Set(); + for app in apps { + if !app.isShared { + continue + } + if let folder = app.getDataUUIDNoAssign() { + appDataFoldersInUse.update(with: folder); + } + if let folder = app.tweakFolder() { + tweakFoldersInUse.update(with: folder); + } + + } + + var movedDataFolderCount = 0 + let sharedDataFolders = try fm.contentsOfDirectory(atPath: LCPath.lcGroupDataPath.path) + for sharedDataFolder in sharedDataFolders { + if appDataFoldersInUse.contains(sharedDataFolder) { + continue + } + try fm.moveItem(at: LCPath.lcGroupDataPath.appendingPathComponent(sharedDataFolder), to: LCPath.dataPath.appendingPathComponent(sharedDataFolder)) + movedDataFolderCount += 1 + } + + var movedTweakFolderCount = 0 + let sharedTweakFolders = try fm.contentsOfDirectory(atPath: LCPath.lcGroupTweakPath.path) + for tweakFolderInUse in sharedTweakFolders { + if tweakFoldersInUse.contains(tweakFolderInUse) || tweakFolderInUse == "TweakLoader.dylib" { + continue + } + try fm.moveItem(at: LCPath.lcGroupTweakPath.appendingPathComponent(tweakFolderInUse), to: LCPath.tweakPath.appendingPathComponent(tweakFolderInUse)) + movedTweakFolderCount += 1 + } + successInfo = "Moved \(movedDataFolderCount) data folder(s) and \(movedTweakFolderCount) tweak folders." + successShow = true + + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + } } From 1ddcbbe7c2d857e038304680686612258367d943 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 10 Sep 2024 14:02:58 +0800 Subject: [PATCH 22/36] users can mark app as JIT needed --- LCSharedUtils.h | 1 + LCSharedUtils.m | 18 +++- .../JITBadgeColor.colorset/Contents.json | 20 ++++ LiveContainerSwiftUI/LCAppBanner.swift | 97 ++++++++++++++++++- LiveContainerSwiftUI/LCAppListView.swift | 2 +- LiveContainerSwiftUI/LCSwiftBridge.h | 1 + LiveContainerSwiftUI/LCSwiftBridge.m | 4 + LiveContainerSwiftUI/LCTabView.swift | 1 + LiveContainerSwiftUI/LCWebView.swift | 3 +- .../project.pbxproj | 12 --- LiveContainerSwiftUI/ObjcBridge.swift | 4 + .../Preview Assets.xcassets/Contents.json | 6 -- LiveContainerSwiftUI/Shared.swift | 10 ++ LiveContainerUI/LCAppDelegateSwiftUI.h | 1 + LiveContainerUI/LCAppDelegateSwiftUI.m | 9 +- LiveContainerUI/LCAppInfo.h | 2 + LiveContainerUI/LCAppInfo.m | 15 ++- LiveContainerUI/LCUtils.h | 1 + LiveContainerUI/LCUtils.m | 4 + 19 files changed, 177 insertions(+), 34 deletions(-) create mode 100644 LiveContainerSwiftUI/Assets.xcassets/JITBadgeColor.colorset/Contents.json delete mode 100644 LiveContainerSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json diff --git a/LCSharedUtils.h b/LCSharedUtils.h index df5e552..322ef6d 100644 --- a/LCSharedUtils.h +++ b/LCSharedUtils.h @@ -3,6 +3,7 @@ @interface LCSharedUtils : NSObject + (NSString *)appGroupID; + (NSString *)certificatePassword; ++ (BOOL)askForJIT; + (BOOL)launchToGuestApp; + (BOOL)launchToGuestAppWithURL:(NSURL *)url; + (void)setWebPageUrlForNextLaunch:(NSString*)urlString; diff --git a/LCSharedUtils.m b/LCSharedUtils.m index 015cd49..d396aa1 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -57,6 +57,22 @@ + (BOOL)launchToGuestApp { return NO; } ++ (BOOL)askForJIT { + NSString *urlScheme; + NSString *tsPath = [NSString stringWithFormat:@"%@/../_TrollStore", NSBundle.mainBundle.bundlePath]; + if (!access(tsPath.UTF8String, F_OK)) { + urlScheme = @"apple-magnifier://enable-jit?bundle-id=%@"; + } else { + urlScheme = @"sidestore://sidejit-enable?bid=%@"; + } + NSURL *launchURL = [NSURL URLWithString:[NSString stringWithFormat:urlScheme, NSBundle.mainBundle.bundleIdentifier]]; + if ([UIApplication.sharedApplication canOpenURL:launchURL]) { + [UIApplication.sharedApplication openURL:launchURL options:@{} completionHandler:nil]; + return YES; + } + return NO; +} + + (BOOL)launchToGuestAppWithURL:(NSURL *)url { NSURLComponents* components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; if(![components.host isEqualToString:@"livecontainer-launch"]) return NO; @@ -176,7 +192,7 @@ + (void)movePreferencesFromPath:(NSString*) plistLocationFrom toPath:(NSString*) [fm moveItemAtPath:fromPlistPath toPath:toPlistPath error:&error1]; if(error1) { - NSLog(@"[NMSL] error1 = %@", error1.description); + NSLog(@"[LC] error1 = %@", error1.description); } } diff --git a/LiveContainerSwiftUI/Assets.xcassets/JITBadgeColor.colorset/Contents.json b/LiveContainerSwiftUI/Assets.xcassets/JITBadgeColor.colorset/Contents.json new file mode 100644 index 0000000..cae52b1 --- /dev/null +++ b/LiveContainerSwiftUI/Assets.xcassets/JITBadgeColor.colorset/Contents.json @@ -0,0 +1,20 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "190", + "green" : "82", + "red" : "130" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 2d6a1a7..2043a9c 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -18,6 +18,7 @@ struct LCAppBanner : View { var delegate: LCAppBannerDelegate @State var uiIsShared : Bool + @State var uiIsJITNeeded : Bool @Binding var appDataFolders: [String] @Binding var tweakFolders: [String] @@ -47,6 +48,10 @@ struct LCAppBanner : View { @State private var confirmMoveToPrivateDoc = false @State private var confirmMoveToPrivateDocContinuation : CheckedContinuation? = nil + @State private var enablingJITShow = false + @State private var confirmEnablingJIT = false + @State private var confirmEnablingJITContinuation : CheckedContinuation? = nil + @State private var errorShow = false @State private var errorInfo = "" @@ -55,6 +60,7 @@ struct LCAppBanner : View { @State private var isAppRunning = false @State private var observer : NSKeyValueObservation? + @EnvironmentObject private var bundleIDToLaunchModel : BundleIDToLaunchModel init(appInfo: LCAppInfo, delegate: LCAppBannerDelegate, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>) { _appInfo = State(initialValue: appInfo) @@ -67,7 +73,7 @@ struct LCAppBanner : View { _uiPickerTweakFolder = _uiTweakFolder _uiIsShared = State(initialValue: appInfo.isShared) - + _uiIsJITNeeded = State(initialValue: appInfo.isJITNeeded()) } var body: some View { @@ -89,6 +95,13 @@ struct LCAppBanner : View { Capsule().fill(Color("BadgeColor")) ) } + if uiIsJITNeeded { + Text("JIT").font(.system(size: 8)).bold().padding(2) + .frame(width: 30, height:16) + .background( + Capsule().fill(Color("JITBadgeColor")) + ) + } } Text("\(appInfo.version()) - \(appInfo.bundleIdentifier())").font(.system(size: 12)).foregroundColor(Color("FontColor")) @@ -147,6 +160,23 @@ struct LCAppBanner : View { } label: { Label("Convert to Shared App", systemImage: "arrowshape.turn.up.left") } + Button { + Task { await toggleJITNeeded()} + } label: { + if uiIsJITNeeded { + Label("Don't Need JIT", systemImage: "bolt.slash") + } else { + Label("Mark as JIT Needed", systemImage: "bolt") + } + + } + + Button { + copyLaunchUrl() + } label: { + Label("Copy Launch Url", systemImage: "link") + } + Menu(content: { Button { Task{ await createFolder() } @@ -201,6 +231,13 @@ struct LCAppBanner : View { setTweakFolder(folderName: newValue) } }) + .onChange(of: bundleIDToLaunchModel.bundleIdToLaunch, perform: { newValue in + Task { await handleURLSchemeLaunch() } + }) + + .onAppear() { + Task { await handleURLSchemeLaunch() } + } .alert("Confirm Uninstallation", isPresented: $confirmAppRemovalShow) { Button(role: .destructive) { @@ -272,16 +309,36 @@ struct LCAppBanner : View { renameFolerContinuation?.resume() } ) + .alert("Enabling JIT", isPresented: $enablingJITShow) { + Button { + self.confirmEnablingJIT = true + self.confirmEnablingJITContinuation?.resume() + } label: { + Text("Launch Now") + } + Button("Cancel", role: .cancel) { + self.confirmEnablingJIT = false + self.confirmEnablingJITContinuation?.resume() + } + } message: { + Text("Please use your favourite way to enable jit for current LiveContainer.") + } + .alert("Error", isPresented: $errorShow) { Button("OK", action: { }) } message: { Text(errorInfo) } - } + func handleURLSchemeLaunch() async { + if self.appInfo.relativeBundlePath == bundleIDToLaunchModel.bundleIdToLaunch { + await runApp() + } + } + func runApp() async { if let runningLC = LCUtils.getAppRunningLCScheme(bundleId: self.appInfo.relativeBundlePath) { let openURL = URL(string: "\(runningLC)://livecontainer-launch?bundle-name=\(self.appInfo.relativeBundlePath!)")! @@ -324,7 +381,12 @@ struct LCAppBanner : View { return } else { UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") - LCUtils.launchToGuestApp() + if appInfo.isJITNeeded() { + await self.jitLaunch() + } else { + LCUtils.launchToGuestApp() + } + } self.isAppRunning = false @@ -524,7 +586,34 @@ struct LCAppBanner : View { } } - + + func toggleJITNeeded() async { + if appInfo.isJITNeeded() { + appInfo.setIsJITNeeded(false) + uiIsJITNeeded = false + } else { + appInfo.setIsJITNeeded(true) + uiIsJITNeeded = true + } + } + + func jitLaunch() async { + LCUtils.askForJIT() + await withCheckedContinuation { c in + self.confirmEnablingJITContinuation = c + enablingJITShow = true + } + if confirmEnablingJIT { + LCUtils.launchToGuestApp() + } else { + UserDefaults.standard.removeObject(forKey: "selected") + } + } + + func copyLaunchUrl() { + UIPasteboard.general.string = "livecontainer://livecontainer-launch?bundle-name=\(appInfo.relativeBundlePath!)" + } + } diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index b67172d..f0c7cd7 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -37,7 +37,7 @@ struct LCAppListView : View, LCAppBannerDelegate { @State var installOptionContinuation : CheckedContinuation? = nil @State var webViewOpened = false - @State var webViewURL : URL = URL(string: "https://www.google.com")! + @State var webViewURL : URL = URL(string: "about:blank")! @State private var webViewUrlInputOpened = false @State private var webViewUrlInputContent = "" @State private var webViewUrlInputContinuation : CheckedContinuation? = nil diff --git a/LiveContainerSwiftUI/LCSwiftBridge.h b/LiveContainerSwiftUI/LCSwiftBridge.h index 8846c9a..cc1a567 100644 --- a/LiveContainerSwiftUI/LCSwiftBridge.h +++ b/LiveContainerSwiftUI/LCSwiftBridge.h @@ -14,4 +14,5 @@ @interface LCSwiftBridge : NSObject + (UIViewController * _Nonnull)getRootVC; + (void)openWebPageWithUrlStr:(NSURL* _Nonnull)url; ++ (void)launchAppWithBundleId:(NSString* _Nonnull)bundleId; @end diff --git a/LiveContainerSwiftUI/LCSwiftBridge.m b/LiveContainerSwiftUI/LCSwiftBridge.m index a1499e0..68bc858 100644 --- a/LiveContainerSwiftUI/LCSwiftBridge.m +++ b/LiveContainerSwiftUI/LCSwiftBridge.m @@ -19,4 +19,8 @@ + (void)openWebPageWithUrlStr:(NSString* _Nonnull)urlStr { [LCObjcBridge openWebPageWithUrlStr:urlStr]; } ++ (void)launchAppWithBundleId:(NSString* _Nonnull)bundleId { + [LCObjcBridge launchAppWithBundleId:bundleId]; +} + @end diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index bae0d10..928a70d 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -100,6 +100,7 @@ struct LCTabView: View { }.onAppear() { checkLastLaunchError() } + .environmentObject(DataManager.shared.bundleIDToLaunchModel) } func checkLastLaunchError() { diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index 205850f..7740490 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -1,6 +1,5 @@ // // SwiftUIView.swift -// nmsl // // Created by s s on 2024/8/23. // @@ -131,7 +130,7 @@ struct LCWebView: View { let encodedUrl = Data(url.absoluteString.utf8).base64EncodedString() if let urlToOpen = URL(string: "\(runningLC)://livecontainer-launch?bundle-name=\(bundleId)&open-url=\(encodedUrl)"), UIApplication.shared.canOpenURL(urlToOpen) { - NSLog("[NMSL] urlToOpen = \(urlToOpen.absoluteString)") + NSLog("[LC] urlToOpen = \(urlToOpen.absoluteString)") UIApplication.shared.open(urlToOpen) isPresent = false return diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj index 76bcb2a..7310fc3 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -9,7 +9,6 @@ /* Begin PBXBuildFile section */ 173564C92C76FE3500C6C918 /* LCAppListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564BC2C76FE3500C6C918 /* LCAppListView.swift */; }; 173564CA2C76FE3500C6C918 /* LCTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564BD2C76FE3500C6C918 /* LCTabView.swift */; }; - 173564CB2C76FE3500C6C918 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173564BE2C76FE3500C6C918 /* Preview Assets.xcassets */; }; 173564CC2C76FE3500C6C918 /* LCTweaksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C02C76FE3500C6C918 /* LCTweaksView.swift */; }; 173564CD2C76FE3500C6C918 /* LCSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C12C76FE3500C6C918 /* LCSettingsView.swift */; }; 173564CE2C76FE3500C6C918 /* Makefile in Sources */ = {isa = PBXBuildFile; fileRef = 173564C32C76FE3500C6C918 /* Makefile */; }; @@ -25,7 +24,6 @@ /* Begin PBXFileReference section */ 173564BC2C76FE3500C6C918 /* LCAppListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCAppListView.swift; sourceTree = ""; }; 173564BD2C76FE3500C6C918 /* LCTabView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCTabView.swift; sourceTree = ""; }; - 173564BE2C76FE3500C6C918 /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; 173564C02C76FE3500C6C918 /* LCTweaksView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCTweaksView.swift; sourceTree = ""; }; 173564C12C76FE3500C6C918 /* LCSettingsView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCSettingsView.swift; sourceTree = ""; }; 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LCSwiftBridge.h; sourceTree = ""; }; @@ -66,7 +64,6 @@ 173564C32C76FE3500C6C918 /* Makefile */, 173564C62C76FE3500C6C918 /* ObjcBridge.swift */, 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */, - 173564BF2C76FE3500C6C918 /* Preview Content */, 178B4C3D2C77654400DD1F74 /* Shared.swift */, 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */, 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */, @@ -74,14 +71,6 @@ name = LiveContainerSwiftUI; sourceTree = ""; }; - 173564BF2C76FE3500C6C918 /* Preview Content */ = { - isa = PBXGroup; - children = ( - 173564BE2C76FE3500C6C918 /* Preview Assets.xcassets */, - ); - path = "Preview Content"; - sourceTree = ""; - }; 17B9B8842C760678009D079E = { isa = PBXGroup; children = ( @@ -195,7 +184,6 @@ buildActionMask = 2147483647; files = ( 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */, - 173564CB2C76FE3500C6C918 /* Preview Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/LiveContainerSwiftUI/ObjcBridge.swift b/LiveContainerSwiftUI/ObjcBridge.swift index 53e4def..497c6c7 100644 --- a/LiveContainerSwiftUI/ObjcBridge.swift +++ b/LiveContainerSwiftUI/ObjcBridge.swift @@ -21,6 +21,10 @@ import SwiftUI } } + @objc public static func launchApp(bundleId: String) { + DataManager.shared.bundleIDToLaunchModel.bundleIdToLaunch = bundleId + } + @objc public static func getRootVC() -> UIViewController { let rootView = LCTabView() let rootVC = UIHostingController(rootView: rootView) diff --git a/LiveContainerSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json b/LiveContainerSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/LiveContainerSwiftUI/Preview Content/Preview Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index aaba0c1..f62a011 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -47,6 +47,16 @@ struct LCPath { } } +class BundleIDToLaunchModel: ObservableObject { + @Published var bundleIdToLaunch: String = "" +} + +class DataManager { + static let shared = DataManager() + let bundleIDToLaunchModel = BundleIDToLaunchModel() +} + + extension String: LocalizedError { public var errorDescription: String? { return self } } diff --git a/LiveContainerUI/LCAppDelegateSwiftUI.h b/LiveContainerUI/LCAppDelegateSwiftUI.h index 2637696..2b304af 100644 --- a/LiveContainerUI/LCAppDelegateSwiftUI.h +++ b/LiveContainerUI/LCAppDelegateSwiftUI.h @@ -3,6 +3,7 @@ @interface LCSwiftBridge : NSObject + (UIViewController * _Nonnull)getRootVC; + (void)openWebPageWithUrlStr:(NSString* _Nonnull)urlStr; ++ (void)launchAppWithBundleId:(NSString* _Nonnull)bundleId; @end @interface LCAppDelegateSwiftUI : UIResponder diff --git a/LiveContainerUI/LCAppDelegateSwiftUI.m b/LiveContainerUI/LCAppDelegateSwiftUI.m index f3da20c..a9a6e91 100644 --- a/LiveContainerUI/LCAppDelegateSwiftUI.m +++ b/LiveContainerUI/LCAppDelegateSwiftUI.m @@ -28,18 +28,13 @@ - (BOOL)application:(UIApplication *)application openURL:(NSURL *)url options:(N NSURLComponents* components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; for (NSURLQueryItem* queryItem in components.queryItems) { if ([queryItem.name isEqualToString:@"bundle-name"]) { - NSString* runningLC = [NSClassFromString(@"LCSharedUtils") getAppRunningLCSchemeWithBundleId:queryItem.value]; - if(runningLC) { - NSString* urlStr = [NSString stringWithFormat:@"%@://livecontainer-launch?bundle-name=%@", runningLC, queryItem.value]; - [UIApplication.sharedApplication openURL:[NSURL URLWithString:urlStr] options:@{} completionHandler:nil]; - return YES; - } + [NSClassFromString(@"LCSwiftBridge") launchAppWithBundleId:queryItem.value]; break; } } } - return [LCUtils launchToGuestAppWithURL:url]; + return NO; } @end diff --git a/LiveContainerUI/LCAppInfo.h b/LiveContainerUI/LCAppInfo.h index be284d7..bec80af 100644 --- a/LiveContainerUI/LCAppInfo.h +++ b/LiveContainerUI/LCAppInfo.h @@ -14,6 +14,8 @@ } @property NSString* relativeBundlePath; @property bool isShared; +- (bool)isJITNeeded; +- (void)setIsJITNeeded:(bool)isJITNeeded; - (void)setBundlePath:(NSString*)newBundlePath; - (NSMutableDictionary*)info; - (UIImage*)icon; diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index 2b68e3b..dee58d8 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -141,7 +141,7 @@ - (NSDictionary *)generateWebClipConfig { @"PayloadDisplayName": self.displayName, @"PayloadIdentifier": self.bundleIdentifier, @"PayloadType": @"com.apple.webClip.managed", - @"PayloadUUID": self.dataUUID, + @"PayloadUUID": NSUUID.UUID.UUIDString, @"PayloadVersion": @(1), @"Precomposed": @NO, @"toPayloadOrganization": @"LiveContainer", @@ -263,4 +263,17 @@ - (void) signCleanUpWithSuccessStatus:(BOOL)isSignSuccess { self._signStatus = nil; return; } + +- (bool)isJITNeeded { + if(_info[@"isJITNeeded"] != nil) { + return [_info[@"isJITNeeded"] boolValue]; + } else { + return NO; + } +} +- (void)setIsJITNeeded:(bool)isJITNeeded { + _info[@"isJITNeeded"] = [NSNumber numberWithBool:isJITNeeded]; + [self save]; + +} @end diff --git a/LiveContainerUI/LCUtils.h b/LiveContainerUI/LCUtils.h index c6dd17b..eccf807 100644 --- a/LiveContainerUI/LCUtils.h +++ b/LiveContainerUI/LCUtils.h @@ -23,6 +23,7 @@ void LCPatchExecSlice(const char *path, struct mach_header_64 *header); + (BOOL)deleteKeychainItem:(NSString *)key ofStore:(NSString *)store; + (NSData *)keychainItem:(NSString *)key ofStore:(NSString *)store; ++ (BOOL)askForJIT; + (BOOL)launchToGuestApp; + (BOOL)launchToGuestAppWithURL:(NSURL *)url; diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index ea4a413..7d87340 100644 --- a/LiveContainerUI/LCUtils.m +++ b/LiveContainerUI/LCUtils.m @@ -94,6 +94,10 @@ + (BOOL)launchToGuestApp { return [NSClassFromString(@"LCSharedUtils") launchToGuestApp]; } ++ (BOOL)askForJIT { + return [NSClassFromString(@"LCSharedUtils") askForJIT]; +} + + (BOOL)launchToGuestAppWithURL:(NSURL *)url { return [NSClassFromString(@"LCSharedUtils") launchToGuestAppWithURL:url]; } From 33c98d4d7afab5b191f6bc866e8758b4a147deef Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 10 Sep 2024 20:21:10 +0800 Subject: [PATCH 23/36] support SideJITServer --- LCSharedUtils.m | 29 +++++++++--- LiveContainerSwiftUI/LCAppBanner.swift | 2 +- LiveContainerSwiftUI/LCSettingsView.swift | 55 ++++++++++++++++++++--- LiveContainerSwiftUI/Shared.swift | 2 + LiveContainerUI/LCMachOUtils.m | 14 ++++++ LiveContainerUI/LCUtils.h | 1 + LiveContainerUI/LCUtils.m | 24 ++++++++++ 7 files changed, 114 insertions(+), 13 deletions(-) diff --git a/LCSharedUtils.m b/LCSharedUtils.m index d396aa1..3bf42c0 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -62,13 +62,30 @@ + (BOOL)askForJIT { NSString *tsPath = [NSString stringWithFormat:@"%@/../_TrollStore", NSBundle.mainBundle.bundlePath]; if (!access(tsPath.UTF8String, F_OK)) { urlScheme = @"apple-magnifier://enable-jit?bundle-id=%@"; + NSURL *launchURL = [NSURL URLWithString:[NSString stringWithFormat:urlScheme, NSBundle.mainBundle.bundleIdentifier]]; + if ([UIApplication.sharedApplication canOpenURL:launchURL]) { + [UIApplication.sharedApplication openURL:launchURL options:@{} completionHandler:nil]; + return YES; + } } else { - urlScheme = @"sidestore://sidejit-enable?bid=%@"; - } - NSURL *launchURL = [NSURL URLWithString:[NSString stringWithFormat:urlScheme, NSBundle.mainBundle.bundleIdentifier]]; - if ([UIApplication.sharedApplication canOpenURL:launchURL]) { - [UIApplication.sharedApplication openURL:launchURL options:@{} completionHandler:nil]; - return YES; + NSUserDefaults* groupUserDefaults = [[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]]; + + NSString* sideJITServerAddress = [groupUserDefaults objectForKey:@"LCSideJITServerAddress"]; + NSString* deviceUDID = [groupUserDefaults objectForKey:@"LCDeviceUDID"]; + if (!sideJITServerAddress || !deviceUDID) { + return NO; + } + NSString* launchJITUrlStr = [NSString stringWithFormat: @"%@/%@/%@", sideJITServerAddress, deviceUDID, NSBundle.mainBundle.bundleIdentifier]; + NSURLSession* session = [NSURLSession sharedSession]; + NSURL* launchJITUrl = [NSURL URLWithString:launchJITUrlStr]; + NSURLRequest* req = [[NSURLRequest alloc] initWithURL:launchJITUrl]; + NSURLSessionDataTask *task = [session dataTaskWithRequest:req completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { + if(error) { + NSLog(@"[LC] failed to contact SideJITServer: %@", error); + } + }]; + [task resume]; + } return NO; } diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 2043a9c..ea0370b 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -309,7 +309,7 @@ struct LCAppBanner : View { renameFolerContinuation?.resume() } ) - .alert("Enabling JIT", isPresented: $enablingJITShow) { + .alert("Waiting for JIT", isPresented: $enablingJITShow) { Button { self.confirmEnablingJIT = true self.confirmEnablingJITContinuation?.resume() diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index 8b4f531..fc5ea62 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -33,6 +33,9 @@ struct LCSettingsView: View { @State var silentSwitchApp = false @State var injectToLCItelf = false + @State var sideJITServerAddress : String + @State var deviceUDID: String + init(apps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>) { _isJitLessEnabled = State(initialValue: LCUtils.certificatePassword() != nil) _isAltCertIgnored = State(initialValue: UserDefaults.standard.bool(forKey: "LCIgnoreALTCertificate")) @@ -42,6 +45,18 @@ struct LCSettingsView: View { _apps = apps _appDataFolderNames = appDataFolderNames + + if let configSideJITServerAddress = LCUtils.appGroupUserDefault.string(forKey: "LCSideJITServerAddress") { + _sideJITServerAddress = State(initialValue: configSideJITServerAddress) + } else { + _sideJITServerAddress = State(initialValue: "") + } + + if let configDeviceUDID = LCUtils.appGroupUserDefault.string(forKey: "LCDeviceUDID") { + _deviceUDID = State(initialValue: configDeviceUDID) + } else { + _deviceUDID = State(initialValue: "") + } } @@ -95,14 +110,32 @@ struct LCSettingsView: View { Text("If you see frequent re-sign, enable this option.") } - Section{ - Toggle(isOn: $frameShortIcon) { - Text("Frame Short Icon") +// Section{ +// Toggle(isOn: $frameShortIcon) { +// Text("Frame Short Icon") +// } +// } header: { +// Text("Miscellaneous") +// } footer: { +// Text("Frame shortcut icons with LiveContainer icon.") +// } + Section { + HStack { + Text("Address") + Spacer() + TextField("http://x.x.x.x:8080", text: $sideJITServerAddress) + .multilineTextAlignment(.trailing) + } + HStack { + Text("UDID") + Spacer() + TextField("", text: $deviceUDID) + .multilineTextAlignment(.trailing) } } header: { - Text("Miscellaneous") + Text("JIT") } footer: { - Text("Frame shortcut icons with LiveContainer icon.") + Text("Set up your SideJITServer/JITStreamer server. Local Network permission is required.") } Section { @@ -203,15 +236,25 @@ struct LCSettingsView: View { .onChange(of: injectToLCItelf) { newValue in saveItem(key: "LCLoadTweaksToSelf", val: newValue) } + .onChange(of: deviceUDID) { newValue in + saveAppGroupItem(key: "LCDeviceUDID", val: newValue) + } + .onChange(of: sideJITServerAddress) { newValue in + saveAppGroupItem(key: "LCSideJITServerAddress", val: newValue) + } } .navigationViewStyle(StackNavigationViewStyle()) } - func saveItem(key: String, val: Bool) { + func saveItem(key: String, val: Any) { UserDefaults.standard.setValue(val, forKey: key) } + func saveAppGroupItem(key: String, val: Any) { + LCUtils.appGroupUserDefault.setValue(val, forKey: key) + } + func setupJitLess() { if !LCUtils.isAppGroupAltStoreLike() { errorInfo = "Unsupported installation method. Please use AltStore or SideStore to setup this feature." diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index f62a011..7738056 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -171,6 +171,8 @@ struct SiteAssociation : Codable { } extension LCUtils { + public static let appGroupUserDefault = UserDefaults.init(suiteName: LCUtils.appGroupID())! + // 0= not installed, 1= is installed, 2=current liveContainer is the second one public static let multiLCStatus = { if LCUtils.appUrlScheme()?.lowercased() != "livecontainer" { diff --git a/LiveContainerUI/LCMachOUtils.m b/LiveContainerUI/LCMachOUtils.m index f1fb327..56d62eb 100644 --- a/LiveContainerUI/LCMachOUtils.m +++ b/LiveContainerUI/LCMachOUtils.m @@ -114,3 +114,17 @@ void LCPatchExecSlice(const char *path, struct mach_header_64 *header) { close(fd); return nil; } + +void LCChangeExecUUID(struct mach_header_64 *header) { + uint8_t *imageHeaderPtr = (uint8_t*)header + sizeof(struct mach_header_64); + struct load_command *command = (struct load_command *)imageHeaderPtr; + for(int i = 0; i < header->ncmds > 0; i++) { + if(command->cmd == LC_UUID) { + struct uuid_command *uuidCmd = (struct uuid_command *)command; + // let's add the first byte by 1 + uuidCmd->uuid[0] += 1; + break; + } + command = (struct load_command *)((void *)command + command->cmdsize); + } +} diff --git a/LiveContainerUI/LCUtils.h b/LiveContainerUI/LCUtils.h index eccf807..cc64eb4 100644 --- a/LiveContainerUI/LCUtils.h +++ b/LiveContainerUI/LCUtils.h @@ -5,6 +5,7 @@ typedef void (^LCParseMachOCallback)(const char *path, struct mach_header_64 *he NSString *LCParseMachO(const char *path, LCParseMachOCallback callback); void LCPatchAddRPath(const char *path, struct mach_header_64 *header); void LCPatchExecSlice(const char *path, struct mach_header_64 *header); +void LCChangeExecUUID(struct mach_header_64 *header); @interface PKZipArchiver : NSObject diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index 7d87340..9190cbd 100644 --- a/LiveContainerUI/LCUtils.m +++ b/LiveContainerUI/LCUtils.m @@ -350,7 +350,31 @@ + (NSURL *)archiveIPAWithBundleName:(NSString*)newBundleName error:(NSError **)e infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon60x60_2"; infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][1] = @"AppIcon76x76_2"; infoDict[@"CFBundleIcons"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon60x60_2"; + // reset a executable name so they don't look the same on the log + NSURL* appBundlePath = [tmpPayloadPath URLByAppendingPathComponent:@"App.app"]; + NSURL* execFromPath = [appBundlePath URLByAppendingPathComponent:infoDict[@"CFBundleExecutable"]]; + infoDict[@"CFBundleExecutable"] = @"LiveContainer_PleaseDoNotShortenTheExecutableNameBecauseItIsUsedToReserveSpaceForOverwritingThankYou2"; + NSURL* execToPath = [appBundlePath URLByAppendingPathComponent:infoDict[@"CFBundleExecutable"]]; + + [manager moveItemAtURL:execFromPath toURL:execToPath error:error]; + if (*error) { + NSLog(@"[LC] %@", *error); + return nil; + } + + // We have to change executable's UUID so iOS won't consider 2 executables the same + NSString* errorChangeUUID = LCParseMachO([execToPath.path UTF8String], ^(const char *path, struct mach_header_64 *header) { + LCChangeExecUUID(header); + }); + if (errorChangeUUID) { + NSMutableDictionary* details = [NSMutableDictionary dictionary]; + [details setValue:errorChangeUUID forKey:NSLocalizedDescriptionKey]; + // populate the error object with the details + *error = [NSError errorWithDomain:@"world" code:200 userInfo:details]; + NSLog(@"[LC] %@", errorChangeUUID); + return nil; + } [infoDict writeToURL:infoPath error:error]; From 5e9d119fa9727ba1d96e24c749121903bee5ef06 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 12 Sep 2024 00:33:56 +0800 Subject: [PATCH 24/36] users can hide apps --- LiveContainerSwiftUI/LCAppBanner.swift | 65 +++++-- LiveContainerSwiftUI/LCAppListView.swift | 221 +++++++++++++++++----- LiveContainerSwiftUI/LCSettingsView.swift | 40 +++- LiveContainerSwiftUI/LCTabView.swift | 21 +- LiveContainerSwiftUI/LCWebView.swift | 58 +++++- LiveContainerSwiftUI/ObjcBridge.swift | 6 +- LiveContainerSwiftUI/Shared.swift | 65 ++++++- LiveContainerUI/LCAppInfo.h | 2 + LiveContainerUI/LCAppInfo.m | 13 ++ main.m | 5 + 10 files changed, 413 insertions(+), 83 deletions(-) diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index ea0370b..470ad64 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -11,6 +11,7 @@ import UniformTypeIdentifiers protocol LCAppBannerDelegate { func removeApp(app: LCAppInfo) + func changeAppVisibility(app: LCAppInfo) } struct LCAppBanner : View { @@ -19,6 +20,7 @@ struct LCAppBanner : View { @State var uiIsShared : Bool @State var uiIsJITNeeded : Bool + @State private var uiIsHidden : Bool @Binding var appDataFolders: [String] @Binding var tweakFolders: [String] @@ -60,7 +62,7 @@ struct LCAppBanner : View { @State private var isAppRunning = false @State private var observer : NSKeyValueObservation? - @EnvironmentObject private var bundleIDToLaunchModel : BundleIDToLaunchModel + @EnvironmentObject private var sharedModel : SharedModel init(appInfo: LCAppInfo, delegate: LCAppBannerDelegate, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>) { _appInfo = State(initialValue: appInfo) @@ -74,6 +76,7 @@ struct LCAppBanner : View { _uiIsShared = State(initialValue: appInfo.isShared) _uiIsJITNeeded = State(initialValue: appInfo.isJITNeeded()) + _uiIsHidden = State(initialValue: appInfo.isHidden()) } var body: some View { @@ -149,6 +152,35 @@ struct LCAppBanner : View { .contextMenu{ Text(appInfo.relativeBundlePath) + Button { + Task { await toggleJITNeeded()} + } label: { + if uiIsJITNeeded { + Label("Don't Need JIT", systemImage: "bolt.slash") + } else { + Label("Mark as JIT Needed", systemImage: "bolt") + } + + } + + Button { + copyLaunchUrl() + } label: { + Label("Copy Launch Url", systemImage: "link") + } + + if sharedModel.isHiddenAppUnlocked { + Button { + Task { await toggleHidden()} + } label: { + if uiIsHidden { + Label("Unhide App", systemImage: "eye") + } else { + Label("Hide App", systemImage: "eye.slash") + } + + } + } if !uiIsShared { Button(role: .destructive) { Task{ await uninstall() } @@ -160,22 +192,6 @@ struct LCAppBanner : View { } label: { Label("Convert to Shared App", systemImage: "arrowshape.turn.up.left") } - Button { - Task { await toggleJITNeeded()} - } label: { - if uiIsJITNeeded { - Label("Don't Need JIT", systemImage: "bolt.slash") - } else { - Label("Mark as JIT Needed", systemImage: "bolt") - } - - } - - Button { - copyLaunchUrl() - } label: { - Label("Copy Launch Url", systemImage: "link") - } Menu(content: { Button { @@ -231,7 +247,7 @@ struct LCAppBanner : View { setTweakFolder(folderName: newValue) } }) - .onChange(of: bundleIDToLaunchModel.bundleIdToLaunch, perform: { newValue in + .onChange(of: sharedModel.bundleIdToLaunch, perform: { newValue in Task { await handleURLSchemeLaunch() } }) @@ -334,7 +350,7 @@ struct LCAppBanner : View { } func handleURLSchemeLaunch() async { - if self.appInfo.relativeBundlePath == bundleIDToLaunchModel.bundleIdToLaunch { + if self.appInfo.relativeBundlePath == sharedModel.bundleIdToLaunch { await runApp() } } @@ -615,5 +631,16 @@ struct LCAppBanner : View { UIPasteboard.general.string = "livecontainer://livecontainer-launch?bundle-name=\(appInfo.relativeBundlePath!)" } + func toggleHidden() async { + if appInfo.isHidden() { + appInfo.setIsHidden(false) + uiIsHidden = false + } else { + appInfo.setIsHidden(true) + uiIsHidden = true + } + delegate.changeAppVisibility(app: appInfo) + } + } diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index f0c7cd7..3f13895 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -16,6 +16,7 @@ struct AppReplaceOption : Hashable { struct LCAppListView : View, LCAppBannerDelegate { @Binding var apps: [LCAppInfo] + @Binding var hiddenApps: [LCAppInfo] @Binding var appDataFolderNames: [String] @Binding var tweakFolderNames: [String] @@ -41,11 +42,14 @@ struct LCAppListView : View, LCAppBannerDelegate { @State private var webViewUrlInputOpened = false @State private var webViewUrlInputContent = "" @State private var webViewUrlInputContinuation : CheckedContinuation? = nil + + @EnvironmentObject private var sharedModel : SharedModel - init(apps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>, tweakFolderNames: Binding<[String]>) { + init(apps: Binding<[LCAppInfo]>, hiddenApps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>, tweakFolderNames: Binding<[String]>) { _installOptions = State(initialValue: []) _installOptionChosen = State(initialValue: nil) _apps = apps + _hiddenApps = hiddenApps _appDataFolderNames = appDataFolderNames _tweakFolderNames = tweakFolderNames } @@ -53,47 +57,80 @@ struct LCAppListView : View, LCAppBannerDelegate { var body: some View { NavigationView { ScrollView { - LazyVStack(pinnedViews:[.sectionHeaders]) { - Section { - LazyVStack { - ForEach(apps, id: \.self) { app in - LCAppBanner(appInfo: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) - } - .transition(.scale) - - if LCUtils.multiLCStatus == 2 { - Text("Manage apps in the primary LiveContainer").foregroundStyle(.gray).padding() + GeometryReader { g in + ProgressView(value: uiInstallProgressPercentage) + .labelsHidden() + .opacity(installprogressVisible ? 1 : 0) + .scaleEffect(y: 0.5) + .onChange(of: installProgressPercentage) { newValue in + if newValue > uiInstallProgressPercentage { + withAnimation(.easeIn(duration: 0.3)) { + uiInstallProgressPercentage = newValue + } + } else { + uiInstallProgressPercentage = newValue } } - .padding() - } header: { - GeometryReader{ g in - ProgressView(value: uiInstallProgressPercentage) - .labelsHidden() - .opacity(installprogressVisible ? 1 : 0) - .scaleEffect(y: 0.5) - .onChange(of: installProgressPercentage) { newValue in - if newValue > uiInstallProgressPercentage { - withAnimation(.easeIn(duration: 0.3)) { - uiInstallProgressPercentage = newValue - } - } else { - uiInstallProgressPercentage = newValue - } - } - } + .offset(CGSize(width: 0, height: max(0,-g.frame(in: .named("scroll")).minY) - 1)) + } + .zIndex(.infinity) + LazyVStack { + ForEach(apps, id: \.self) { app in + LCAppBanner(appInfo: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) } - + .transition(.scale) + } + .padding() .animation(.easeInOut, value: apps) + + if !sharedModel.isHiddenAppUnlocked { + Text(apps.count > 0 ? "\(apps.count) Apps in Total" : "Press the Plus Button to Install Apps.").foregroundStyle(.gray) + .onTapGesture(count: 3) { + Task { await authenticateUser() } + } + } + + + if sharedModel.isHiddenAppUnlocked { + LazyVStack { + HStack { + Text("Hidden Apps") + .font(.system(.title2).bold()) + .border(Color.black) + Spacer() + } + ForEach(hiddenApps, id: \.self) { app in + LCAppBanner(appInfo: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) + } + .transition(.scale) + } + .padding() + .animation(.easeInOut, value: apps) + + if hiddenApps.count == 0 { + Text("Long Press on a App to Make it Hidden.") + .foregroundStyle(.gray) + } + Text(apps.count + hiddenApps.count > 0 ? "\(apps.count + hiddenApps.count) Apps in Total" : "Press the Plus Button to Install Apps.").foregroundStyle(.gray) + } + + if LCUtils.multiLCStatus == 2 { + Text("Manage apps in the primary LiveContainer").foregroundStyle(.gray).padding() + } } + .coordinateSpace(name: "scroll") .onAppear { if !didAppear { didAppear = true - checkIfAppDelegateNeedOpenWebPage() + Task { await checkIfAppDelegateNeedOpenWebPage() } + onLaunchBundleIdChange() } } + .onChange(of: sharedModel.bundleIdToLaunch) { newValue in + onLaunchBundleIdChange() + } .navigationTitle("My Apps") .toolbar { @@ -167,7 +204,7 @@ struct LCAppListView : View, LCAppBannerDelegate { } ) .fullScreenCover(isPresented: $webViewOpened) { - LCWebView(url: $webViewURL, apps: $apps, isPresent: $webViewOpened) + LCWebView(url: $webViewURL, apps: $apps, hiddenApps: $hiddenApps, isPresent: $webViewOpened) } } @@ -180,23 +217,23 @@ struct LCAppListView : View, LCAppBannerDelegate { if webViewUrlInputContent == "" { return } - openWebView(urlString: webViewUrlInputContent) + await openWebView(urlString: webViewUrlInputContent) webViewUrlInputContent = "" } - func checkIfAppDelegateNeedOpenWebPage() { + func checkIfAppDelegateNeedOpenWebPage() async { LCObjcBridge.openUrlStrFunc = openWebView; if LCObjcBridge.urlStrToOpen != nil { - self.openWebView(urlString: LCObjcBridge.urlStrToOpen!) + await self.openWebView(urlString: LCObjcBridge.urlStrToOpen!) LCObjcBridge.urlStrToOpen = nil } else if let urlStr = UserDefaults.standard.string(forKey: "webPageToOpen") { UserDefaults.standard.removeObject(forKey: "webPageToOpen") - self.openWebView(urlString: urlStr) + await self.openWebView(urlString: urlStr) } } - func openWebView(urlString: String) { + func openWebView(urlString: String) async { guard var urlToOpen = URLComponents(string: urlString), urlToOpen.url != nil else { errorInfo = "The input url is invalid. Please check and try again" errorShow = true @@ -209,23 +246,43 @@ struct LCAppListView : View, LCAppBannerDelegate { } if urlToOpen.scheme != "https" && urlToOpen.scheme != "http" { var appToLaunch : LCAppInfo? = nil - appLoop: - for app in apps { - if let schemes = app.urlSchemes() { - for scheme in schemes { - if let scheme = scheme as? String, scheme == urlToOpen.scheme { - appToLaunch = app - break appLoop + var appListsToConsider = [apps] + if sharedModel.isHiddenAppUnlocked || !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") { + appListsToConsider.append(hiddenApps) + } + appLoop: + for appList in appListsToConsider { + for app in appList { + if let schemes = app.urlSchemes() { + for scheme in schemes { + if let scheme = scheme as? String, scheme == urlToOpen.scheme { + appToLaunch = app + break appLoop + } } } } } + + guard let appToLaunch = appToLaunch else { errorInfo = "Scheme \"\(urlToOpen.scheme!)\" cannot be opened by any app installed in LiveContainer." errorShow = true return } + if appToLaunch.isHidden() && !sharedModel.isHiddenAppUnlocked { + do { + if !(try await LCUtils.authenticateUser()) { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } + UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") UserDefaults.standard.setValue(urlToOpen.url!.absoluteString, forKey: "launchAppUrlScheme") LCUtils.launchToGuestApp() @@ -257,6 +314,9 @@ struct LCAppListView : View, LCAppBannerDelegate { } } + nonisolated func decompress(_ path: String, _ destination: String ,_ progress: Progress) async { + extract(path, destination, progress) + } func installIpaFile(_ url:URL) async throws { if(!url.startAccessingSecurityScopedResource()) { @@ -267,7 +327,9 @@ struct LCAppListView : View, LCAppBannerDelegate { let installProgress = Progress.discreteProgress(totalUnitCount: 100) self.installProgressPercentage = 0.0 self.installObserver = installProgress.observe(\.fractionCompleted) { p, v in - self.installProgressPercentage = p.fractionCompleted + DispatchQueue.main.async { + self.installProgressPercentage = p.fractionCompleted + } } let decompressProgress = Progress.discreteProgress(totalUnitCount: 100) installProgress.addChild(decompressProgress, withPendingUnitCount: 80) @@ -277,7 +339,7 @@ struct LCAppListView : View, LCAppBannerDelegate { } // decompress - extract(url.path, fm.temporaryDirectory.path, decompressProgress) + await decompress(url.path, fm.temporaryDirectory.path, decompressProgress) url.stopAccessingSecurityScopedResource() let payloadContents = try fm.contentsOfDirectory(atPath: payloadPath.path) @@ -385,4 +447,73 @@ struct LCAppListView : View, LCAppBannerDelegate { } } } + + func changeAppVisibility(app: LCAppInfo) { + DispatchQueue.main.async { + if app.isHidden() { + self.apps.removeAll { now in + return app == now + } + self.hiddenApps.append(app) + } else { + self.hiddenApps.removeAll { now in + return app == now + } + self.apps.append(app) + } + } + } + + + func onLaunchBundleIdChange() { + if sharedModel.bundleIdToLaunch == "" { + return + } + var appFound = false + var isFoundAppHidden = false + for app in apps { + if app.relativeBundlePath == sharedModel.bundleIdToLaunch { + appFound = true + break + } + } + if !appFound && !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") { + for app in hiddenApps { + if app.relativeBundlePath == sharedModel.bundleIdToLaunch { + appFound = true + isFoundAppHidden = true + break + } + } + } + + if isFoundAppHidden && !sharedModel.isHiddenAppUnlocked { + Task { + do { + let _ = try await LCUtils.authenticateUser() + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + + } + } + + if !appFound { + errorInfo = "App not Found" + errorShow = true + } + } + + func authenticateUser() async { + do { + if !(try await LCUtils.authenticateUser()) { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } } diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index fc5ea62..1445517 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -15,6 +15,7 @@ struct LCSettingsView: View { @State var successInfo = "" @Binding var apps: [LCAppInfo] + @Binding var hiddenApps: [LCAppInfo] @Binding var appDataFolderNames: [String] @State private var confirmAppFolderRemovalShow = false @@ -32,11 +33,14 @@ struct LCSettingsView: View { @State var frameShortIcon = false @State var silentSwitchApp = false @State var injectToLCItelf = false + @State var strictHiding = false @State var sideJITServerAddress : String @State var deviceUDID: String - init(apps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>) { + @EnvironmentObject private var sharedModel : SharedModel + + init(apps: Binding<[LCAppInfo]>, hiddenApps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>) { _isJitLessEnabled = State(initialValue: LCUtils.certificatePassword() != nil) _isAltCertIgnored = State(initialValue: UserDefaults.standard.bool(forKey: "LCIgnoreALTCertificate")) _frameShortIcon = State(initialValue: UserDefaults.standard.bool(forKey: "LCFrameShortcutIcons")) @@ -44,6 +48,7 @@ struct LCSettingsView: View { _injectToLCItelf = State(initialValue: UserDefaults.standard.bool(forKey: "LCLoadTweaksToSelf")) _apps = apps + _hiddenApps = hiddenApps _appDataFolderNames = appDataFolderNames if let configSideJITServerAddress = LCUtils.appGroupUserDefault.string(forKey: "LCSideJITServerAddress") { @@ -57,6 +62,7 @@ struct LCSettingsView: View { } else { _deviceUDID = State(initialValue: "") } + _strictHiding = State(initialValue: LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding")) } @@ -154,6 +160,16 @@ struct LCSettingsView: View { Text("Place your tweaks into the global “Tweaks” folder and LiveContainer will pick them up.") } + if sharedModel.isHiddenAppUnlocked { + Section { + Toggle(isOn: $strictHiding) { + Text("Strict Hiding Mode") + } + } footer: { + Text("Enabling this mode will only allow hidden apps to be launched by triple clicking the installed app counter.") + } + } + Section { Button { Task { await moveDanglingFolders() } @@ -236,6 +252,9 @@ struct LCSettingsView: View { .onChange(of: injectToLCItelf) { newValue in saveItem(key: "LCLoadTweaksToSelf", val: newValue) } + .onChange(of: strictHiding) { newValue in + saveAppGroupItem(key: "LCStrictHiding", val: newValue) + } .onChange(of: deviceUDID) { newValue in saveAppGroupItem(key: "LCDeviceUDID", val: newValue) } @@ -302,6 +321,12 @@ struct LCSettingsView: View { } folderNameToAppDict[folderName] = app } + for app in hiddenApps { + guard let folderName = app.getDataUUIDNoAssign() else { + continue + } + folderNameToAppDict[folderName] = app + } var foldersToDelete : [String] = [] for appDataFolderName in appDataFolderNames { @@ -376,6 +401,19 @@ struct LCSettingsView: View { } + for app in hiddenApps { + if !app.isShared { + continue + } + if let folder = app.getDataUUIDNoAssign() { + appDataFoldersInUse.update(with: folder); + } + if let folder = app.tweakFolder() { + tweakFoldersInUse.update(with: folder); + } + + } + var movedDataFolderCount = 0 let sharedDataFolders = try fm.contentsOfDirectory(atPath: LCPath.lcGroupDataPath.path) for sharedDataFolder in sharedDataFolders { diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index 928a70d..648a975 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -10,6 +10,7 @@ import SwiftUI struct LCTabView: View { @State var apps: [LCAppInfo] + @State var hiddenApps: [LCAppInfo] @State var appDataFolderNames: [String] @State var tweakFolderNames: [String] @@ -22,6 +23,7 @@ struct LCTabView: View { var tempTweakFolderNames : [String] = [] var tempApps: [LCAppInfo] = [] + var tempHiddenApps: [LCAppInfo] = [] do { // load apps @@ -34,7 +36,11 @@ struct LCTabView: View { let newApp = LCAppInfo(bundlePath: "\(LCPath.bundlePath.path)/\(appDir)")! newApp.relativeBundlePath = appDir newApp.isShared = false - tempApps.append(newApp) + if newApp.isHidden() { + tempHiddenApps.append(newApp) + } else { + tempApps.append(newApp) + } } try fm.createDirectory(at: LCPath.lcGroupBundlePath, withIntermediateDirectories: true) @@ -46,7 +52,11 @@ struct LCTabView: View { let newApp = LCAppInfo(bundlePath: "\(LCPath.lcGroupBundlePath.path)/\(appDir)")! newApp.relativeBundlePath = appDir newApp.isShared = true - tempApps.append(newApp) + if newApp.isHidden() { + tempHiddenApps.append(newApp) + } else { + tempApps.append(newApp) + } } // load document folders try fm.createDirectory(at: LCPath.dataPath, withIntermediateDirectories: true) @@ -75,11 +85,12 @@ struct LCTabView: View { _apps = State(initialValue: tempApps) _appDataFolderNames = State(initialValue: tempAppDataFolderNames) _tweakFolderNames = State(initialValue: tempTweakFolderNames) + _hiddenApps = State(initialValue: tempHiddenApps) } var body: some View { TabView { - LCAppListView(apps: $apps, appDataFolderNames: $appDataFolderNames, tweakFolderNames: $tweakFolderNames) + LCAppListView(apps: $apps, hiddenApps: $hiddenApps, appDataFolderNames: $appDataFolderNames, tweakFolderNames: $tweakFolderNames) .tabItem { Label("Apps", systemImage: "square.stack.3d.up.fill") } @@ -90,7 +101,7 @@ struct LCTabView: View { } } - LCSettingsView(apps: $apps, appDataFolderNames: $appDataFolderNames) + LCSettingsView(apps: $apps, hiddenApps: $hiddenApps, appDataFolderNames: $appDataFolderNames) .tabItem { Label("Settings", systemImage: "gearshape.fill") } @@ -100,7 +111,7 @@ struct LCTabView: View { }.onAppear() { checkLastLaunchError() } - .environmentObject(DataManager.shared.bundleIDToLaunchModel) + .environmentObject(DataManager.shared.model) } func checkLastLaunchError() { diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index 7740490..80fb758 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -18,6 +18,7 @@ struct LCWebView: View { @State private var pageTitle = "" @Binding var apps : [LCAppInfo] + @Binding var hiddenApps : [LCAppInfo] @State private var runAppAlertShow = false @State private var runAppAlertMsg = "" @@ -28,11 +29,14 @@ struct LCWebView: View { @State private var errorShow = false @State private var errorInfo = "" - init(url: Binding, apps: Binding<[LCAppInfo]>, isPresent: Binding) { + @EnvironmentObject private var sharedModel : SharedModel + + init(url: Binding, apps: Binding<[LCAppInfo]>, hiddenApps: Binding<[LCAppInfo]>, isPresent: Binding) { self.webView = WebView() self._url = url self._apps = apps self._isPresent = isPresent + self._hiddenApps = hiddenApps } var body: some View { @@ -145,16 +149,24 @@ struct LCWebView: View { public func onURLSchemeDetected(url: URL) async { var appToLaunch : LCAppInfo? = nil - appLoop: for app in apps { - if let schemes = app.urlSchemes() { - for scheme in schemes { - if let scheme = scheme as? String, scheme == url.scheme { - appToLaunch = app - break appLoop + var appListsToConsider = [apps] + if sharedModel.isHiddenAppUnlocked || !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") { + appListsToConsider.append(hiddenApps) + } + appLoop: + for appList in appListsToConsider { + for app in appList { + if let schemes = app.urlSchemes() { + for scheme in schemes { + if let scheme = scheme as? String, scheme == url.scheme { + appToLaunch = app + break appLoop + } + } } } } - } + guard let appToLaunch = appToLaunch else { errorInfo = "Scheme \"\(url.scheme!)\" cannot be opened by any app installed in LiveContainer." @@ -162,6 +174,19 @@ struct LCWebView: View { return } + if appToLaunch.isHidden() && !sharedModel.isHiddenAppUnlocked { + + do { + if !(try await LCUtils.authenticateUser()) { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } + runAppAlertMsg = "This web page is trying to launch \"\(appToLaunch.displayName()!)\", continue?" await withCheckedContinuation { c in @@ -182,6 +207,11 @@ struct LCWebView: View { for app in apps { bundleIDToAppDict[app.bundleIdentifier()!] = app } + if !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") || sharedModel.isHiddenAppUnlocked { + for app in hiddenApps { + bundleIDToAppDict[app.bundleIdentifier()!] = app + } + } var appToLaunch: LCAppInfo? = nil for bundleID in bundleIDs { @@ -194,6 +224,18 @@ struct LCWebView: View { return } + if appToLaunch.isHidden() && !sharedModel.isHiddenAppUnlocked { + do { + if !(try await LCUtils.authenticateUser()) { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } + runAppAlertMsg = "This web page can be opened in \"\(appToLaunch.displayName()!)\" according to its Associated Domains, continue?" runAppAlertShow = true await withCheckedContinuation { c in diff --git a/LiveContainerSwiftUI/ObjcBridge.swift b/LiveContainerSwiftUI/ObjcBridge.swift index 497c6c7..dee7dd0 100644 --- a/LiveContainerSwiftUI/ObjcBridge.swift +++ b/LiveContainerSwiftUI/ObjcBridge.swift @@ -11,18 +11,18 @@ import SwiftUI @objc public class LCObjcBridge: NSObject { public static var urlStrToOpen: String? = nil - public static var openUrlStrFunc: ((String) -> Void)? + public static var openUrlStrFunc: ((String) async -> Void)? @objc public static func openWebPage(urlStr: String) { if openUrlStrFunc == nil { urlStrToOpen = urlStr } else { - openUrlStrFunc!(urlStr) + Task { await openUrlStrFunc!(urlStr) } } } @objc public static func launchApp(bundleId: String) { - DataManager.shared.bundleIDToLaunchModel.bundleIdToLaunch = bundleId + DataManager.shared.model.bundleIdToLaunch = bundleId } @objc public static func getRootVC() -> UIViewController { diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 7738056..f16fd1d 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -7,6 +7,7 @@ import SwiftUI import UniformTypeIdentifiers +import LocalAuthentication struct LCPath { public static let docPath = { @@ -47,13 +48,14 @@ struct LCPath { } } -class BundleIDToLaunchModel: ObservableObject { +class SharedModel: ObservableObject { @Published var bundleIdToLaunch: String = "" + @Published var isHiddenAppUnlocked = false } class DataManager { static let shared = DataManager() - let bundleIDToLaunchModel = BundleIDToLaunchModel() + let model = SharedModel() } @@ -246,4 +248,63 @@ extension LCUtils { return nil } + + private static func authenticateUser(completion: @escaping (Bool, Error?) -> Void) { + // Create a context for authentication + let context = LAContext() + var error: NSError? + + // Check if the device supports biometric authentication + if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) { + // Determine the reason for the authentication request + let reason = "Authentication Required." + + // Evaluate the authentication policy + context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, evaluationError in + DispatchQueue.main.async { + if success { + // Authentication successful + completion(true, nil) + } else { + if let evaluationError = evaluationError as? LAError, evaluationError.code == LAError.appCancel { + completion(false, nil) + } else { + // Authentication failed + completion(false, evaluationError) + } + + } + } + } + } else { + // Biometric authentication is not available + DispatchQueue.main.async { + completion(false, error) + } + } + } + + public static func authenticateUser() async throws -> Bool { + if DataManager.shared.model.isHiddenAppUnlocked { + return true + } + + var success = false + var error : Error? = nil + await withCheckedContinuation { c in + LCUtils.authenticateUser { success1, error1 in + success = success1 + error = error1 + c.resume() + } + } + if let error = error { + throw error + } + if !success { + return false + } + DataManager.shared.model.isHiddenAppUnlocked = true + return true + } } diff --git a/LiveContainerUI/LCAppInfo.h b/LiveContainerUI/LCAppInfo.h index bec80af..bce5859 100644 --- a/LiveContainerUI/LCAppInfo.h +++ b/LiveContainerUI/LCAppInfo.h @@ -35,4 +35,6 @@ @property SignTmpStatus* _signStatus; - (NSString*)patchExec; - (void) signCleanUpWithSuccessStatus:(BOOL)isSignSuccess; +- (bool)isHidden; +- (void)setIsHidden:(bool)isHidden; @end diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index dee58d8..8170d88 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -275,5 +275,18 @@ - (void)setIsJITNeeded:(bool)isJITNeeded { _info[@"isJITNeeded"] = [NSNumber numberWithBool:isJITNeeded]; [self save]; +} + +- (bool)isHidden { + if(_info[@"isHidden"] != nil) { + return [_info[@"isHidden"] boolValue]; + } else { + return NO; + } +} +- (void)setIsHidden:(bool)isHidden { + _info[@"isHidden"] = [NSNumber numberWithBool:isHidden]; + [self save]; + } @end diff --git a/main.m b/main.m index 950a017..d60e1ac 100644 --- a/main.m +++ b/main.m @@ -18,12 +18,16 @@ static int (*appMain)(int, char**); static const char *dyldImageName; NSUserDefaults *lcUserDefaults; +NSUserDefaults *lcSharedDefaults; NSString* lcAppUrlScheme; @implementation NSUserDefaults(LiveContainer) + (instancetype)lcUserDefaults { return lcUserDefaults; } ++ (instancetype)lcSharedDefaults { + return lcSharedDefaults; +} + (NSString *)lcAppUrlScheme { return lcAppUrlScheme; } @@ -367,6 +371,7 @@ int LiveContainerMain(int argc, char *argv[]) { NSLog(@"Ignore this: %@", UIScreen.mainScreen); lcUserDefaults = NSUserDefaults.standardUserDefaults; + lcSharedDefaults = [[NSUserDefaults alloc] initWithSuiteName: [LCSharedUtils appGroupID]]; lcAppUrlScheme = NSBundle.mainBundle.infoDictionary[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0]; // move preferences first then the entire folder From 3b48909690a0c8b359fc1f62e6a31c3fc37a4405 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 12 Sep 2024 13:26:39 +0800 Subject: [PATCH 25/36] bring back AppClip icon --- LiveContainerSwiftUI/LCAppBanner.swift | 38 +++++++- LiveContainerSwiftUI/LCAppListView.swift | 26 ++++++ LiveContainerSwiftUI/LCMDMServer.swift | 93 +++++++++++++++++++ LiveContainerSwiftUI/LCSettingsView.swift | 18 ++-- .../project.pbxproj | 8 +- .../LiveContainerSwiftUIApp.swift | 16 ---- LiveContainerSwiftUI/Shared.swift | 69 ++++++++++++++ LiveContainerUI/LCAppInfo.m | 2 +- 8 files changed, 237 insertions(+), 33 deletions(-) create mode 100644 LiveContainerSwiftUI/LCMDMServer.swift delete mode 100644 LiveContainerSwiftUI/LiveContainerSwiftUIApp.swift diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 470ad64..f9b9a36 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -12,6 +12,7 @@ import UniformTypeIdentifiers protocol LCAppBannerDelegate { func removeApp(app: LCAppInfo) func changeAppVisibility(app: LCAppInfo) + func installMdm(data: Data) } struct LCAppBanner : View { @@ -163,12 +164,30 @@ struct LCAppBanner : View { } - Button { - copyLaunchUrl() + Menu { + Button { + openSafariViewToCreateAppClip() + } label: { + Label("Create App Clip", systemImage: "appclip") + } + Button { + copyLaunchUrl() + } label: { + Label("Copy Launch Url", systemImage: "link") + } + Button { + showShareSheet(item: ShareableImage(image: appInfo.icon()!, title: appInfo.displayName())) + } label: { + Label("Save App Icon", systemImage: "square.and.arrow.down") + } + + } label: { - Label("Copy Launch Url", systemImage: "link") + Label("Add to Home Screen", systemImage: "plus.app") } + + if sharedModel.isHiddenAppUnlocked { Button { Task { await toggleHidden()} @@ -181,6 +200,8 @@ struct LCAppBanner : View { } } + + if !uiIsShared { Button(role: .destructive) { Task{ await uninstall() } @@ -631,6 +652,17 @@ struct LCAppBanner : View { UIPasteboard.general.string = "livecontainer://livecontainer-launch?bundle-name=\(appInfo.relativeBundlePath!)" } + func openSafariViewToCreateAppClip() { + do { + let data = try PropertyListSerialization.data(fromPropertyList: appInfo.generateWebClipConfig()!, format: .xml, options: 0) + delegate.installMdm(data: data) + } catch { + errorShow = true + errorInfo = error.localizedDescription + } + + } + func toggleHidden() async { if appInfo.isHidden() { appInfo.setIsHidden(false) diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 3f13895..4d44734 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -43,6 +43,9 @@ struct LCAppListView : View, LCAppBannerDelegate { @State private var webViewUrlInputContent = "" @State private var webViewUrlInputContinuation : CheckedContinuation? = nil + @State var safariViewOpened = false + @State var safariViewURL = URL(string: "https://google.com")! + @EnvironmentObject private var sharedModel : SharedModel init(apps: Binding<[LCAppInfo]>, hiddenApps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>, tweakFolderNames: Binding<[String]>) { @@ -206,6 +209,9 @@ struct LCAppListView : View, LCAppBannerDelegate { .fullScreenCover(isPresented: $webViewOpened) { LCWebView(url: $webViewURL, apps: $apps, hiddenApps: $hiddenApps, isPresent: $webViewOpened) } + .fullScreenCover(isPresented: $safariViewOpened) { + SafariView(url: $safariViewURL) + } } @@ -516,4 +522,24 @@ struct LCAppListView : View, LCAppBannerDelegate { return } } + + func installMdm(data: Data) { + Task { + do { + if LCMDMServer.instance == nil { + LCMDMServer.instance = try LCMDMServer() + await withCheckedContinuation { c in + LCMDMServer.instance!.start(c) + } + safariViewURL = URL(string:"http://127.0.0.1:\(LCMDMServer.instance!.getPort())")! + } + LCMDMServer.mdmData = data + safariViewOpened = true + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + } + + } } diff --git a/LiveContainerSwiftUI/LCMDMServer.swift b/LiveContainerSwiftUI/LCMDMServer.swift new file mode 100644 index 0000000..1855ccc --- /dev/null +++ b/LiveContainerSwiftUI/LCMDMServer.swift @@ -0,0 +1,93 @@ +// +// LCMDMServer.swift +// LCMDMServer +// +// Created by s s on 2024/8/21. +// + +import Foundation +import Network + +struct LCMDMServer { + public static var instance : LCMDMServer? = nil + + public static var mdmData : Data? = nil + private let listener : NWListener + + init() throws { + self.listener = try NWListener(using: .tcp, on: .any) + } + + func start(_ continuation: CheckedContinuation?) { + // Define the state update handler + listener.stateUpdateHandler = { state in + switch state { + case .ready: + if let port = listener.port { + if let continuation = continuation { + continuation.resume() + } + } + case .failed(let error): + NSLog("[NMSL] Server failed with error: \(error)") + if let continuation = continuation { + continuation.resume() + } + default: + break + } + } + + // Define the connection handler + listener.newConnectionHandler = { connection in + connection.start(queue: .main) + + // Set up a receive handler to read data + connection.receive(minimumIncompleteLength: 1, maximumLength: 1024) { data, context, isComplete, error in + if let data = data, !data.isEmpty { + + // Create the HTTP response + let response : String + if let mdmData = LCMDMServer.mdmData { + response = """ + HTTP/1.1 200 OK + Content-Type: application/x-apple-aspen-config + Content-Length: \(mdmData.count) + + \(String(data: mdmData, encoding: .utf8)!) + """ + } else { + response = """ + HTTP/1.1 404 + """ + } + + // Send the response back to the client + connection.send(content: response.data(using: .utf8), completion: .contentProcessed({ sendError in + if let sendError = sendError { + NSLog("[LC] Failed to send response: \(sendError)") + } + connection.cancel() + })) + } else { + if let error = error { + NSLog("[LC] Error receiving data: \(error)") + } + connection.cancel() + } + } + } + + // Start the listener + listener.start(queue: .global()) + } + + // start the server if needed + func getPort() -> UInt16 { + if let port = listener.port { + return port.rawValue + } else { + return 0 + } + } +} diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index 1445517..a53a87e 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -116,15 +116,15 @@ struct LCSettingsView: View { Text("If you see frequent re-sign, enable this option.") } -// Section{ -// Toggle(isOn: $frameShortIcon) { -// Text("Frame Short Icon") -// } -// } header: { -// Text("Miscellaneous") -// } footer: { -// Text("Frame shortcut icons with LiveContainer icon.") -// } + Section{ + Toggle(isOn: $frameShortIcon) { + Text("Frame Short Icon") + } + } header: { + Text("Miscellaneous") + } footer: { + Text("Frame shortcut icons with LiveContainer icon.") + } Section { HStack { Text("Address") diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj index 7310fc3..bdaf184 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -13,7 +13,7 @@ 173564CD2C76FE3500C6C918 /* LCSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C12C76FE3500C6C918 /* LCSettingsView.swift */; }; 173564CE2C76FE3500C6C918 /* Makefile in Sources */ = {isa = PBXBuildFile; fileRef = 173564C32C76FE3500C6C918 /* Makefile */; }; 173564CF2C76FE3500C6C918 /* LCSwiftBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */; }; - 173564D02C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C52C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift */; }; + 173564D02C76FE3500C6C918 /* LCMDMServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C52C76FE3500C6C918 /* LCMDMServer.swift */; }; 173564D12C76FE3500C6C918 /* ObjcBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C62C76FE3500C6C918 /* ObjcBridge.swift */; }; 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C72C76FE3500C6C918 /* LCAppBanner.swift */; }; 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173564C82C76FE3500C6C918 /* Assets.xcassets */; }; @@ -29,7 +29,7 @@ 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LCSwiftBridge.h; sourceTree = ""; }; 173564C32C76FE3500C6C918 /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LCSwiftBridge.m; sourceTree = ""; }; - 173564C52C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LiveContainerSwiftUIApp.swift; sourceTree = ""; }; + 173564C52C76FE3500C6C918 /* LCMDMServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCMDMServer.swift; sourceTree = ""; }; 173564C62C76FE3500C6C918 /* ObjcBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcBridge.swift; sourceTree = ""; }; 173564C72C76FE3500C6C918 /* LCAppBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCAppBanner.swift; sourceTree = ""; }; 173564C82C76FE3500C6C918 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -60,7 +60,7 @@ 173564C12C76FE3500C6C918 /* LCSettingsView.swift */, 173564BD2C76FE3500C6C918 /* LCTabView.swift */, 173564C02C76FE3500C6C918 /* LCTweaksView.swift */, - 173564C52C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift */, + 173564C52C76FE3500C6C918 /* LCMDMServer.swift */, 173564C32C76FE3500C6C918 /* Makefile */, 173564C62C76FE3500C6C918 /* ObjcBridge.swift */, 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */, @@ -202,7 +202,7 @@ 173564C92C76FE3500C6C918 /* LCAppListView.swift in Sources */, 173564CC2C76FE3500C6C918 /* LCTweaksView.swift in Sources */, 173564CD2C76FE3500C6C918 /* LCSettingsView.swift in Sources */, - 173564D02C76FE3500C6C918 /* LiveContainerSwiftUIApp.swift in Sources */, + 173564D02C76FE3500C6C918 /* LCMDMServer.swift in Sources */, 173564D12C76FE3500C6C918 /* ObjcBridge.swift in Sources */, 173564CA2C76FE3500C6C918 /* LCTabView.swift in Sources */, ); diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUIApp.swift b/LiveContainerSwiftUI/LiveContainerSwiftUIApp.swift deleted file mode 100644 index e314a03..0000000 --- a/LiveContainerSwiftUI/LiveContainerSwiftUIApp.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// LiveContainerSwiftUIApp.swift -// LiveContainerSwiftUI -// -// Created by s s on 2024/8/21. -// - -import SwiftUI - -struct LiveContainerSwiftUIApp: App { - var body: some Scene { - WindowGroup { - LCTabView() - } - } -} diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index f16fd1d..7c339b9 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -8,6 +8,8 @@ import SwiftUI import UniformTypeIdentifiers import LocalAuthentication +import SafariServices +import LinkPresentation struct LCPath { public static let docPath = { @@ -70,6 +72,16 @@ extension UTType { static let lcFramework = UTType(filenameExtension: "framework", conformingTo: .package)! } +struct SafariView: UIViewControllerRepresentable { + let url: Binding + func makeUIViewController(context: UIViewControllerRepresentableContext) -> SFSafariViewController { + return SFSafariViewController(url: url.wrappedValue) + } + func updateUIViewController(_ uiViewController: SFSafariViewController, context: UIViewControllerRepresentableContext) { + + } +} + // https://stackoverflow.com/questions/56726663/how-to-add-a-textfield-to-alert-in-swiftui extension View { @@ -139,6 +151,63 @@ public struct TextFieldAlertModifier: ViewModifier { } +class ShareableImage: NSObject, UIActivityItemSource { + private let image: UIImage + private let title: String + private let subtitle: String? + + init(image: UIImage, title: String, subtitle: String? = nil) { + self.image = image + self.title = title + self.subtitle = subtitle + + super.init() + } + + func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { + return title + } + + func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { + return image + } + + func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { + let metadata = LPLinkMetadata() + + metadata.iconProvider = NSItemProvider(object: image) + metadata.title = title + if let subtitle = subtitle { + metadata.originalURL = URL(fileURLWithPath: subtitle) + } + + return metadata + } +} + +func showShareSheet(item: Any) { + let activityVC = UIActivityViewController(activityItems: [item], applicationActivities: nil) + UIApplication.shared.currentUIWindow()?.rootViewController?.present(activityVC, animated: true, completion: nil) +} + +// utility extension to easily get the window +public extension UIApplication { + func currentUIWindow() -> UIWindow? { + let connectedScenes = UIApplication.shared.connectedScenes + .filter { $0.activationState == .foregroundActive } + .compactMap { $0 as? UIWindowScene } + + let window = connectedScenes.first? + .windows + .first { $0.isKeyWindow } + + return window + + } +} + + + struct SiteAssociationDetailItem : Codable { var appID: String? var appIDs: [String]? diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index 8170d88..ef46918 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -145,7 +145,7 @@ - (NSDictionary *)generateWebClipConfig { @"PayloadVersion": @(1), @"Precomposed": @NO, @"toPayloadOrganization": @"LiveContainer", - @"URL": [NSString stringWithFormat:@"%@://livecontainer-launch?bundle-name=%@", [LCUtils appUrlScheme], self.bundlePath.lastPathComponent] + @"URL": [NSString stringWithFormat:@"livecontainer://livecontainer-launch?bundle-name=%@", self.bundlePath.lastPathComponent] }; return @{ @"ConsentText": @{ From 186f11776e2242e5209849d5e2a825a773e6ff86 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 12 Sep 2024 14:19:36 +0800 Subject: [PATCH 26/36] use fileExporter to export app icon, solve compile warnings --- LiveContainerSwiftUI/LCAppBanner.swift | 25 ++++++++-- LiveContainerSwiftUI/LCMDMServer.swift | 7 ++- LiveContainerSwiftUI/Shared.swift | 68 +++++++------------------- 3 files changed, 43 insertions(+), 57 deletions(-) diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index f9b9a36..832952b 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -55,6 +55,9 @@ struct LCAppBanner : View { @State private var confirmEnablingJIT = false @State private var confirmEnablingJITContinuation : CheckedContinuation? = nil + @State private var saveIconExporterShow = false + @State private var saveIconFile : ImageDocument? + @State private var errorShow = false @State private var errorInfo = "" @@ -150,7 +153,14 @@ struct LCAppBanner : View { .frame(height: 88) .background(RoundedRectangle(cornerSize: CGSize(width:22, height: 22)).fill(Color("AppBannerBG"))) - + .fileExporter( + isPresented: $saveIconExporterShow, + document: saveIconFile, + contentType: .image, + defaultFilename: "\(appInfo.displayName()!) Icon.png", + onCompletion: { result in + + }) .contextMenu{ Text(appInfo.relativeBundlePath) Button { @@ -176,7 +186,7 @@ struct LCAppBanner : View { Label("Copy Launch Url", systemImage: "link") } Button { - showShareSheet(item: ShareableImage(image: appInfo.icon()!, title: appInfo.displayName())) + saveIcon() } label: { Label("Save App Icon", systemImage: "square.and.arrow.down") } @@ -379,7 +389,7 @@ struct LCAppBanner : View { func runApp() async { if let runningLC = LCUtils.getAppRunningLCScheme(bundleId: self.appInfo.relativeBundlePath) { let openURL = URL(string: "\(runningLC)://livecontainer-launch?bundle-name=\(self.appInfo.relativeBundlePath!)")! - if await UIApplication.shared.canOpenURL(openURL) { + if UIApplication.shared.canOpenURL(openURL) { await UIApplication.shared.open(openURL) return } @@ -409,7 +419,9 @@ struct LCAppBanner : View { } self.isSingingInProgress = true self.observer = signProgress.observe(\.fractionCompleted) { p, v in - self.signProgress = signProgress.fractionCompleted + DispatchQueue.main.async { + self.signProgress = signProgress.fractionCompleted + } } } else if patchInfo != nil { errorInfo = patchInfo! @@ -674,5 +686,10 @@ struct LCAppBanner : View { delegate.changeAppVisibility(app: appInfo) } + func saveIcon() { + let img = appInfo.icon()! + self.saveIconFile = ImageDocument(uiImage: img) + self.saveIconExporterShow = true + } } diff --git a/LiveContainerSwiftUI/LCMDMServer.swift b/LiveContainerSwiftUI/LCMDMServer.swift index 1855ccc..8cdcb40 100644 --- a/LiveContainerSwiftUI/LCMDMServer.swift +++ b/LiveContainerSwiftUI/LCMDMServer.swift @@ -23,11 +23,10 @@ struct LCMDMServer { listener.stateUpdateHandler = { state in switch state { case .ready: - if let port = listener.port { - if let continuation = continuation { - continuation.resume() - } + if let continuation = continuation { + continuation.resume() } + case .failed(let error): NSLog("[NMSL] Server failed with error: \(error)") if let continuation = continuation { diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 7c339b9..1139dc0 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -9,7 +9,6 @@ import SwiftUI import UniformTypeIdentifiers import LocalAuthentication import SafariServices -import LinkPresentation struct LCPath { public static let docPath = { @@ -151,58 +150,29 @@ public struct TextFieldAlertModifier: ViewModifier { } -class ShareableImage: NSObject, UIActivityItemSource { - private let image: UIImage - private let title: String - private let subtitle: String? - - init(image: UIImage, title: String, subtitle: String? = nil) { - self.image = image - self.title = title - self.subtitle = subtitle - - super.init() - } - - func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { - return title +struct ImageDocument: FileDocument { + var data: Data + + static var readableContentTypes: [UTType] { + [UTType.image] // Specify that the document supports image files } - - func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { - return image + + // Initialize with data + init(uiImage: UIImage) { + self.data = uiImage.pngData()! } - - func activityViewControllerLinkMetadata(_ activityViewController: UIActivityViewController) -> LPLinkMetadata? { - let metadata = LPLinkMetadata() - - metadata.iconProvider = NSItemProvider(object: image) - metadata.title = title - if let subtitle = subtitle { - metadata.originalURL = URL(fileURLWithPath: subtitle) + + // Function to read the data from the file + init(configuration: ReadConfiguration) throws { + guard let data = configuration.file.regularFileContents else { + throw CocoaError(.fileReadCorruptFile) } - - return metadata + self.data = data } -} - -func showShareSheet(item: Any) { - let activityVC = UIActivityViewController(activityItems: [item], applicationActivities: nil) - UIApplication.shared.currentUIWindow()?.rootViewController?.present(activityVC, animated: true, completion: nil) -} - -// utility extension to easily get the window -public extension UIApplication { - func currentUIWindow() -> UIWindow? { - let connectedScenes = UIApplication.shared.connectedScenes - .filter { $0.activationState == .foregroundActive } - .compactMap { $0 as? UIWindowScene } - - let window = connectedScenes.first? - .windows - .first { $0.isKeyWindow } - - return window - + + // Write data to the file + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + return FileWrapper(regularFileWithContents: data) } } From 901282bffb516bccf56be62ddeed01c7482b97e8 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Thu, 12 Sep 2024 17:47:20 +0800 Subject: [PATCH 27/36] Ask for authentication in guest tweak & bug fix --- LCSharedUtils.h | 1 + LCSharedUtils.m | 18 ++++++ LiveContainerSwiftUI/LCAppListView.swift | 8 ++- LiveContainerSwiftUI/LCMDMServer.swift | 6 +- LiveContainerSwiftUI/Shared.swift | 6 +- LiveContainerUI/LCUtils.m | 2 +- TweakLoader/NSFileManager+GuestHooks.m | 5 ++ TweakLoader/UIKit+GuestHooks.m | 70 +++++++++++++++++++++++- TweakLoader/utils.h | 1 + 9 files changed, 109 insertions(+), 8 deletions(-) diff --git a/LCSharedUtils.h b/LCSharedUtils.h index 322ef6d..f8d6aa3 100644 --- a/LCSharedUtils.h +++ b/LCSharedUtils.h @@ -13,4 +13,5 @@ + (void)loadPreferencesFromPath:(NSString*) plistLocationFrom; + (void)moveSharedAppFolderBack; + (void)removeAppRunningByLC:(NSString*)LCScheme; ++ (NSBundle*)findBundleWithBundleId:(NSString*)bundleId; @end diff --git a/LCSharedUtils.m b/LCSharedUtils.m index 3bf42c0..28c3691 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -278,4 +278,22 @@ + (void)moveSharedAppFolderBack { } ++ (NSBundle*)findBundleWithBundleId:(NSString*)bundleId { + NSString *docPath = [NSString stringWithFormat:@"%s/Documents", getenv("LC_HOME_PATH")]; + + NSURL *appGroupFolder = nil; + + NSString *bundlePath = [NSString stringWithFormat:@"%@/Applications/%@", docPath, bundleId]; + NSBundle *appBundle = [[NSBundle alloc] initWithPath:bundlePath]; + // not found locally, let's look for the app in shared folder + if (!appBundle) { + NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:[LCSharedUtils appGroupID]]; + appGroupFolder = [appGroupPath URLByAppendingPathComponent:@"LiveContainer"]; + + bundlePath = [NSString stringWithFormat:@"%@/Applications/%@", appGroupFolder.path, bundleId]; + appBundle = [[NSBundle alloc] initWithPath:bundlePath]; + } + return appBundle; +} + @end diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 4d44734..f03d7b9 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -496,8 +496,12 @@ struct LCAppListView : View, LCAppBannerDelegate { if isFoundAppHidden && !sharedModel.isHiddenAppUnlocked { Task { do { - let _ = try await LCUtils.authenticateUser() + let result = try await LCUtils.authenticateUser() + if !result { + sharedModel.bundleIdToLaunch = "" + } } catch { + sharedModel.bundleIdToLaunch = "" errorInfo = error.localizedDescription errorShow = true } @@ -528,6 +532,8 @@ struct LCAppListView : View, LCAppBannerDelegate { do { if LCMDMServer.instance == nil { LCMDMServer.instance = try LCMDMServer() + } + if (LCMDMServer.instance!.getState() != .ready) { await withCheckedContinuation { c in LCMDMServer.instance!.start(c) } diff --git a/LiveContainerSwiftUI/LCMDMServer.swift b/LiveContainerSwiftUI/LCMDMServer.swift index 8cdcb40..7eea25a 100644 --- a/LiveContainerSwiftUI/LCMDMServer.swift +++ b/LiveContainerSwiftUI/LCMDMServer.swift @@ -28,7 +28,7 @@ struct LCMDMServer { } case .failed(let error): - NSLog("[NMSL] Server failed with error: \(error)") + NSLog("[LC] Server failed with error: \(error)") if let continuation = continuation { continuation.resume() } @@ -89,4 +89,8 @@ struct LCMDMServer { return 0 } } + + func getState() -> NWListener.State { + return listener.state + } } diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 1139dc0..dd0ca5c 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -305,7 +305,7 @@ extension LCUtils { // Authentication successful completion(true, nil) } else { - if let evaluationError = evaluationError as? LAError, evaluationError.code == LAError.appCancel { + if let evaluationError = evaluationError as? LAError, evaluationError.code == LAError.userCancel || evaluationError.code == LAError.appCancel { completion(false, nil) } else { // Authentication failed @@ -343,7 +343,9 @@ extension LCUtils { if !success { return false } - DataManager.shared.model.isHiddenAppUnlocked = true + DispatchQueue.main.async { + DataManager.shared.model.isHiddenAppUnlocked = true + } return true } } diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index 9190cbd..e52dce0 100644 --- a/LiveContainerUI/LCUtils.m +++ b/LiveContainerUI/LCUtils.m @@ -226,7 +226,7 @@ + (NSProgress *)signAppBundle:(NSURL *)path completionHandler:(void (^)(BOOL suc + (NSString *)appGroupID { static dispatch_once_t once; - static NSString *appGroupID; + static NSString *appGroupID = @"group.com.SideStore.SideStore";; dispatch_once(&once, ^{ for (NSString *group in NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"]) { NSURL *path = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:group]; diff --git a/TweakLoader/NSFileManager+GuestHooks.m b/TweakLoader/NSFileManager+GuestHooks.m index e25cf87..4c4ba58 100644 --- a/TweakLoader/NSFileManager+GuestHooks.m +++ b/TweakLoader/NSFileManager+GuestHooks.m @@ -1,5 +1,6 @@ @import Foundation; #import "utils.h" +#import "LCSharedUtils.h" __attribute__((constructor)) static void NSFMGuestHooksInit() { @@ -10,6 +11,10 @@ static void NSFMGuestHooksInit() { @implementation NSFileManager(LiveContainerHooks) - (nullable NSURL *)hook_containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier { + if([groupIdentifier isEqualToString:[NSClassFromString(@"LCSharedUtils") appGroupID]]) { + return [self hook_containerURLForSecurityApplicationGroupIdentifier: groupIdentifier]; + } + NSURL *result = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%s/Documents/Data/AppGroup/%@", getenv("LC_HOME_PATH"), groupIdentifier]]; [NSFileManager.defaultManager createDirectoryAtURL:result withIntermediateDirectories:YES attributes:nil error:nil]; return result; diff --git a/TweakLoader/UIKit+GuestHooks.m b/TweakLoader/UIKit+GuestHooks.m index 620de6b..678ef0a 100644 --- a/TweakLoader/UIKit+GuestHooks.m +++ b/TweakLoader/UIKit+GuestHooks.m @@ -2,6 +2,7 @@ #import "LCSharedUtils.h" #import "UIKitPrivate.h" #import "utils.h" +#import __attribute__((constructor)) static void UIKitGuestHooksInit() { @@ -9,13 +10,13 @@ static void UIKitGuestHooksInit() { swizzle(UIScene.class, @selector(scene:didReceiveActions:fromTransitionContext:), @selector(hook_scene:didReceiveActions:fromTransitionContext:)); } -void LCShowSwitchAppConfirmation(NSURL *url) { +void LCShowSwitchAppConfirmation(NSURL *url, NSString* bundleId) { if ([NSUserDefaults.lcUserDefaults boolForKey:@"LCSwitchAppWithoutAsking"]) { [NSClassFromString(@"LCSharedUtils") launchToGuestAppWithURL:url]; return; } - NSString *message = [NSString stringWithFormat:@"%@\nAre you sure you want to switch app? Doing so will terminate this app.", url]; + NSString *message = [NSString stringWithFormat:@"Are you sure you want to switch to %@? Doing so will terminate this app.", bundleId]; UIWindow *window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"LiveContainer" message:message preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction* okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { @@ -45,6 +46,22 @@ void LCShowSwitchAppConfirmation(NSURL *url) { objc_setAssociatedObject(alert, @"window", window, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } +void LCShowAppNotFoundAlert(NSString* bundleId) { + NSString *message = [NSString stringWithFormat:@"App %@ not found.", bundleId]; + UIWindow *window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; + UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"LiveContainer" message:message preferredStyle:UIAlertControllerStyleAlert]; + UIAlertAction* okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + window.windowScene = nil; + }]; + [alert addAction:okAction]; + window.rootViewController = [UIViewController new]; + window.windowLevel = UIApplication.sharedApplication.windows.lastObject.windowLevel + 1; + window.windowScene = (id)UIApplication.sharedApplication.connectedScenes.anyObject; + [window makeKeyAndVisible]; + [window.rootViewController presentViewController:alert animated:YES completion:nil]; + objc_setAssociatedObject(alert, @"window", window, OBJC_ASSOCIATION_RETAIN_NONATOMIC); +} + void openUniversalLink(NSString* decodedUrl) { UIActivityContinuationManager* uacm = [[UIApplication sharedApplication] _getActivityContinuationManager]; NSUserActivity* activity = [[NSUserActivity alloc] initWithActivityType:NSUserActivityTypeBrowsingWeb]; @@ -101,6 +118,32 @@ void LCOpenWebPage(NSString* webPageUrlString, NSString* originalUrl) { } +void authenticateUser(void (^completion)(BOOL success, NSError *error)) { + LAContext *context = [[LAContext alloc] init]; + NSError *error = nil; + + if ([context canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&error]) { + NSString *reason = @"Authentication Required."; + + // Evaluate the policy for both biometric and passcode authentication + [context evaluatePolicy:LAPolicyDeviceOwnerAuthentication + localizedReason:reason + reply:^(BOOL success, NSError * _Nullable evaluationError) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (success) { + completion(YES, nil); + } else { + completion(NO, evaluationError); + } + }); + }]; + } else { + dispatch_async(dispatch_get_main_queue(), ^{ + completion(NO, error); + }); + } +} + void handleLiveContainerLaunch(NSURL* url) { // If it's not current app, then switch // check if there are other LCs is running this app @@ -127,7 +170,28 @@ void handleLiveContainerLaunch(NSURL* url) { [UIApplication.sharedApplication openURL:[NSURL URLWithString:urlStr] options:@{} completionHandler:nil]; return; } - LCShowSwitchAppConfirmation(url); + + NSBundle* bundle = [NSClassFromString(@"LCSharedUtils") findBundleWithBundleId: bundleName]; + if(!bundle || ([bundle.infoDictionary[@"isHidden"] boolValue] && [NSUserDefaults.lcSharedDefaults boolForKey:@"LCStrictHiding"])) { + LCShowAppNotFoundAlert(bundleName); + } else if ([bundle.infoDictionary[@"isHidden"] boolValue]) { + // need authentication + authenticateUser(^(BOOL success, NSError *error) { + if (success) { + LCShowSwitchAppConfirmation(url, bundleName); + } else { + if ([error.domain isEqualToString:LAErrorDomain]) { + if (error.code != LAErrorUserCancel) { + NSLog(@"[LC] Authentication Error: %@", error.localizedDescription); + } + } else { + NSLog(@"[LC] Authentication Error: %@", error.localizedDescription); + } + } + }); + } else { + LCShowSwitchAppConfirmation(url, bundleName); + } } } diff --git a/TweakLoader/utils.h b/TweakLoader/utils.h index fd58f5a..3ea0f74 100644 --- a/TweakLoader/utils.h +++ b/TweakLoader/utils.h @@ -5,6 +5,7 @@ void swizzle(Class class, SEL originalAction, SEL swizzledAction); // Exported from the main executable @interface NSUserDefaults(LiveContainer) ++ (instancetype)lcSharedDefaults; + (instancetype)lcUserDefaults; + (NSString *)lcAppUrlScheme; @end From 4d836b72dcff922594f50fd59960737d6c46af01 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Fri, 13 Sep 2024 14:10:24 +0800 Subject: [PATCH 28/36] bug fix, open data folder, change app group folder to shared folder --- LiveContainerSwiftUI/LCAppBanner.swift | 33 ++++-- LiveContainerSwiftUI/LCAppListView.swift | 32 ++---- LiveContainerSwiftUI/LCMDMServer.swift | 96 ----------------- LiveContainerSwiftUI/LCSettingsView.swift | 101 +++++++++++++++--- LiveContainerSwiftUI/LCSwiftBridge.m | 8 ++ .../project.pbxproj | 4 - LiveContainerSwiftUI/Makefile | 1 + LiveContainerSwiftUI/Shared.swift | 2 + TweakLoader/NSFileManager+GuestHooks.m | 3 +- 9 files changed, 131 insertions(+), 149 deletions(-) delete mode 100644 LiveContainerSwiftUI/LCMDMServer.swift diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 832952b..7d9371a 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -224,6 +224,17 @@ struct LCAppBanner : View { Label("Convert to Shared App", systemImage: "arrowshape.turn.up.left") } + Menu(content: { + Picker(selection: $uiPickerTweakFolder , label: Text("")) { + Label("None", systemImage: "nosign").tag(Optional(nil)) + ForEach(tweakFolders, id:\.self) { folderName in + Text(folderName).tag(Optional(folderName)) + } + } + }, label: { + Label("Change Tweak Folder", systemImage: "gear") + }) + Menu(content: { Button { Task{ await createFolder() } @@ -248,17 +259,14 @@ struct LCAppBanner : View { }, label: { Label("Change Data Folder", systemImage: "folder.badge.questionmark") }) - - Menu(content: { - Picker(selection: $uiPickerTweakFolder , label: Text("")) { - Label("None", systemImage: "nosign").tag(Optional(nil)) - ForEach(tweakFolders, id:\.self) { folderName in - Text(folderName).tag(Optional(folderName)) - } + if uiDataFolder != nil { + Button { + openDataFolder() + } label: { + Label("Open Data Folder", systemImage: "folder") } - }, label: { - Label("Change Tweak Folder", systemImage: "gear") - }) + } + } else if LCUtils.multiLCStatus != 2 { Button { Task { await movePrivateDoc() } @@ -508,6 +516,11 @@ struct LCAppBanner : View { } + func openDataFolder() { + let url = URL(string:"shareddocuments://\(LCPath.docPath.path)/Data/Application/\(appInfo.dataUUID()!)") + UIApplication.shared.open(url!) + } + func setTweakFolder(folderName: String?) { self.appInfo.setTweakFolder(folderName) self.uiTweakFolder = folderName diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index f03d7b9..af90e46 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -412,7 +412,7 @@ struct LCAppListView : View, LCAppBannerDelegate { // patch it let patchResult = finalNewApp?.patchExec() if patchResult != nil && patchResult != "SignNeeded" { - throw patchResult!; + throw patchResult! } if patchResult == "SignNeeded" { // sign it @@ -420,6 +420,7 @@ struct LCAppListView : View, LCAppBannerDelegate { var success = false await withCheckedContinuation { c in let signProgress = LCUtils.signAppBundle(outputFolder) { success1, error1 in + finalNewApp?.signCleanUp(withSuccessStatus: success1) error = error1 success = success1 c.resume() @@ -428,10 +429,8 @@ struct LCAppListView : View, LCAppBannerDelegate { } if let error = error { - finalNewApp?.signCleanUp(withSuccessStatus: false) throw error } - finalNewApp?.signCleanUp(withSuccessStatus: success) if !success { throw "Unknow error occurred" } @@ -442,8 +441,10 @@ struct LCAppListView : View, LCAppBannerDelegate { if let appToReplace = appToReplace { finalNewApp?.setDataUUID(appToReplace.getDataUUIDNoAssign()) } - self.apps.append(finalNewApp!) - self.installprogressVisible = false + DispatchQueue.main.async { + self.apps.append(finalNewApp!) + self.installprogressVisible = false + } } func removeApp(app: LCAppInfo) { @@ -528,24 +529,7 @@ struct LCAppListView : View, LCAppBannerDelegate { } func installMdm(data: Data) { - Task { - do { - if LCMDMServer.instance == nil { - LCMDMServer.instance = try LCMDMServer() - } - if (LCMDMServer.instance!.getState() != .ready) { - await withCheckedContinuation { c in - LCMDMServer.instance!.start(c) - } - safariViewURL = URL(string:"http://127.0.0.1:\(LCMDMServer.instance!.getPort())")! - } - LCMDMServer.mdmData = data - safariViewOpened = true - } catch { - errorInfo = error.localizedDescription - errorShow = true - } - } - + safariViewURL = URL(string:"data:application/x-apple-aspen-config;base64,\(data.base64EncodedString())")! + safariViewOpened = true } } diff --git a/LiveContainerSwiftUI/LCMDMServer.swift b/LiveContainerSwiftUI/LCMDMServer.swift deleted file mode 100644 index 7eea25a..0000000 --- a/LiveContainerSwiftUI/LCMDMServer.swift +++ /dev/null @@ -1,96 +0,0 @@ -// -// LCMDMServer.swift -// LCMDMServer -// -// Created by s s on 2024/8/21. -// - -import Foundation -import Network - -struct LCMDMServer { - public static var instance : LCMDMServer? = nil - - public static var mdmData : Data? = nil - private let listener : NWListener - - init() throws { - self.listener = try NWListener(using: .tcp, on: .any) - } - - func start(_ continuation: CheckedContinuation?) { - // Define the state update handler - listener.stateUpdateHandler = { state in - switch state { - case .ready: - if let continuation = continuation { - continuation.resume() - } - - case .failed(let error): - NSLog("[LC] Server failed with error: \(error)") - if let continuation = continuation { - continuation.resume() - } - default: - break - } - } - - // Define the connection handler - listener.newConnectionHandler = { connection in - connection.start(queue: .main) - - // Set up a receive handler to read data - connection.receive(minimumIncompleteLength: 1, maximumLength: 1024) { data, context, isComplete, error in - if let data = data, !data.isEmpty { - - // Create the HTTP response - let response : String - if let mdmData = LCMDMServer.mdmData { - response = """ - HTTP/1.1 200 OK - Content-Type: application/x-apple-aspen-config - Content-Length: \(mdmData.count) - - \(String(data: mdmData, encoding: .utf8)!) - """ - } else { - response = """ - HTTP/1.1 404 - """ - } - - // Send the response back to the client - connection.send(content: response.data(using: .utf8), completion: .contentProcessed({ sendError in - if let sendError = sendError { - NSLog("[LC] Failed to send response: \(sendError)") - } - connection.cancel() - })) - } else { - if let error = error { - NSLog("[LC] Error receiving data: \(error)") - } - connection.cancel() - } - } - } - - // Start the listener - listener.start(queue: .global()) - } - - // start the server if needed - func getPort() -> UInt16 { - if let port = listener.port { - return port.rawValue - } else { - return 0 - } - } - - func getState() -> NWListener.State { - return listener.state - } -} diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index a53a87e..7eb6d85 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -171,16 +171,30 @@ struct LCSettingsView: View { } Section { - Button { - Task { await moveDanglingFolders() } - } label: { - Text("Move Dangling Folders Out of App Group") - } - Button(role:.destructive) { - Task { await cleanUpUnusedFolders() } - } label: { - Text("Clean Unused Data Folders") + if LCUtils.multiLCStatus != 2 { + Button { + moveAppGroupFolderFromPrivateToAppGroup() + } label: { + Text("Move Private App Group to Shared Documents Folder") + } + Button { + moveAppGroupFolderFromAppGroupToPrivate() + } label: { + Text("Move Shared App Group Files to Private Documents Folder") + } + + Button { + Task { await moveDanglingFolders() } + } label: { + Text("Move Dangling Folders Out of App Group") + } + Button(role:.destructive) { + Task { await cleanUpUnusedFolders() } + } label: { + Text("Clean Unused Data Folders") + } } + Button(role:.destructive) { Task { await removeKeyChain() } } label: { @@ -197,11 +211,13 @@ struct LCSettingsView: View { .listRowInsets(EdgeInsets()) } .navigationBarTitle("Settings") - .alert(isPresented: $errorShow){ - Alert(title: Text("Error"), message: Text(errorInfo)) + .alert("Error", isPresented: $errorShow){ + } message: { + Text(errorInfo) } - .alert(isPresented: $successShow){ - Alert(title: Text("Success"), message: Text(successInfo)) + .alert("Success", isPresented: $successShow){ + } message: { + Text(successInfo) } .alert("Data Folder Clean Up", isPresented: $confirmAppFolderRemovalShow) { if folderRemoveCount > 0 { @@ -441,5 +457,62 @@ struct LCSettingsView: View { errorShow = true } } - + + func moveAppGroupFolderFromAppGroupToPrivate() { + let fm = FileManager() + do { + if !fm.fileExists(atPath: LCPath.appGroupPath.path) { + try fm.createDirectory(atPath: LCPath.appGroupPath.path, withIntermediateDirectories: true) + } + if !fm.fileExists(atPath: LCPath.lcGroupAppGroupPath.path) { + try fm.createDirectory(atPath: LCPath.lcGroupAppGroupPath.path, withIntermediateDirectories: true) + } + + let privateFolderContents = try fm.contentsOfDirectory(at: LCPath.appGroupPath, includingPropertiesForKeys: nil) + let sharedFolderContents = try fm.contentsOfDirectory(at: LCPath.lcGroupAppGroupPath, includingPropertiesForKeys: nil) + if privateFolderContents.count > 0 { + errorInfo = "There are files in the private app group folder. Clean it up and try again." + errorShow = true + return + } + for file in sharedFolderContents { + try fm.moveItem(at: file, to: LCPath.appGroupPath.appendingPathComponent(file.lastPathComponent)) + } + successInfo = "Move success." + successShow = true + + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + } + + func moveAppGroupFolderFromPrivateToAppGroup() { + let fm = FileManager() + do { + if !fm.fileExists(atPath: LCPath.appGroupPath.path) { + try fm.createDirectory(atPath: LCPath.appGroupPath.path, withIntermediateDirectories: true) + } + if !fm.fileExists(atPath: LCPath.lcGroupAppGroupPath.path) { + try fm.createDirectory(atPath: LCPath.lcGroupAppGroupPath.path, withIntermediateDirectories: true) + } + + let privateFolderContents = try fm.contentsOfDirectory(at: LCPath.appGroupPath, includingPropertiesForKeys: nil) + let sharedFolderContents = try fm.contentsOfDirectory(at: LCPath.lcGroupAppGroupPath, includingPropertiesForKeys: nil) + if sharedFolderContents.count > 0 { + errorInfo = "There are files in the shared app group folder. Move it out first and try again." + errorShow = true + return + } + for file in privateFolderContents { + try fm.moveItem(at: file, to: LCPath.lcGroupAppGroupPath.appendingPathComponent(file.lastPathComponent)) + } + successInfo = "Move success." + successShow = true + + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + } } diff --git a/LiveContainerSwiftUI/LCSwiftBridge.m b/LiveContainerSwiftUI/LCSwiftBridge.m index 68bc858..2fd3332 100644 --- a/LiveContainerSwiftUI/LCSwiftBridge.m +++ b/LiveContainerSwiftUI/LCSwiftBridge.m @@ -24,3 +24,11 @@ + (void)launchAppWithBundleId:(NSString* _Nonnull)bundleId { } @end + +// make SFSafariView happy and open data: URLs +@implementation NSURL(hack) +- (BOOL)safari_isHTTPFamilyURL { + // Screw it, Apple + return YES; +} +@end diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj index bdaf184..12077ab 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -13,7 +13,6 @@ 173564CD2C76FE3500C6C918 /* LCSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C12C76FE3500C6C918 /* LCSettingsView.swift */; }; 173564CE2C76FE3500C6C918 /* Makefile in Sources */ = {isa = PBXBuildFile; fileRef = 173564C32C76FE3500C6C918 /* Makefile */; }; 173564CF2C76FE3500C6C918 /* LCSwiftBridge.m in Sources */ = {isa = PBXBuildFile; fileRef = 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */; }; - 173564D02C76FE3500C6C918 /* LCMDMServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C52C76FE3500C6C918 /* LCMDMServer.swift */; }; 173564D12C76FE3500C6C918 /* ObjcBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C62C76FE3500C6C918 /* ObjcBridge.swift */; }; 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C72C76FE3500C6C918 /* LCAppBanner.swift */; }; 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173564C82C76FE3500C6C918 /* Assets.xcassets */; }; @@ -29,7 +28,6 @@ 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = LCSwiftBridge.h; sourceTree = ""; }; 173564C32C76FE3500C6C918 /* Makefile */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.make; path = Makefile; sourceTree = ""; }; 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = LCSwiftBridge.m; sourceTree = ""; }; - 173564C52C76FE3500C6C918 /* LCMDMServer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCMDMServer.swift; sourceTree = ""; }; 173564C62C76FE3500C6C918 /* ObjcBridge.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ObjcBridge.swift; sourceTree = ""; }; 173564C72C76FE3500C6C918 /* LCAppBanner.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCAppBanner.swift; sourceTree = ""; }; 173564C82C76FE3500C6C918 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -60,7 +58,6 @@ 173564C12C76FE3500C6C918 /* LCSettingsView.swift */, 173564BD2C76FE3500C6C918 /* LCTabView.swift */, 173564C02C76FE3500C6C918 /* LCTweaksView.swift */, - 173564C52C76FE3500C6C918 /* LCMDMServer.swift */, 173564C32C76FE3500C6C918 /* Makefile */, 173564C62C76FE3500C6C918 /* ObjcBridge.swift */, 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */, @@ -202,7 +199,6 @@ 173564C92C76FE3500C6C918 /* LCAppListView.swift in Sources */, 173564CC2C76FE3500C6C918 /* LCTweaksView.swift in Sources */, 173564CD2C76FE3500C6C918 /* LCSettingsView.swift in Sources */, - 173564D02C76FE3500C6C918 /* LCMDMServer.swift in Sources */, 173564D12C76FE3500C6C918 /* ObjcBridge.swift in Sources */, 173564CA2C76FE3500C6C918 /* LCTabView.swift in Sources */, ); diff --git a/LiveContainerSwiftUI/Makefile b/LiveContainerSwiftUI/Makefile index 9b3ece0..b78979a 100644 --- a/LiveContainerSwiftUI/Makefile +++ b/LiveContainerSwiftUI/Makefile @@ -20,4 +20,5 @@ LiveContainerSwiftUI_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks include $(THEOS_MAKE_PATH)/framework.mk all:: + @rm ../Resources/Assets.car @/Applications/Xcode.app/Contents/Developer/usr/bin/actool Assets.xcassets --compile ../Resources --platform iphoneos --minimum-deployment-target 14.0 diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index dd0ca5c..61635a0 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -17,6 +17,7 @@ struct LCPath { }() public static let bundlePath = docPath.appendingPathComponent("Applications") public static let dataPath = docPath.appendingPathComponent("Data/Application") + public static let appGroupPath = docPath.appendingPathComponent("Data/AppGroup") public static let tweakPath = docPath.appendingPathComponent("Tweaks") public static let lcGroupDocPath = { @@ -33,6 +34,7 @@ struct LCPath { }() public static let lcGroupBundlePath = lcGroupDocPath.appendingPathComponent("Applications") public static let lcGroupDataPath = lcGroupDocPath.appendingPathComponent("Data/Application") + public static let lcGroupAppGroupPath = lcGroupDocPath.appendingPathComponent("Data/AppGroup") public static let lcGroupTweakPath = lcGroupDocPath.appendingPathComponent("Tweaks") public static func ensureAppGroupPaths() throws { diff --git a/TweakLoader/NSFileManager+GuestHooks.m b/TweakLoader/NSFileManager+GuestHooks.m index 4c4ba58..617daa8 100644 --- a/TweakLoader/NSFileManager+GuestHooks.m +++ b/TweakLoader/NSFileManager+GuestHooks.m @@ -14,8 +14,9 @@ - (nullable NSURL *)hook_containerURLForSecurityApplicationGroupIdentifier:(NSSt if([groupIdentifier isEqualToString:[NSClassFromString(@"LCSharedUtils") appGroupID]]) { return [self hook_containerURLForSecurityApplicationGroupIdentifier: groupIdentifier]; } + NSURL *appGroupPath = [self hook_containerURLForSecurityApplicationGroupIdentifier:[NSClassFromString(@"LCSharedUtils") appGroupID]]; - NSURL *result = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%s/Documents/Data/AppGroup/%@", getenv("LC_HOME_PATH"), groupIdentifier]]; + NSURL *result = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/LiveContainer/Data/AppGroup/%@", appGroupPath.path, groupIdentifier]]; [NSFileManager.defaultManager createDirectoryAtURL:result withIntermediateDirectories:YES attributes:nil error:nil]; return result; } From cd2c019812f8f3d240ef466d2f37d0ad29323a2a Mon Sep 17 00:00:00 2001 From: Huge_Black <60165378+hugeBlack@users.noreply.github.com> Date: Fri, 13 Sep 2024 16:08:16 +0800 Subject: [PATCH 29/36] fix GitHub build not packing Assets.car (#2) * Update Makefile * Update Makefile * Update Makefile * move to main make file --- .gitignore | 3 +-- LiveContainerSwiftUI/Makefile | 6 +----- Makefile | 1 + 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index d7927b6..a0dbe6d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ packages/ .DS_Store LiveContainer.xcodeproj project.xcworkspace -xcuserdata -Resources/Assets.car \ No newline at end of file +xcuserdata \ No newline at end of file diff --git a/LiveContainerSwiftUI/Makefile b/LiveContainerSwiftUI/Makefile index b78979a..7668c0c 100644 --- a/LiveContainerSwiftUI/Makefile +++ b/LiveContainerSwiftUI/Makefile @@ -17,8 +17,4 @@ LiveContainerSwiftUI_CFLAGS = \ LiveContainerSwiftUI_LIBRARIES = archive LiveContainerSwiftUI_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks -include $(THEOS_MAKE_PATH)/framework.mk - -all:: - @rm ../Resources/Assets.car - @/Applications/Xcode.app/Contents/Developer/usr/bin/actool Assets.xcassets --compile ../Resources --platform iphoneos --minimum-deployment-target 14.0 +include $(THEOS_MAKE_PATH)/framework.mk \ No newline at end of file diff --git a/Makefile b/Makefile index 3dcfb50..7764a7e 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ include $(THEOS_MAKE_PATH)/aggregate.mk # Make the executable name longer so we have space to overwrite it with the guest app's name before-package:: + @/Applications/Xcode.app/Contents/Developer/usr/bin/actool LiveContainerSwiftUI/Assets.xcassets --compile $(THEOS_STAGING_DIR)/Applications/LiveContainer.app --platform iphoneos --minimum-deployment-target 14.0 @cp $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/LiveContainer $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/JITLessSetup @ldid -Sentitlements_setup.xml $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/JITLessSetup @mv $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/LiveContainer $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/LiveContainer_PleaseDoNotShortenTheExecutableNameBecauseItIsUsedToReserveSpaceForOverwritingThankYou From cafde56091a05d957cdced38b94f150ca0e7173e Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Sun, 15 Sep 2024 18:37:12 +0800 Subject: [PATCH 30/36] fix file picker issue, fix app crash after install, rearrange app managing menu, fix wrong guest app group path --- LiveContainerSwiftUI/LCAppBanner.swift | 136 ++++++++++++++--------- LiveContainerSwiftUI/LCAppListView.swift | 45 +++----- LiveContainerUI/LCAppInfo.h | 11 +- LiveContainerUI/LCAppInfo.m | 124 +++++++++++---------- TweakLoader/DocumentPicker.m | 30 +++++ TweakLoader/Makefile | 2 +- TweakLoader/NSFileManager+GuestHooks.m | 5 +- TweakLoader/utils.h | 1 + main.m | 6 +- 9 files changed, 209 insertions(+), 151 deletions(-) create mode 100644 TweakLoader/DocumentPicker.m diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 7d9371a..998a008 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -163,15 +163,15 @@ struct LCAppBanner : View { }) .contextMenu{ Text(appInfo.relativeBundlePath) - Button { - Task { await toggleJITNeeded()} - } label: { - if uiIsJITNeeded { - Label("Don't Need JIT", systemImage: "bolt.slash") - } else { - Label("Mark as JIT Needed", systemImage: "bolt") + + if !uiIsShared { + if uiDataFolder != nil { + Button { + openDataFolder() + } label: { + Label("Open Data Folder", systemImage: "folder") + } } - } Menu { @@ -196,6 +196,16 @@ struct LCAppBanner : View { Label("Add to Home Screen", systemImage: "plus.app") } + Button { + Task { await toggleJITNeeded()} + } label: { + if uiIsJITNeeded { + Label("Don't Need JIT", systemImage: "bolt.slash") + } else { + Label("Mark as JIT Needed", systemImage: "bolt") + } + + } if sharedModel.isHiddenAppUnlocked { @@ -210,14 +220,15 @@ struct LCAppBanner : View { } } + + Button { + Task{ await forceResign() } + } label: { + Label("Force Sign", systemImage: "signature") + } if !uiIsShared { - Button(role: .destructive) { - Task{ await uninstall() } - } label: { - Label("Uninstall", systemImage: "trash") - } Button { Task { await moveToAppGroup()} } label: { @@ -259,12 +270,11 @@ struct LCAppBanner : View { }, label: { Label("Change Data Folder", systemImage: "folder.badge.questionmark") }) - if uiDataFolder != nil { - Button { - openDataFolder() - } label: { - Label("Open Data Folder", systemImage: "folder") - } + + Button(role: .destructive) { + Task{ await uninstall() } + } label: { + Label("Uninstall", systemImage: "trash") } } else if LCUtils.multiLCStatus != 2 { @@ -404,51 +414,71 @@ struct LCAppBanner : View { } isAppRunning = true - let patchInfo = appInfo.patchExec() - if patchInfo == "SignNeeded" { - let bundlePath = URL(fileURLWithPath: appInfo.bundlePath()) - let signProgress = LCUtils.signAppBundle(bundlePath) { success, error in - self.appInfo.signCleanUp(withSuccessStatus: success) - self.isSingingInProgress = false - if success { - self.isSingingInProgress = false - UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") - LCUtils.launchToGuestApp() - } else { - errorInfo = error != nil ? error!.localizedDescription : "Signing failed with unknown error" - errorShow = true + var signError : String? = nil + await withCheckedContinuation({ c in + appInfo.patchExecAndSignIfNeed(completionHandler: { error in + signError = error; + c.resume() + }, progressHandler: { signProgress in + guard let signProgress else { + return } - } - guard let signProgress = signProgress else { - errorInfo = "Failed to initiate signing!" - errorShow = true - self.isAppRunning = false - return - } - self.isSingingInProgress = true - self.observer = signProgress.observe(\.fractionCompleted) { p, v in - DispatchQueue.main.async { - self.signProgress = signProgress.fractionCompleted + self.isSingingInProgress = true + self.observer = signProgress.observe(\.fractionCompleted) { p, v in + DispatchQueue.main.async { + self.signProgress = signProgress.fractionCompleted + } } - } - } else if patchInfo != nil { - errorInfo = patchInfo! + }, forceSign: false) + }) + self.isSingingInProgress = false + if let signError { + errorInfo = signError errorShow = true self.isAppRunning = false return + } + + UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") + if appInfo.isJITNeeded() { + await self.jitLaunch() } else { - UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") - if appInfo.isJITNeeded() { - await self.jitLaunch() - } else { - LCUtils.launchToGuestApp() - } - + LCUtils.launchToGuestApp() } + self.isAppRunning = false } + func forceResign() async { + self.isAppRunning = true + var signError : String? = nil + await withCheckedContinuation({ c in + appInfo.patchExecAndSignIfNeed(completionHandler: { error in + signError = error; + c.resume() + }, progressHandler: { signProgress in + guard let signProgress else { + return + } + self.isSingingInProgress = true + self.observer = signProgress.observe(\.fractionCompleted) { p, v in + DispatchQueue.main.async { + self.signProgress = signProgress.fractionCompleted + } + } + }, forceSign: true) + }) + self.isSingingInProgress = false + if let signError { + errorInfo = signError + errorShow = true + self.isAppRunning = false + return + } + self.isAppRunning = false + } + func setDataFolder(folderName: String?) { self.appInfo.setDataUUID(folderName!) self.uiDataFolder = folderName diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index af90e46..9dbd3b6 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -410,39 +410,30 @@ struct LCAppListView : View, LCAppBannerDelegate { finalNewApp?.relativeBundlePath = appRelativePath // patch it - let patchResult = finalNewApp?.patchExec() - if patchResult != nil && patchResult != "SignNeeded" { - throw patchResult! - } - if patchResult == "SignNeeded" { - // sign it - var error : Error? = nil - var success = false - await withCheckedContinuation { c in - let signProgress = LCUtils.signAppBundle(outputFolder) { success1, error1 in - finalNewApp?.signCleanUp(withSuccessStatus: success1) - error = error1 - success = success1 - c.resume() - } + guard let finalNewApp else { + errorInfo = "Failed to Initialize AppInfo!" + errorShow = true + return + } + var signError : String? = nil + await withCheckedContinuation({ c in + finalNewApp.patchExecAndSignIfNeed(completionHandler: { error in + signError = error + c.resume() + }, progressHandler: { signProgress in installProgress.addChild(signProgress!, withPendingUnitCount: 20) - } - - if let error = error { - throw error - } - if !success { - throw "Unknow error occurred" - } - - + }, forceSign: false) + }) + + if let signError { + throw signError } // set data folder to the folder of the chosen app if let appToReplace = appToReplace { - finalNewApp?.setDataUUID(appToReplace.getDataUUIDNoAssign()) + finalNewApp.setDataUUID(appToReplace.getDataUUIDNoAssign()) } DispatchQueue.main.async { - self.apps.append(finalNewApp!) + self.apps.append(finalNewApp) self.installprogressVisible = false } } diff --git a/LiveContainerUI/LCAppInfo.h b/LiveContainerUI/LCAppInfo.h index bce5859..4a36374 100644 --- a/LiveContainerUI/LCAppInfo.h +++ b/LiveContainerUI/LCAppInfo.h @@ -1,13 +1,6 @@ #import #import -@interface SignTmpStatus : NSObject -@property NSUInteger newSignId; -@property NSString *tmpExecPath; -@property NSString *infoPath; - -@end - @interface LCAppInfo : NSObject { NSMutableDictionary* _info; NSString* _bundlePath; @@ -32,9 +25,7 @@ - (instancetype)initWithBundlePath:(NSString*)bundlePath; - (NSDictionary *)generateWebClipConfig; - (void)save; -@property SignTmpStatus* _signStatus; -- (NSString*)patchExec; -- (void) signCleanUpWithSuccessStatus:(BOOL)isSignSuccess; +- (void)patchExecAndSignIfNeedWithCompletionHandler:(void(^)(NSString* errorInfo))completetionHandler progressHandler:(void(^)(NSProgress* errorInfo))progressHandler forceSign:(BOOL)forceSign; - (bool)isHidden; - (void)setIsHidden:(bool)isHidden; @end diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index ef46918..22dfc10 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -5,12 +5,9 @@ #import "LCAppInfo.h" #import "LCUtils.h" -@implementation SignTmpStatus -@end - @implementation LCAppInfo - (instancetype)initWithBundlePath:(NSString*)bundlePath { - self = [super init]; + self = [super init]; self.isShared = false; if(self) { _bundlePath = bundlePath; @@ -167,23 +164,26 @@ - (void)save { [_info writeToFile:[NSString stringWithFormat:@"%@/Info.plist", _bundlePath] atomically:YES]; } -- (void)preprocessBundleBeforeSiging:(NSURL *)bundleURL { - // Remove faulty file - [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"LiveContainer"] error:nil]; - // Remove PlugIns folder - [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"PlugIns"] error:nil]; - // Remove code signature from all library files - [LCUtils removeCodeSignatureFromBundleURL:bundleURL]; - +- (void)preprocessBundleBeforeSiging:(NSURL *)bundleURL completion:(dispatch_block_t)completion { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ + // Remove faulty file + [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"LiveContainer"] error:nil]; + // Remove PlugIns folder + [NSFileManager.defaultManager removeItemAtURL:[bundleURL URLByAppendingPathComponent:@"PlugIns"] error:nil]; + // Remove code signature from all library files + [LCUtils removeCodeSignatureFromBundleURL:bundleURL]; + dispatch_async(dispatch_get_main_queue(), completion); + }); } // return "SignNeeded" if sign is needed, other wise return an error -- (NSString*)patchExec { +- (void)patchExecAndSignIfNeedWithCompletionHandler:(void(^)(NSString* errorInfo))completetionHandler progressHandler:(void(^)(NSProgress* errorInfo))progressHandler forceSign:(BOOL)forceSign { NSString *appPath = self.bundlePath; NSString *infoPath = [NSString stringWithFormat:@"%@/Info.plist", appPath]; NSMutableDictionary *info = [NSMutableDictionary dictionaryWithContentsOfFile:infoPath]; if (!info) { - return @"Info.plist not found"; + completetionHandler(@"Info.plist not found"); + return; } // Update patch @@ -194,14 +194,16 @@ - (NSString*)patchExec { LCPatchExecSlice(path, header); }); if (error) { - return error; + completetionHandler(error); + return; } info[@"LCPatchRevision"] = @(currentPatchRev); [info writeToFile:infoPath atomically:YES]; } if (!LCUtils.certificatePassword) { - return nil; + completetionHandler(nil); + return; } int signRevision = 1; @@ -213,55 +215,65 @@ - (NSString*)patchExec { CC_SHA1(LCUtils.certificateData.bytes, (CC_LONG)LCUtils.certificateData.length, digest); signID = *(uint64_t *)digest + signRevision; } else { - return @"Failed to find ALTCertificate.p12. Please refresh your store and try again."; + completetionHandler(@"Failed to find ALTCertificate.p12. Please refresh your store and try again."); + return; } // Sign app if JIT-less is set up - if ([info[@"LCJITLessSignID"] unsignedLongValue] != signID) { + if ([info[@"LCJITLessSignID"] unsignedLongValue] != signID || forceSign) { NSURL *appPathURL = [NSURL fileURLWithPath:appPath]; - [self preprocessBundleBeforeSiging:appPathURL]; - // We need to temporarily fake bundle ID and main executable to sign properly - NSString *tmpExecPath = [appPath stringByAppendingPathComponent:@"LiveContainer.tmp"]; - if (!info[@"LCBundleIdentifier"]) { - // Don't let main executable get entitlements - [NSFileManager.defaultManager copyItemAtPath:NSBundle.mainBundle.executablePath toPath:tmpExecPath error:nil]; + [self preprocessBundleBeforeSiging:appPathURL completion:^{ + // We need to temporarily fake bundle ID and main executable to sign properly + NSString *tmpExecPath = [appPath stringByAppendingPathComponent:@"LiveContainer.tmp"]; + if (!info[@"LCBundleIdentifier"]) { + // Don't let main executable get entitlements + [NSFileManager.defaultManager copyItemAtPath:NSBundle.mainBundle.executablePath toPath:tmpExecPath error:nil]; - info[@"LCBundleExecutable"] = info[@"CFBundleExecutable"]; - info[@"LCBundleIdentifier"] = info[@"CFBundleIdentifier"]; - info[@"CFBundleExecutable"] = tmpExecPath.lastPathComponent; - info[@"CFBundleIdentifier"] = NSBundle.mainBundle.bundleIdentifier; - [info writeToFile:infoPath atomically:YES]; - } - info[@"CFBundleExecutable"] = info[@"LCBundleExecutable"]; - info[@"CFBundleIdentifier"] = info[@"LCBundleIdentifier"]; - [info removeObjectForKey:@"LCBundleExecutable"]; - [info removeObjectForKey:@"LCBundleIdentifier"]; - self._signStatus = [[SignTmpStatus alloc] init]; - self._signStatus.newSignId = signID; - self._signStatus.tmpExecPath = tmpExecPath; - self._signStatus.infoPath = infoPath; - - return @"SignNeeded"; + info[@"LCBundleExecutable"] = info[@"CFBundleExecutable"]; + info[@"LCBundleIdentifier"] = info[@"CFBundleIdentifier"]; + info[@"CFBundleExecutable"] = tmpExecPath.lastPathComponent; + info[@"CFBundleIdentifier"] = NSBundle.mainBundle.bundleIdentifier; + [info writeToFile:infoPath atomically:YES]; + } + info[@"CFBundleExecutable"] = info[@"LCBundleExecutable"]; + info[@"CFBundleIdentifier"] = info[@"LCBundleIdentifier"]; + [info removeObjectForKey:@"LCBundleExecutable"]; + [info removeObjectForKey:@"LCBundleIdentifier"]; - } - return nil; -} + __block NSProgress *progress = [LCUtils signAppBundle:appPathURL + completionHandler:^(BOOL success, NSError *_Nullable error) { + dispatch_async(dispatch_get_main_queue(), ^{ + if (!error) { + info[@"LCJITLessSignID"] = @(signID); + } + + // Remove fake main executable + [NSFileManager.defaultManager removeItemAtPath:tmpExecPath error:nil]; + + // Save sign ID and restore bundle ID + [info writeToFile:infoPath atomically:YES]; + + + if(error) { + completetionHandler(error.localizedDescription); + return; + } else { + completetionHandler(nil); + return; + } + + }); + }]; + if (progress) { + progressHandler(progress); + } + }]; -- (void) signCleanUpWithSuccessStatus:(BOOL)isSignSuccess { - if(self._signStatus == nil) { + } else { + // no need to sign again + completetionHandler(nil); return; } - if (isSignSuccess) { - _info[@"LCJITLessSignID"] = @(self._signStatus.newSignId); - } - - // Remove fake main executable - [NSFileManager.defaultManager removeItemAtPath:self._signStatus.tmpExecPath error:nil]; - - // Save sign ID and restore bundle ID - [_info writeToFile:self._signStatus.infoPath atomically:YES]; - self._signStatus = nil; - return; } - (bool)isJITNeeded { diff --git a/TweakLoader/DocumentPicker.m b/TweakLoader/DocumentPicker.m new file mode 100644 index 0000000..f59369a --- /dev/null +++ b/TweakLoader/DocumentPicker.m @@ -0,0 +1,30 @@ +@import UniformTypeIdentifiers; +#import "LCSharedUtils.h" +#import "UIKitPrivate.h" +#import "utils.h" + +__attribute__((constructor)) +static void NSFMGuestHooksInit() { + swizzle(UIDocumentPickerViewController.class, @selector(initForOpeningContentTypes:asCopy:), @selector(hook_initForOpeningContentTypes:asCopy:)); + swizzle(UIDocumentBrowserViewController.class, @selector(initForOpeningContentTypes:), @selector(hook_initForOpeningContentTypes)); +} + + +@implementation UIDocumentPickerViewController(LiveContainerHook) + +- (instancetype)hook_initForOpeningContentTypes:(NSArray *)contentTypes asCopy:(BOOL)asCopy { + NSArray * contentTypesNew = @[UTTypeItem, UTTypeFolder]; + return [self hook_initForOpeningContentTypes:contentTypesNew asCopy:YES]; +} + +@end + + +@implementation UIDocumentBrowserViewController(LiveContainerHook) + +- (instancetype)hook_initForOpeningContentTypes:(NSArray *)contentTypes { + NSArray * contentTypesNew = @[UTTypeItem, UTTypeFolder]; + return [self hook_initForOpeningContentTypes:contentTypesNew]; +} + +@end diff --git a/TweakLoader/Makefile b/TweakLoader/Makefile index 1105f0f..0f0aa66 100644 --- a/TweakLoader/Makefile +++ b/TweakLoader/Makefile @@ -6,7 +6,7 @@ include $(THEOS)/makefiles/common.mk LIBRARY_NAME = TweakLoader -TweakLoader_FILES = TweakLoader.m NSBundle+FixCydiaSubstrate.m NSFileManager+GuestHooks.m UIKit+GuestHooks.m utils.m +TweakLoader_FILES = TweakLoader.m NSBundle+FixCydiaSubstrate.m NSFileManager+GuestHooks.m UIKit+GuestHooks.m utils.m DocumentPicker.m TweakLoader_CFLAGS = -objc-arc TweakLoader_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks diff --git a/TweakLoader/NSFileManager+GuestHooks.m b/TweakLoader/NSFileManager+GuestHooks.m index 617daa8..8fecacb 100644 --- a/TweakLoader/NSFileManager+GuestHooks.m +++ b/TweakLoader/NSFileManager+GuestHooks.m @@ -12,11 +12,10 @@ @implementation NSFileManager(LiveContainerHooks) - (nullable NSURL *)hook_containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier { if([groupIdentifier isEqualToString:[NSClassFromString(@"LCSharedUtils") appGroupID]]) { - return [self hook_containerURLForSecurityApplicationGroupIdentifier: groupIdentifier]; + return [NSURL fileURLWithPath: NSUserDefaults.lcAppGroupPath]; } - NSURL *appGroupPath = [self hook_containerURLForSecurityApplicationGroupIdentifier:[NSClassFromString(@"LCSharedUtils") appGroupID]]; - NSURL *result = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/LiveContainer/Data/AppGroup/%@", appGroupPath.path, groupIdentifier]]; + NSURL *result = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%@/LiveContainer/Data/AppGroup/%@", NSUserDefaults.lcAppGroupPath, groupIdentifier]]; [NSFileManager.defaultManager createDirectoryAtURL:result withIntermediateDirectories:YES attributes:nil error:nil]; return result; } diff --git a/TweakLoader/utils.h b/TweakLoader/utils.h index 3ea0f74..a193b99 100644 --- a/TweakLoader/utils.h +++ b/TweakLoader/utils.h @@ -8,4 +8,5 @@ void swizzle(Class class, SEL originalAction, SEL swizzledAction); + (instancetype)lcSharedDefaults; + (instancetype)lcUserDefaults; + (NSString *)lcAppUrlScheme; ++ (NSString *)lcAppGroupPath; @end diff --git a/main.m b/main.m index d60e1ac..e8105fb 100644 --- a/main.m +++ b/main.m @@ -19,6 +19,7 @@ static const char *dyldImageName; NSUserDefaults *lcUserDefaults; NSUserDefaults *lcSharedDefaults; +NSString *lcAppGroupPath; NSString* lcAppUrlScheme; @implementation NSUserDefaults(LiveContainer) @@ -28,6 +29,9 @@ + (instancetype)lcUserDefaults { + (instancetype)lcSharedDefaults { return lcSharedDefaults; } ++ (NSString *)lcAppGroupPath { + return lcAppGroupPath; +} + (NSString *)lcAppUrlScheme { return lcAppUrlScheme; } @@ -373,7 +377,7 @@ int LiveContainerMain(int argc, char *argv[]) { lcUserDefaults = NSUserDefaults.standardUserDefaults; lcSharedDefaults = [[NSUserDefaults alloc] initWithSuiteName: [LCSharedUtils appGroupID]]; lcAppUrlScheme = NSBundle.mainBundle.infoDictionary[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0]; - + lcAppGroupPath = [[NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:[NSClassFromString(@"LCSharedUtils") appGroupID]] path]; // move preferences first then the entire folder From 41cab666520e4559de692e6a123475668cd54bcf Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Mon, 16 Sep 2024 18:31:11 +0800 Subject: [PATCH 31/36] Users can select whether to fix file picker or not --- LiveContainerSwiftUI/LCAppBanner.swift | 40 ++++++++++++++++++------ LiveContainerSwiftUI/LCAppListView.swift | 4 +-- LiveContainerSwiftUI/LCTabView.swift | 21 ++++++++++--- LiveContainerSwiftUI/LCWebView.swift | 4 +-- LiveContainerUI/LCAppInfo.h | 8 ++--- LiveContainerUI/LCAppInfo.m | 13 ++++++++ TweakLoader/DocumentPicker.m | 6 +++- main.m | 31 ++++++++++++++++++ 8 files changed, 104 insertions(+), 23 deletions(-) diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 998a008..274e3b4 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -22,6 +22,7 @@ struct LCAppBanner : View { @State var uiIsShared : Bool @State var uiIsJITNeeded : Bool @State private var uiIsHidden : Bool + @State private var uiDoSymlinkInbox : Bool @Binding var appDataFolders: [String] @Binding var tweakFolders: [String] @@ -79,8 +80,9 @@ struct LCAppBanner : View { _uiPickerTweakFolder = _uiTweakFolder _uiIsShared = State(initialValue: appInfo.isShared) - _uiIsJITNeeded = State(initialValue: appInfo.isJITNeeded()) - _uiIsHidden = State(initialValue: appInfo.isHidden()) + _uiIsJITNeeded = State(initialValue: appInfo.isJITNeeded) + _uiIsHidden = State(initialValue: appInfo.isHidden) + _uiDoSymlinkInbox = State(initialValue: appInfo.doSymlinkInbox) } var body: some View { @@ -226,6 +228,16 @@ struct LCAppBanner : View { } label: { Label("Force Sign", systemImage: "signature") } + Button { + Task{ await toggleSimlinkInbox() } + } label: { + if uiDoSymlinkInbox { + Label("Don't Fix File Picker", systemImage: "tray.fill") + } else { + Label("Fix File Picker", systemImage: "tray") + } + + } if !uiIsShared { @@ -440,7 +452,7 @@ struct LCAppBanner : View { } UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") - if appInfo.isJITNeeded() { + if appInfo.isJITNeeded { await self.jitLaunch() } else { LCUtils.launchToGuestApp() @@ -680,11 +692,11 @@ struct LCAppBanner : View { } func toggleJITNeeded() async { - if appInfo.isJITNeeded() { - appInfo.setIsJITNeeded(false) + if appInfo.isJITNeeded { + appInfo.isJITNeeded = false uiIsJITNeeded = false } else { - appInfo.setIsJITNeeded(true) + appInfo.isJITNeeded = true uiIsJITNeeded = true } } @@ -719,11 +731,11 @@ struct LCAppBanner : View { } func toggleHidden() async { - if appInfo.isHidden() { - appInfo.setIsHidden(false) + if appInfo.isHidden { + appInfo.isHidden = false uiIsHidden = false } else { - appInfo.setIsHidden(true) + appInfo.isHidden = true uiIsHidden = true } delegate.changeAppVisibility(app: appInfo) @@ -735,4 +747,14 @@ struct LCAppBanner : View { self.saveIconExporterShow = true } + func toggleSimlinkInbox() async { + if appInfo.doSymlinkInbox { + appInfo.doSymlinkInbox = false + uiDoSymlinkInbox = false + } else { + appInfo.doSymlinkInbox = true + uiDoSymlinkInbox = true + } + } + } diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 9dbd3b6..6cd4d1a 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -277,7 +277,7 @@ struct LCAppListView : View, LCAppBannerDelegate { return } - if appToLaunch.isHidden() && !sharedModel.isHiddenAppUnlocked { + if appToLaunch.isHidden && !sharedModel.isHiddenAppUnlocked { do { if !(try await LCUtils.authenticateUser()) { return @@ -448,7 +448,7 @@ struct LCAppListView : View, LCAppBannerDelegate { func changeAppVisibility(app: LCAppInfo) { DispatchQueue.main.async { - if app.isHidden() { + if app.isHidden { self.apps.removeAll { now in return app == now } diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index 648a975..b2e2f81 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -36,7 +36,7 @@ struct LCTabView: View { let newApp = LCAppInfo(bundlePath: "\(LCPath.bundlePath.path)/\(appDir)")! newApp.relativeBundlePath = appDir newApp.isShared = false - if newApp.isHidden() { + if newApp.isHidden { tempHiddenApps.append(newApp) } else { tempApps.append(newApp) @@ -52,7 +52,7 @@ struct LCTabView: View { let newApp = LCAppInfo(bundlePath: "\(LCPath.lcGroupBundlePath.path)/\(appDir)")! newApp.relativeBundlePath = appDir newApp.isShared = true - if newApp.isHidden() { + if newApp.isHidden { tempHiddenApps.append(newApp) } else { tempApps.append(newApp) @@ -106,9 +106,16 @@ struct LCTabView: View { Label("Settings", systemImage: "gearshape.fill") } } - .alert(isPresented: $errorShow){ - Alert(title: Text("Error"), message: Text(errorInfo)) - }.onAppear() { + .alert("Error", isPresented: $errorShow){ + Button("OK", action: { + }) + Button("Copy", action: { + copyError() + }) + } message: { + Text(errorInfo) + } + .onAppear() { checkLastLaunchError() } .environmentObject(DataManager.shared.model) @@ -122,4 +129,8 @@ struct LCTabView: View { errorInfo = errorStr errorShow = true } + + func copyError() { + UIPasteboard.general.string = errorInfo + } } diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index 80fb758..41f2640 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -174,7 +174,7 @@ struct LCWebView: View { return } - if appToLaunch.isHidden() && !sharedModel.isHiddenAppUnlocked { + if appToLaunch.isHidden && !sharedModel.isHiddenAppUnlocked { do { if !(try await LCUtils.authenticateUser()) { @@ -224,7 +224,7 @@ struct LCWebView: View { return } - if appToLaunch.isHidden() && !sharedModel.isHiddenAppUnlocked { + if appToLaunch.isHidden && !sharedModel.isHiddenAppUnlocked { do { if !(try await LCUtils.authenticateUser()) { return diff --git a/LiveContainerUI/LCAppInfo.h b/LiveContainerUI/LCAppInfo.h index 4a36374..30a3cdf 100644 --- a/LiveContainerUI/LCAppInfo.h +++ b/LiveContainerUI/LCAppInfo.h @@ -7,8 +7,10 @@ } @property NSString* relativeBundlePath; @property bool isShared; -- (bool)isJITNeeded; -- (void)setIsJITNeeded:(bool)isJITNeeded; +@property bool isJITNeeded; +@property bool isHidden; +@property bool doSymlinkInbox; + - (void)setBundlePath:(NSString*)newBundlePath; - (NSMutableDictionary*)info; - (UIImage*)icon; @@ -26,6 +28,4 @@ - (NSDictionary *)generateWebClipConfig; - (void)save; - (void)patchExecAndSignIfNeedWithCompletionHandler:(void(^)(NSString* errorInfo))completetionHandler progressHandler:(void(^)(NSProgress* errorInfo))progressHandler forceSign:(BOOL)forceSign; -- (bool)isHidden; -- (void)setIsHidden:(bool)isHidden; @end diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index 22dfc10..2919140 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -300,5 +300,18 @@ - (void)setIsHidden:(bool)isHidden { _info[@"isHidden"] = [NSNumber numberWithBool:isHidden]; [self save]; +} + +- (bool)doSymlinkInbox { + if(_info[@"doSymlinkInbox"] != nil) { + return [_info[@"doSymlinkInbox"] boolValue]; + } else { + return NO; + } +} +- (void)setDoSymlinkInbox:(bool)doSymlinkInbox { + _info[@"doSymlinkInbox"] = [NSNumber numberWithBool:doSymlinkInbox]; + [self save]; + } @end diff --git a/TweakLoader/DocumentPicker.m b/TweakLoader/DocumentPicker.m index f59369a..e4cd53e 100644 --- a/TweakLoader/DocumentPicker.m +++ b/TweakLoader/DocumentPicker.m @@ -14,7 +14,11 @@ @implementation UIDocumentPickerViewController(LiveContainerHook) - (instancetype)hook_initForOpeningContentTypes:(NSArray *)contentTypes asCopy:(BOOL)asCopy { NSArray * contentTypesNew = @[UTTypeItem, UTTypeFolder]; - return [self hook_initForOpeningContentTypes:contentTypesNew asCopy:YES]; + if([NSBundle.mainBundle.infoDictionary[@"doSymlinkInbox"] boolValue]) { + return [self hook_initForOpeningContentTypes:contentTypesNew asCopy:YES]; + } else { + return [self hook_initForOpeningContentTypes:contentTypesNew asCopy:asCopy]; + } } @end diff --git a/main.m b/main.m index e8105fb..40fb96f 100644 --- a/main.m +++ b/main.m @@ -291,6 +291,37 @@ static void overwriteExecPath(NSString *bundlePath) { NSString *newTmpPath = [newHomePath stringByAppendingPathComponent:@"tmp"]; remove(newTmpPath.UTF8String); symlink(getenv("TMPDIR"), newTmpPath.UTF8String); + + if([appBundle.infoDictionary[@"doSymlinkInbox"] boolValue]) { + NSString* inboxSymlinkPath = [NSString stringWithFormat:@"%s/%@-Inbox", getenv("TMPDIR"), [appBundle bundleIdentifier]]; + NSString* inboxPath = [newHomePath stringByAppendingPathComponent:@"Inbox"]; + + if (![fm fileExistsAtPath:inboxPath]) { + [fm createDirectoryAtPath:inboxPath withIntermediateDirectories:YES attributes:nil error:&error]; + } + if([fm fileExistsAtPath:inboxSymlinkPath]) { + NSString* fileType = [fm attributesOfItemAtPath:inboxSymlinkPath error:&error][NSFileType]; + if(fileType == NSFileTypeDirectory) { + NSArray* contents = [fm contentsOfDirectoryAtPath:inboxSymlinkPath error:&error]; + for(NSString* content in contents) { + [fm moveItemAtPath:[inboxSymlinkPath stringByAppendingPathComponent:content] toPath:[inboxPath stringByAppendingPathComponent:content] error:&error]; + } + [fm removeItemAtPath:inboxSymlinkPath error:&error]; + } + } + + + symlink(inboxPath.UTF8String, inboxSymlinkPath.UTF8String); + } else { + NSString* inboxSymlinkPath = [NSString stringWithFormat:@"%s/%@-Inbox", getenv("TMPDIR"), [appBundle bundleIdentifier]]; + NSDictionary* targetAttribute = [fm attributesOfItemAtPath:inboxSymlinkPath error:&error]; + if(targetAttribute) { + if(targetAttribute[NSFileType] == NSFileTypeSymbolicLink) { + [fm removeItemAtPath:inboxSymlinkPath error:&error]; + } + } + + } setenv("CFFIXED_USER_HOME", newHomePath.UTF8String, 1); setenv("HOME", newHomePath.UTF8String, 1); From 9ec2ba6de7a4a2b603810ffd37e46efa2aed8cef Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 17 Sep 2024 00:38:56 +0800 Subject: [PATCH 32/36] app settings view, fix #155 --- LiveContainerSwiftUI/LCAppBanner.swift | 445 +++------------- LiveContainerSwiftUI/LCAppListView.swift | 21 + LiveContainerSwiftUI/LCAppSettingsView.swift | 476 ++++++++++++++++++ .../project.pbxproj | 4 + LiveContainerUI/LCAppInfo.h | 1 + LiveContainerUI/LCAppInfo.m | 17 + TweakLoader/FBSSerialQueue.m | 28 ++ TweakLoader/Makefile | 2 +- 8 files changed, 616 insertions(+), 378 deletions(-) create mode 100644 LiveContainerSwiftUI/LCAppSettingsView.swift create mode 100644 TweakLoader/FBSSerialQueue.m diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 274e3b4..3eecab6 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -13,24 +13,19 @@ protocol LCAppBannerDelegate { func removeApp(app: LCAppInfo) func changeAppVisibility(app: LCAppInfo) func installMdm(data: Data) + func openNavigationView(view: AnyView) + func closeNavigationView() } -struct LCAppBanner : View { +struct LCAppBanner : View, LCAppSettingDelegate { @State var appInfo: LCAppInfo var delegate: LCAppBannerDelegate - @State var uiIsShared : Bool - @State var uiIsJITNeeded : Bool - @State private var uiIsHidden : Bool - @State private var uiDoSymlinkInbox : Bool + @StateObject var model : LCAppModel @Binding var appDataFolders: [String] @Binding var tweakFolders: [String] - @State private var uiDataFolder : String? - @State private var uiTweakFolder : String? - @State private var uiPickerDataFolder : String? - @State private var uiPickerTweakFolder : String? @State private var confirmAppRemovalShow = false @State private var confirmAppFolderRemovalShow = false @@ -40,18 +35,6 @@ struct LCAppBanner : View { @State private var appRemovalContinuation : CheckedContinuation? = nil @State private var appFolderRemovalContinuation : CheckedContinuation? = nil - @State private var renameFolderShow = false - @State private var renameFolderContent = "" - @State private var renameFolerContinuation : CheckedContinuation? = nil - - @State private var confirmMoveToAppGroupShow = false - @State private var confirmMoveToAppGroup = false - @State private var confirmMoveToAppGroupContinuation : CheckedContinuation? = nil - - @State private var confirmMoveToPrivateDocShow = false - @State private var confirmMoveToPrivateDoc = false - @State private var confirmMoveToPrivateDocContinuation : CheckedContinuation? = nil - @State private var enablingJITShow = false @State private var confirmEnablingJIT = false @State private var confirmEnablingJITContinuation : CheckedContinuation? = nil @@ -64,7 +47,6 @@ struct LCAppBanner : View { @State private var isSingingInProgress = false @State private var signProgress = 0.0 - @State private var isAppRunning = false @State private var observer : NSKeyValueObservation? @EnvironmentObject private var sharedModel : SharedModel @@ -74,15 +56,8 @@ struct LCAppBanner : View { _appDataFolders = appDataFolders _tweakFolders = tweakFolders self.delegate = delegate - _uiDataFolder = State(initialValue: appInfo.getDataUUIDNoAssign()) - _uiTweakFolder = State(initialValue: appInfo.tweakFolder()) - _uiPickerDataFolder = _uiDataFolder - _uiPickerTweakFolder = _uiTweakFolder - _uiIsShared = State(initialValue: appInfo.isShared) - _uiIsJITNeeded = State(initialValue: appInfo.isJITNeeded) - _uiIsHidden = State(initialValue: appInfo.isHidden) - _uiDoSymlinkInbox = State(initialValue: appInfo.doSymlinkInbox) + _model = StateObject(wrappedValue: LCAppModel(appInfo: appInfo)) } var body: some View { @@ -97,14 +72,14 @@ struct LCAppBanner : View { VStack (alignment: .leading, content: { HStack { Text(appInfo.displayName()).font(.system(size: 16)).bold() - if uiIsShared { + if model.uiIsShared { Text("SHARED").font(.system(size: 8)).bold().padding(2) .frame(width: 50, height:16) .background( Capsule().fill(Color("BadgeColor")) ) } - if uiIsJITNeeded { + if model.uiIsJITNeeded { Text("JIT").font(.system(size: 8)).bold().padding(2) .frame(width: 30, height:16) .background( @@ -114,7 +89,7 @@ struct LCAppBanner : View { } Text("\(appInfo.version()) - \(appInfo.bundleIdentifier())").font(.system(size: 12)).foregroundColor(Color("FontColor")) - Text(uiDataFolder == nil ? "Data folder not created yet" : uiDataFolder!).font(.system(size: 8)).foregroundColor(Color("FontColor")) + Text(model.uiDataFolder == nil ? "Data folder not created yet" : model.uiDataFolder!).font(.system(size: 8)).foregroundColor(Color("FontColor")) }) } Spacer() @@ -148,7 +123,7 @@ struct LCAppBanner : View { }) .clipShape(Capsule()) - .disabled(isAppRunning) + .disabled(model.isAppRunning) } .padding() @@ -164,150 +139,67 @@ struct LCAppBanner : View { }) .contextMenu{ - Text(appInfo.relativeBundlePath) - - if !uiIsShared { - if uiDataFolder != nil { - Button { - openDataFolder() - } label: { - Label("Open Data Folder", systemImage: "folder") - } - } - } - - Menu { - Button { - openSafariViewToCreateAppClip() - } label: { - Label("Create App Clip", systemImage: "appclip") - } - Button { - copyLaunchUrl() - } label: { - Label("Copy Launch Url", systemImage: "link") - } - Button { - saveIcon() - } label: { - Label("Save App Icon", systemImage: "square.and.arrow.down") - } - - - } label: { - Label("Add to Home Screen", systemImage: "plus.app") - } - - Button { - Task { await toggleJITNeeded()} - } label: { - if uiIsJITNeeded { - Label("Don't Need JIT", systemImage: "bolt.slash") + Section(appInfo.relativeBundlePath) { + if #available(iOS 16.0, *){ + } else { - Label("Mark as JIT Needed", systemImage: "bolt") + Text(appInfo.relativeBundlePath) } - - } - - - if sharedModel.isHiddenAppUnlocked { - Button { - Task { await toggleHidden()} - } label: { - if uiIsHidden { - Label("Unhide App", systemImage: "eye") - } else { - Label("Hide App", systemImage: "eye.slash") + if !model.uiIsShared { + if model.uiDataFolder != nil { + Button { + openDataFolder() + } label: { + Label("Open Data Folder", systemImage: "folder") + } } - - } - } - - Button { - Task{ await forceResign() } - } label: { - Label("Force Sign", systemImage: "signature") - } - Button { - Task{ await toggleSimlinkInbox() } - } label: { - if uiDoSymlinkInbox { - Label("Don't Fix File Picker", systemImage: "tray.fill") - } else { - Label("Fix File Picker", systemImage: "tray") - } - - } - - - if !uiIsShared { - Button { - Task { await moveToAppGroup()} - } label: { - Label("Convert to Shared App", systemImage: "arrowshape.turn.up.left") } - Menu(content: { - Picker(selection: $uiPickerTweakFolder , label: Text("")) { - Label("None", systemImage: "nosign").tag(Optional(nil)) - ForEach(tweakFolders, id:\.self) { folderName in - Text(folderName).tag(Optional(folderName)) - } + Menu { + Button { + openSafariViewToCreateAppClip() + } label: { + Label("Create App Clip", systemImage: "appclip") } - }, label: { - Label("Change Tweak Folder", systemImage: "gear") - }) - - Menu(content: { Button { - Task{ await createFolder() } + copyLaunchUrl() } label: { - Label("New data folder", systemImage: "plus") + Label("Copy Launch Url", systemImage: "link") } - if uiDataFolder != nil { - Button { - Task{ await renameDataFolder() } - } label: { - Label("Rename data folder", systemImage: "pencil") - } + Button { + saveIcon() + } label: { + Label("Save App Icon", systemImage: "square.and.arrow.down") } - Picker(selection: $uiPickerDataFolder , label: Text("")) { - ForEach(appDataFolders, id:\.self) { folderName in - Button(folderName) { - setDataFolder(folderName: folderName) - }.tag(Optional(folderName)) - } - } - }, label: { - Label("Change Data Folder", systemImage: "folder.badge.questionmark") - }) - - Button(role: .destructive) { - Task{ await uninstall() } + } label: { - Label("Uninstall", systemImage: "trash") + Label("Add to Home Screen", systemImage: "plus.app") } - } else if LCUtils.multiLCStatus != 2 { Button { - Task { await movePrivateDoc() } + openSettings() } label: { - Label("Convert to Private App", systemImage: "arrowshape.turn.up.left") + Label("Settings", systemImage: "gear") } + + + if !model.uiIsShared { + Button(role: .destructive) { + Task{ await uninstall() } + } label: { + Label("Uninstall", systemImage: "trash") + } + + } + } + + + } - .onChange(of: uiPickerDataFolder, perform: { newValue in - if newValue != uiDataFolder { - setDataFolder(folderName: newValue) - } - }) - .onChange(of: uiPickerTweakFolder, perform: { newValue in - if newValue != uiTweakFolder { - setTweakFolder(folderName: newValue) - } - }) + .onChange(of: sharedModel.bundleIdToLaunch, perform: { newValue in Task { await handleURLSchemeLaunch() } }) @@ -344,48 +236,6 @@ struct LCAppBanner : View { } message: { Text("Do you also want to delete data folder of \(appInfo.displayName()!)? You can keep it for future use.") } - .alert("Move to App Group", isPresented: $confirmMoveToAppGroupShow) { - Button { - self.confirmMoveToAppGroup = true - self.confirmMoveToAppGroupContinuation?.resume() - } label: { - Text("Move") - } - Button("Cancel", role: .cancel) { - self.confirmMoveToAppGroup = false - self.confirmMoveToAppGroupContinuation?.resume() - } - } message: { - Text("Moving this app to App Group allows other LiveContainers to run this app with all its data and tweak preserved. If you want to access its data and tweak again from the file app, move it back.") - } - .alert("Move to Private Document Folder", isPresented: $confirmMoveToPrivateDocShow) { - Button { - self.confirmMoveToPrivateDoc = true - self.confirmMoveToPrivateDocContinuation?.resume() - } label: { - Text("Move") - } - Button("Cancel", role: .cancel) { - self.confirmMoveToPrivateDoc = false - self.confirmMoveToPrivateDocContinuation?.resume() - } - } message: { - Text("Moving this app to Private Document Folder allows you to access its data and tweaks in the Files app, but it can not be run by other LiveContainers.") - } - .textFieldAlert( - isPresented: $renameFolderShow, - title: "Enter the name of new folder", - text: $renameFolderContent, - placeholder: "", - action: { newText in - self.renameFolderContent = newText! - renameFolerContinuation?.resume() - }, - actionCancel: {_ in - self.renameFolderContent = "" - renameFolerContinuation?.resume() - } - ) .alert("Waiting for JIT", isPresented: $enablingJITShow) { Button { self.confirmEnablingJIT = true @@ -424,7 +274,7 @@ struct LCAppBanner : View { return } } - isAppRunning = true + model.isAppRunning = true var signError : String? = nil await withCheckedContinuation({ c in @@ -447,7 +297,7 @@ struct LCAppBanner : View { if let signError { errorInfo = signError errorShow = true - self.isAppRunning = false + model.isAppRunning = false return } @@ -458,12 +308,20 @@ struct LCAppBanner : View { LCUtils.launchToGuestApp() } - self.isAppRunning = false + model.isAppRunning = false } + func openSettings() { + delegate.openNavigationView(view: AnyView(LCAppSettingsView(model: model, appDataFolders: $appDataFolders, tweakFolders: $tweakFolders, delegate: self))) + } + func forceResign() async { - self.isAppRunning = true + if model.isAppRunning { + return + } + + model.isAppRunning = true var signError : String? = nil await withCheckedContinuation({ c in appInfo.patchExecAndSignIfNeed(completionHandler: { error in @@ -485,89 +343,20 @@ struct LCAppBanner : View { if let signError { errorInfo = signError errorShow = true - self.isAppRunning = false + model.isAppRunning = false return } - self.isAppRunning = false + model.isAppRunning = false } - func setDataFolder(folderName: String?) { - self.appInfo.setDataUUID(folderName!) - self.uiDataFolder = folderName - self.uiPickerDataFolder = folderName - } - func createFolder() async { - - self.renameFolderContent = NSUUID().uuidString - - await withCheckedContinuation { c in - self.renameFolerContinuation = c - self.renameFolderShow = true - } - - if self.renameFolderContent == "" { - return - } - let fm = FileManager() - let dest = LCPath.dataPath.appendingPathComponent(self.renameFolderContent) - do { - try fm.createDirectory(at: dest, withIntermediateDirectories: false) - } catch { - errorShow = true - errorInfo = error.localizedDescription - return - } - - self.appDataFolders.append(self.renameFolderContent) - self.setDataFolder(folderName: self.renameFolderContent) - - } - - func renameDataFolder() async { - if self.appInfo.getDataUUIDNoAssign() == nil { - return - } - - self.renameFolderContent = self.uiDataFolder == nil ? "" : self.uiDataFolder! - await withCheckedContinuation { c in - self.renameFolerContinuation = c - self.renameFolderShow = true - } - if self.renameFolderContent == "" { - return - } - let fm = FileManager() - let orig = LCPath.dataPath.appendingPathComponent(appInfo.getDataUUIDNoAssign()) - let dest = LCPath.dataPath.appendingPathComponent(self.renameFolderContent) - do { - try fm.moveItem(at: orig, to: dest) - } catch { - errorShow = true - errorInfo = error.localizedDescription - return - } - - let i = self.appDataFolders.firstIndex(of: self.appInfo.getDataUUIDNoAssign()) - guard let i = i else { - return - } - - self.appDataFolders[i] = self.renameFolderContent - self.setDataFolder(folderName: self.renameFolderContent) - - } func openDataFolder() { let url = URL(string:"shareddocuments://\(LCPath.docPath.path)/Data/Application/\(appInfo.dataUUID()!)") UIApplication.shared.open(url!) } - func setTweakFolder(folderName: String?) { - self.appInfo.setTweakFolder(folderName) - self.uiTweakFolder = folderName - self.uiPickerTweakFolder = folderName - } + func uninstall() async { do { @@ -611,96 +400,6 @@ struct LCAppBanner : View { } } - func moveToAppGroup() async { - await withCheckedContinuation { c in - confirmMoveToAppGroupContinuation = c - confirmMoveToAppGroupShow = true - } - if !confirmMoveToAppGroup { - return - } - - do { - try LCPath.ensureAppGroupPaths() - let fm = FileManager() - try fm.moveItem(atPath: appInfo.bundlePath(), toPath: LCPath.lcGroupBundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) - if let dataFolder = appInfo.getDataUUIDNoAssign(), dataFolder.count > 0 { - try fm.moveItem(at: LCPath.dataPath.appendingPathComponent(dataFolder), - to: LCPath.lcGroupDataPath.appendingPathComponent(dataFolder)) - appDataFolders.removeAll(where: { s in - return s == dataFolder - }) - } - if let tweakFolder = appInfo.tweakFolder(), tweakFolder.count > 0 { - try fm.moveItem(at: LCPath.tweakPath.appendingPathComponent(tweakFolder), - to: LCPath.lcGroupTweakPath.appendingPathComponent(tweakFolder)) - tweakFolders.removeAll(where: { s in - return s == tweakFolder - }) - } - appInfo.setBundlePath(LCPath.lcGroupBundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) - appInfo.isShared = true - uiIsShared = true - } catch { - errorInfo = error.localizedDescription - errorShow = true - } - - } - - func movePrivateDoc() async { - let runningLC = LCUtils.getAppRunningLCScheme(bundleId: appInfo.relativeBundlePath!) - if runningLC != nil { - errorInfo = "Data of this app is currently in \(runningLC!). Open \(runningLC!) and launch it to 'My Apps' screen and try again." - errorShow = true - return - } - - await withCheckedContinuation { c in - confirmMoveToPrivateDocContinuation = c - confirmMoveToPrivateDocShow = true - } - if !confirmMoveToPrivateDoc { - return - } - - do { - let fm = FileManager() - try fm.moveItem(atPath: appInfo.bundlePath(), toPath: LCPath.bundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) - if let dataFolder = appInfo.getDataUUIDNoAssign(), dataFolder.count > 0 { - try fm.moveItem(at: LCPath.lcGroupDataPath.appendingPathComponent(dataFolder), - to: LCPath.dataPath.appendingPathComponent(dataFolder)) - appDataFolders.append(dataFolder) - uiDataFolder = dataFolder - uiPickerDataFolder = dataFolder - } - if let tweakFolder = appInfo.tweakFolder(), tweakFolder.count > 0 { - try fm.moveItem(at: LCPath.lcGroupTweakPath.appendingPathComponent(tweakFolder), - to: LCPath.tweakPath.appendingPathComponent(tweakFolder)) - tweakFolders.append(tweakFolder) - uiTweakFolder = tweakFolder - uiPickerTweakFolder = tweakFolder - } - appInfo.setBundlePath(LCPath.bundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) - appInfo.isShared = false - uiIsShared = false - } catch { - errorShow = true - errorInfo = error.localizedDescription - } - - } - - func toggleJITNeeded() async { - if appInfo.isJITNeeded { - appInfo.isJITNeeded = false - uiIsJITNeeded = false - } else { - appInfo.isJITNeeded = true - uiIsJITNeeded = true - } - } - func jitLaunch() async { LCUtils.askForJIT() @@ -731,12 +430,13 @@ struct LCAppBanner : View { } func toggleHidden() async { + delegate.closeNavigationView() if appInfo.isHidden { appInfo.isHidden = false - uiIsHidden = false + model.uiIsHidden = false } else { appInfo.isHidden = true - uiIsHidden = true + model.uiIsHidden = true } delegate.changeAppVisibility(app: appInfo) } @@ -747,14 +447,5 @@ struct LCAppBanner : View { self.saveIconExporterShow = true } - func toggleSimlinkInbox() async { - if appInfo.doSymlinkInbox { - appInfo.doSymlinkInbox = false - uiDoSymlinkInbox = false - } else { - appInfo.doSymlinkInbox = true - uiDoSymlinkInbox = true - } - } } diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 6cd4d1a..1956f36 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -46,6 +46,9 @@ struct LCAppListView : View, LCAppBannerDelegate { @State var safariViewOpened = false @State var safariViewURL = URL(string: "https://google.com")! + @State private var navigateTo : AnyView? + @State private var isNavigationActive = false + @EnvironmentObject private var sharedModel : SharedModel init(apps: Binding<[LCAppInfo]>, hiddenApps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>, tweakFolderNames: Binding<[String]>) { @@ -60,6 +63,14 @@ struct LCAppListView : View, LCAppBannerDelegate { var body: some View { NavigationView { ScrollView { + + NavigationLink( + destination: navigateTo, + isActive: $isNavigationActive, + label: { + EmptyView() + }) + GeometryReader { g in ProgressView(value: uiInstallProgressPercentage) .labelsHidden() @@ -523,4 +534,14 @@ struct LCAppListView : View, LCAppBannerDelegate { safariViewURL = URL(string:"data:application/x-apple-aspen-config;base64,\(data.base64EncodedString())")! safariViewOpened = true } + + func openNavigationView(view: AnyView) { + navigateTo = view + isNavigationActive = true + } + + func closeNavigationView() { + isNavigationActive = false + navigateTo = nil + } } diff --git a/LiveContainerSwiftUI/LCAppSettingsView.swift b/LiveContainerSwiftUI/LCAppSettingsView.swift new file mode 100644 index 0000000..033dd22 --- /dev/null +++ b/LiveContainerSwiftUI/LCAppSettingsView.swift @@ -0,0 +1,476 @@ +// +// LCAppSettingsView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/9/16. +// + +import Foundation +import SwiftUI + +protocol LCAppSettingDelegate { + func forceResign() async + func toggleHidden() async +} + +class LCAppModel: ObservableObject { + @Published var appInfo : LCAppInfo + + @Published var isAppRunning = false + + @Published var uiIsJITNeeded : Bool + @Published var uiIsHidden : Bool + @Published var uiIsShared : Bool + @Published var uiDataFolder : String? + @Published var uiTweakFolder : String? + @Published var uiDoSymlinkInbox : Bool + @Published var uiBypassAssertBarrierOnQueue : Bool + + init(appInfo : LCAppInfo) { + self.appInfo = appInfo + + self.uiIsJITNeeded = appInfo.isJITNeeded + self.uiIsHidden = appInfo.isHidden + self.uiIsShared = appInfo.isShared + self.uiDataFolder = appInfo.getDataUUIDNoAssign() + self.uiTweakFolder = appInfo.tweakFolder() + self.uiDoSymlinkInbox = appInfo.doSymlinkInbox + self.uiBypassAssertBarrierOnQueue = appInfo.bypassAssertBarrierOnQueue + } +} + + +struct LCAppSettingsView : View{ + + private var appInfo : LCAppInfo + + @ObservedObject private var model : LCAppModel + + @Binding var appDataFolders: [String] + @Binding var tweakFolders: [String] + + + @State private var uiPickerDataFolder : String? + @State private var uiPickerTweakFolder : String? + + @State private var renameFolderShow = false + @State private var renameFolderContent = "" + @State private var renameFolerContinuation : CheckedContinuation? = nil + + @State private var confirmMoveToAppGroupShow = false + @State private var confirmMoveToAppGroup = false + @State private var confirmMoveToAppGroupContinuation : CheckedContinuation? = nil + + @State private var confirmMoveToPrivateDocShow = false + @State private var confirmMoveToPrivateDoc = false + @State private var confirmMoveToPrivateDocContinuation : CheckedContinuation? = nil + + @State private var errorShow = false + @State private var errorInfo = "" + + private let delegate : LCAppSettingDelegate + @EnvironmentObject private var sharedModel : SharedModel + + init(model: LCAppModel, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>, delegate: LCAppSettingDelegate) { + self.appInfo = model.appInfo + self._model = ObservedObject(wrappedValue: model) + _appDataFolders = appDataFolders + _tweakFolders = tweakFolders + self.delegate = delegate + self._uiPickerDataFolder = State(initialValue: model.uiDataFolder) + self._uiPickerTweakFolder = State(initialValue: model.uiTweakFolder) + } + + var body: some View { + Form { + Section { + HStack { + Text("Bundle Identifier") + Spacer() + Text(appInfo.relativeBundlePath) + .foregroundColor(.gray) + .multilineTextAlignment(.trailing) + } + if !model.uiIsShared { + Menu { + Button { + Task{ await createFolder() } + } label: { + Label("New data folder", systemImage: "plus") + } + if model.uiDataFolder != nil { + Button { + Task{ await renameDataFolder() } + } label: { + Label("Rename data folder", systemImage: "pencil") + } + } + + Picker(selection: $uiPickerDataFolder , label: Text("")) { + ForEach(appDataFolders, id:\.self) { folderName in + Button(folderName) { + setDataFolder(folderName: folderName) + }.tag(Optional(folderName)) + } + } + } label: { + HStack { + Text("Data Folder") + .foregroundColor(.primary) + Spacer() + Text(model.uiDataFolder == nil ? "Not created yet" : model.uiDataFolder!) + .multilineTextAlignment(.trailing) + } + } + .onChange(of: uiPickerDataFolder, perform: { newValue in + if newValue != model.uiDataFolder { + setDataFolder(folderName: newValue) + } + }) + + Menu { + Picker(selection: $uiPickerTweakFolder , label: Text("")) { + Label("None", systemImage: "nosign").tag(Optional(nil)) + ForEach(tweakFolders, id:\.self) { folderName in + Text(folderName).tag(Optional(folderName)) + } + } + } label: { + HStack { + Text("Tweak Folder") + .foregroundColor(.primary) + Spacer() + Text(model.uiTweakFolder == nil ? "None" : model.uiTweakFolder!) + .multilineTextAlignment(.trailing) + } + } + .onChange(of: uiPickerTweakFolder, perform: { newValue in + if newValue != model.uiTweakFolder { + setTweakFolder(folderName: newValue) + } + }) + + + } else { + HStack { + Text("Data Folder") + .foregroundColor(.primary) + Spacer() + Text(model.uiDataFolder == nil ? "Data folder not created yet" : model.uiDataFolder!) + .foregroundColor(.gray) + .multilineTextAlignment(.trailing) + } + HStack { + Text("Tweak Folder") + .foregroundColor(.primary) + Spacer() + Text(model.uiTweakFolder == nil ? "None" : model.uiTweakFolder!) + .foregroundColor(.gray) + .multilineTextAlignment(.trailing) + } + } + + if !model.uiIsShared { + Button("Convert to Shared App") { + Task { await moveToAppGroup()} + } + + } else if LCUtils.multiLCStatus != 2 { + Button("Convert to Private App") { + Task { await movePrivateDoc() } + } + } + } header: { + Text("Data") + } + + + Section { + Toggle(isOn: $model.uiIsJITNeeded) { + Text("Launch with JIT") + } + .onChange(of: model.uiIsJITNeeded, perform: { newValue in + Task { await setJITNeeded(newValue) } + }) + } footer: { + Text("LiveContainer will try to acquire JIT permission before launching the app.") + } + + if sharedModel.isHiddenAppUnlocked { + Section { + Toggle(isOn: $model.uiIsHidden) { + Text("Hide App") + } + .onChange(of: model.uiIsHidden, perform: { newValue in + Task { await toggleHidden() } + }) + } footer: { + Text("To completely hide apps, enable Strict Hiding mode in settings.") + } + + } + + Section { + Toggle(isOn: $model.uiDoSymlinkInbox) { + Text("Fix File Picker") + } + .onChange(of: model.uiDoSymlinkInbox, perform: { newValue in + Task { await setSimlinkInbox(newValue) } + }) + } header: { + Text("Fixes") + } footer: { + Text("Fix file picker not responding when hitting 'open' by forcing this app to copy selected files to its inbox. You may view imported files in the 'Inbox' folder in app's data folder.") + } + + Section { + Toggle(isOn: $model.uiBypassAssertBarrierOnQueue) { + Text("Bypass AssertBarrierOnQueue") + } + .onChange(of: model.uiBypassAssertBarrierOnQueue, perform: { newValue in + Task { await setBypassAssertBarrierOnQueue(newValue) } + }) + + } footer: { + Text("Might prevent some games from crashing, but may cause them to be unstable.") + } + + + Section { + Button("Force Sign") { + Task { await forceResign() } + } + .disabled(model.isAppRunning) + } footer: { + Text("Try to sign again if this app failed to launch with error like 'Invalid Signature'. It this still don't work, renew JIT-Less certificate.") + } + + } + .navigationTitle(appInfo.displayName()) + + .alert("Error", isPresented: $errorShow) { + Button("OK", action: { + }) + } message: { + Text(errorInfo) + } + + .textFieldAlert( + isPresented: $renameFolderShow, + title: "Enter the name of new folder", + text: $renameFolderContent, + placeholder: "", + action: { newText in + self.renameFolderContent = newText! + renameFolerContinuation?.resume() + }, + actionCancel: {_ in + self.renameFolderContent = "" + renameFolerContinuation?.resume() + } + ) + .alert("Move to App Group", isPresented: $confirmMoveToAppGroupShow) { + Button { + self.confirmMoveToAppGroup = true + self.confirmMoveToAppGroupContinuation?.resume() + } label: { + Text("Move") + } + Button("Cancel", role: .cancel) { + self.confirmMoveToAppGroup = false + self.confirmMoveToAppGroupContinuation?.resume() + } + } message: { + Text("Moving this app to App Group allows other LiveContainers to run this app with all its data and tweak preserved. If you want to access its data and tweak again from the file app, move it back.") + } + .alert("Move to Private Document Folder", isPresented: $confirmMoveToPrivateDocShow) { + Button { + self.confirmMoveToPrivateDoc = true + self.confirmMoveToPrivateDocContinuation?.resume() + } label: { + Text("Move") + } + Button("Cancel", role: .cancel) { + self.confirmMoveToPrivateDoc = false + self.confirmMoveToPrivateDocContinuation?.resume() + } + } message: { + Text("Moving this app to Private Document Folder allows you to access its data and tweaks in the Files app, but it can not be run by other LiveContainers.") + } + } + + func setDataFolder(folderName: String?) { + self.appInfo.setDataUUID(folderName!) + self.model.uiDataFolder = folderName + self.uiPickerDataFolder = folderName + } + + func createFolder() async { + + self.renameFolderContent = NSUUID().uuidString + + await withCheckedContinuation { c in + self.renameFolerContinuation = c + self.renameFolderShow = true + } + + if self.renameFolderContent == "" { + return + } + let fm = FileManager() + let dest = LCPath.dataPath.appendingPathComponent(self.renameFolderContent) + do { + try fm.createDirectory(at: dest, withIntermediateDirectories: false) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + + self.appDataFolders.append(self.renameFolderContent) + self.setDataFolder(folderName: self.renameFolderContent) + + } + + func renameDataFolder() async { + if self.appInfo.getDataUUIDNoAssign() == nil { + return + } + + self.renameFolderContent = self.model.uiDataFolder == nil ? "" : self.model.uiDataFolder! + await withCheckedContinuation { c in + self.renameFolerContinuation = c + self.renameFolderShow = true + } + if self.renameFolderContent == "" { + return + } + let fm = FileManager() + let orig = LCPath.dataPath.appendingPathComponent(appInfo.getDataUUIDNoAssign()) + let dest = LCPath.dataPath.appendingPathComponent(self.renameFolderContent) + do { + try fm.moveItem(at: orig, to: dest) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + + let i = self.appDataFolders.firstIndex(of: self.appInfo.getDataUUIDNoAssign()) + guard let i = i else { + return + } + + self.appDataFolders[i] = self.renameFolderContent + self.setDataFolder(folderName: self.renameFolderContent) + + } + + func setTweakFolder(folderName: String?) { + self.appInfo.setTweakFolder(folderName) + self.model.uiTweakFolder = folderName + self.uiPickerTweakFolder = folderName + } + + func moveToAppGroup() async { + await withCheckedContinuation { c in + confirmMoveToAppGroupContinuation = c + confirmMoveToAppGroupShow = true + } + if !confirmMoveToAppGroup { + return + } + + do { + try LCPath.ensureAppGroupPaths() + let fm = FileManager() + try fm.moveItem(atPath: appInfo.bundlePath(), toPath: LCPath.lcGroupBundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) + if let dataFolder = appInfo.getDataUUIDNoAssign(), dataFolder.count > 0 { + try fm.moveItem(at: LCPath.dataPath.appendingPathComponent(dataFolder), + to: LCPath.lcGroupDataPath.appendingPathComponent(dataFolder)) + appDataFolders.removeAll(where: { s in + return s == dataFolder + }) + } + if let tweakFolder = appInfo.tweakFolder(), tweakFolder.count > 0 { + try fm.moveItem(at: LCPath.tweakPath.appendingPathComponent(tweakFolder), + to: LCPath.lcGroupTweakPath.appendingPathComponent(tweakFolder)) + tweakFolders.removeAll(where: { s in + return s == tweakFolder + }) + } + appInfo.setBundlePath(LCPath.lcGroupBundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) + appInfo.isShared = true + model.uiIsShared = true + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + + } + + func movePrivateDoc() async { + let runningLC = LCUtils.getAppRunningLCScheme(bundleId: appInfo.relativeBundlePath!) + if runningLC != nil { + errorInfo = "Data of this app is currently in \(runningLC!). Open \(runningLC!) and launch it to 'My Apps' screen and try again." + errorShow = true + return + } + + await withCheckedContinuation { c in + confirmMoveToPrivateDocContinuation = c + confirmMoveToPrivateDocShow = true + } + if !confirmMoveToPrivateDoc { + return + } + + do { + let fm = FileManager() + try fm.moveItem(atPath: appInfo.bundlePath(), toPath: LCPath.bundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) + if let dataFolder = appInfo.getDataUUIDNoAssign(), dataFolder.count > 0 { + try fm.moveItem(at: LCPath.lcGroupDataPath.appendingPathComponent(dataFolder), + to: LCPath.dataPath.appendingPathComponent(dataFolder)) + appDataFolders.append(dataFolder) + model.uiDataFolder = dataFolder + } + if let tweakFolder = appInfo.tweakFolder(), tweakFolder.count > 0 { + try fm.moveItem(at: LCPath.lcGroupTweakPath.appendingPathComponent(tweakFolder), + to: LCPath.tweakPath.appendingPathComponent(tweakFolder)) + tweakFolders.append(tweakFolder) + model.uiTweakFolder = tweakFolder + } + appInfo.setBundlePath(LCPath.bundlePath.appendingPathComponent(appInfo.relativeBundlePath).path) + appInfo.isShared = false + model.uiIsShared = false + } catch { + errorShow = true + errorInfo = error.localizedDescription + } + + } + + func setJITNeeded(_ JITNeeded: Bool) async { + appInfo.isJITNeeded = JITNeeded + model.uiIsJITNeeded = JITNeeded + + } + + func setSimlinkInbox(_ simlinkInbox : Bool) async { + appInfo.doSymlinkInbox = simlinkInbox + model.uiDoSymlinkInbox = simlinkInbox + + } + + func setBypassAssertBarrierOnQueue(_ enabled : Bool) async { + appInfo.bypassAssertBarrierOnQueue = enabled + model.uiBypassAssertBarrierOnQueue = enabled + } + func toggleHidden() async { + await delegate.toggleHidden() + } + + func forceResign() async { + await delegate.forceResign() + } +} diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj index 12077ab..2dbc0fc 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -18,6 +18,7 @@ 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173564C82C76FE3500C6C918 /* Assets.xcassets */; }; 173F18402C7B7B74002953AA /* LCWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173F183F2C7B7B74002953AA /* LCWebView.swift */; }; 178B4C3E2C77654400DD1F74 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178B4C3D2C77654400DD1F74 /* Shared.swift */; }; + 17C536F42C98529D006C2C75 /* LCAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C536F32C98529D006C2C75 /* LCAppSettingsView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -35,6 +36,7 @@ 178B4C3D2C77654400DD1F74 /* Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LiveContainerSwiftUI-Bridging-Header.h"; sourceTree = ""; }; 17B9B88D2C760678009D079E /* LiveContainerSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveContainerSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 17C536F32C98529D006C2C75 /* LCAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCAppSettingsView.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -57,6 +59,7 @@ 173564BC2C76FE3500C6C918 /* LCAppListView.swift */, 173564C12C76FE3500C6C918 /* LCSettingsView.swift */, 173564BD2C76FE3500C6C918 /* LCTabView.swift */, + 17C536F32C98529D006C2C75 /* LCAppSettingsView.swift */, 173564C02C76FE3500C6C918 /* LCTweaksView.swift */, 173564C32C76FE3500C6C918 /* Makefile */, 173564C62C76FE3500C6C918 /* ObjcBridge.swift */, @@ -195,6 +198,7 @@ 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */, 173F18402C7B7B74002953AA /* LCWebView.swift in Sources */, 173564CE2C76FE3500C6C918 /* Makefile in Sources */, + 17C536F42C98529D006C2C75 /* LCAppSettingsView.swift in Sources */, 173564CF2C76FE3500C6C918 /* LCSwiftBridge.m in Sources */, 173564C92C76FE3500C6C918 /* LCAppListView.swift in Sources */, 173564CC2C76FE3500C6C918 /* LCTweaksView.swift in Sources */, diff --git a/LiveContainerUI/LCAppInfo.h b/LiveContainerUI/LCAppInfo.h index 30a3cdf..329f241 100644 --- a/LiveContainerUI/LCAppInfo.h +++ b/LiveContainerUI/LCAppInfo.h @@ -10,6 +10,7 @@ @property bool isJITNeeded; @property bool isHidden; @property bool doSymlinkInbox; +@property bool bypassAssertBarrierOnQueue; - (void)setBundlePath:(NSString*)newBundlePath; - (NSMutableDictionary*)info; diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index 2919140..e88b306 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -103,6 +103,10 @@ - (NSMutableDictionary*)info { - (UIImage*)icon { UIImage* icon = [UIImage imageNamed:[_info valueForKeyPath:@"CFBundleIcons.CFBundlePrimaryIcon.CFBundleIconFiles"][0] inBundle:[[NSBundle alloc] initWithPath: _bundlePath] compatibleWithTraitCollection:nil]; + if(!icon) { + icon = [UIImage imageNamed:[_info valueForKeyPath:@"CFBundleIconFiles"][0] inBundle:[[NSBundle alloc] initWithPath: _bundlePath] compatibleWithTraitCollection:nil]; + } + if(!icon) { icon = [UIImage imageNamed:@"DefaultIcon"]; } @@ -313,5 +317,18 @@ - (void)setDoSymlinkInbox:(bool)doSymlinkInbox { _info[@"doSymlinkInbox"] = [NSNumber numberWithBool:doSymlinkInbox]; [self save]; +} + +- (bool)bypassAssertBarrierOnQueue { + if(_info[@"bypassAssertBarrierOnQueue"] != nil) { + return [_info[@"bypassAssertBarrierOnQueue"] boolValue]; + } else { + return NO; + } +} +- (void)setBypassAssertBarrierOnQueue:(bool)enabled { + _info[@"bypassAssertBarrierOnQueue"] = [NSNumber numberWithBool:enabled]; + [self save]; + } @end diff --git a/TweakLoader/FBSSerialQueue.m b/TweakLoader/FBSSerialQueue.m new file mode 100644 index 0000000..e5e673a --- /dev/null +++ b/TweakLoader/FBSSerialQueue.m @@ -0,0 +1,28 @@ +#import +#import "utils.h" + +@interface FBSSerialQueue1 : NSObject +-(void)assertBarrierOnQueue1; +-(void)assertBarrierOnQueue2; +@end + +@implementation FBSSerialQueue1 +- (void)assertBarrierOnQueue1 { + +} +- (void)assertBarrierOnQueue2 { + +} +@end + +__attribute__((constructor)) +static void NSFMGuestHooksInit() { + if(![[NSBundle.mainBundle infoDictionary][@"bypassAssertBarrierOnQueue"] boolValue]) { + return; + } + + // Use empty function to replace these functions so assertion will never fail + method_exchangeImplementations(class_getInstanceMethod(NSClassFromString(@"FBSSerialQueue"), @selector(assertBarrierOnQueue)), class_getInstanceMethod(FBSSerialQueue1.class, @selector(assertBarrierOnQueue1))); + + method_exchangeImplementations(class_getInstanceMethod(NSClassFromString(@"FBSMainRunLoopSerialQueue"), @selector(assertBarrierOnQueue)), class_getInstanceMethod(FBSSerialQueue1.class, @selector(assertBarrierOnQueue2))); +} diff --git a/TweakLoader/Makefile b/TweakLoader/Makefile index 0f0aa66..45b0142 100644 --- a/TweakLoader/Makefile +++ b/TweakLoader/Makefile @@ -6,7 +6,7 @@ include $(THEOS)/makefiles/common.mk LIBRARY_NAME = TweakLoader -TweakLoader_FILES = TweakLoader.m NSBundle+FixCydiaSubstrate.m NSFileManager+GuestHooks.m UIKit+GuestHooks.m utils.m DocumentPicker.m +TweakLoader_FILES = TweakLoader.m NSBundle+FixCydiaSubstrate.m NSFileManager+GuestHooks.m UIKit+GuestHooks.m utils.m DocumentPicker.m FBSSerialQueue.m TweakLoader_CFLAGS = -objc-arc TweakLoader_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks From dc02e91dd2cee71ce57382b269a7b79f4b06d934 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 17 Sep 2024 12:24:28 +0800 Subject: [PATCH 33/36] fix startAccessingSecurityScopedResource return false and not able to select folder --- TweakLoader/DocumentPicker.m | 54 +++++++++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 4 deletions(-) diff --git a/TweakLoader/DocumentPicker.m b/TweakLoader/DocumentPicker.m index e4cd53e..34c968a 100644 --- a/TweakLoader/DocumentPicker.m +++ b/TweakLoader/DocumentPicker.m @@ -3,24 +3,60 @@ #import "UIKitPrivate.h" #import "utils.h" + +BOOL fixFilePicker; __attribute__((constructor)) static void NSFMGuestHooksInit() { + fixFilePicker = [NSBundle.mainBundle.infoDictionary[@"doSymlinkInbox"] boolValue]; + swizzle(UIDocumentPickerViewController.class, @selector(initForOpeningContentTypes:asCopy:), @selector(hook_initForOpeningContentTypes:asCopy:)); swizzle(UIDocumentBrowserViewController.class, @selector(initForOpeningContentTypes:), @selector(hook_initForOpeningContentTypes)); -} + if (fixFilePicker) { + swizzle(NSURL.class, @selector(startAccessingSecurityScopedResource), @selector(hook_startAccessingSecurityScopedResource)); + swizzle(UIDocumentPickerViewController.class, @selector(setAllowsMultipleSelection:), @selector(hook_setAllowsMultipleSelection:)); + } +} @implementation UIDocumentPickerViewController(LiveContainerHook) - (instancetype)hook_initForOpeningContentTypes:(NSArray *)contentTypes asCopy:(BOOL)asCopy { - NSArray * contentTypesNew = @[UTTypeItem, UTTypeFolder]; - if([NSBundle.mainBundle.infoDictionary[@"doSymlinkInbox"] boolValue]) { - return [self hook_initForOpeningContentTypes:contentTypesNew asCopy:YES]; + + // if app is going to choose any unrecognized file type, then we replace it with @[UTTypeItem, UTTypeFolder]; + NSArray * contentTypesNew = contentTypes; + for(UTType* type in contentTypes) { + if(!type) { + contentTypesNew = @[UTTypeItem, UTTypeFolder]; + break; + } + } + + // prevent crash when selecting only folder + BOOL shouldMultiselect = NO; + + if (fixFilePicker && [contentTypes count] == 1 && contentTypes[0] == UTTypeFolder) { + contentTypesNew = @[UTTypeItem, UTTypeFolder]; + shouldMultiselect = YES; + } + + if(fixFilePicker) { + UIDocumentPickerViewController* ans = [self hook_initForOpeningContentTypes:contentTypesNew asCopy:YES]; + if(shouldMultiselect) { + [ans hook_setAllowsMultipleSelection:YES]; + } + return ans; } else { return [self hook_initForOpeningContentTypes:contentTypesNew asCopy:asCopy]; } } +- (void)hook_setAllowsMultipleSelection:(BOOL)allowsMultipleSelection { + if([self allowsMultipleSelection]) { + return; + } + [self hook_setAllowsMultipleSelection:YES]; +} + @end @@ -32,3 +68,13 @@ - (instancetype)hook_initForOpeningContentTypes:(NSArray *)contentType } @end + + +@implementation NSURL(LiveContainerHook) + +- (BOOL)hook_startAccessingSecurityScopedResource { + [self hook_startAccessingSecurityScopedResource]; + return YES; +} + +@end From 345097ca62eabe7b4ef6f7aa5993e9504db2403e Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 17 Sep 2024 14:04:51 +0800 Subject: [PATCH 34/36] bugfix --- TweakLoader/DocumentPicker.m | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/TweakLoader/DocumentPicker.m b/TweakLoader/DocumentPicker.m index 34c968a..b1ffbac 100644 --- a/TweakLoader/DocumentPicker.m +++ b/TweakLoader/DocumentPicker.m @@ -22,23 +22,17 @@ @implementation UIDocumentPickerViewController(LiveContainerHook) - (instancetype)hook_initForOpeningContentTypes:(NSArray *)contentTypes asCopy:(BOOL)asCopy { - // if app is going to choose any unrecognized file type, then we replace it with @[UTTypeItem, UTTypeFolder]; - NSArray * contentTypesNew = contentTypes; - for(UTType* type in contentTypes) { - if(!type) { - contentTypesNew = @[UTTypeItem, UTTypeFolder]; - break; - } - } - // prevent crash when selecting only folder BOOL shouldMultiselect = NO; - if (fixFilePicker && [contentTypes count] == 1 && contentTypes[0] == UTTypeFolder) { - contentTypesNew = @[UTTypeItem, UTTypeFolder]; shouldMultiselect = YES; } + // if app is going to choose any unrecognized file type, then we replace it with @[UTTypeItem, UTTypeFolder]; + NSArray * contentTypesNew = @[UTTypeItem, UTTypeFolder]; + + + if(fixFilePicker) { UIDocumentPickerViewController* ans = [self hook_initForOpeningContentTypes:contentTypesNew asCopy:YES]; if(shouldMultiselect) { From ae901bb3679d94965c9fcf0c5367fdba17ae59cf Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Wed, 18 Sep 2024 13:04:36 +0800 Subject: [PATCH 35/36] Localization support & Simplified Chinese translation --- .../GitHub.imageset/Contents.json | 22 + .../GitHub.imageset}/GitHub@2x.png | Bin .../GitHub.imageset}/GitHub@3x.png | Bin .../Twitter.imageset/Contents.json | 22 + .../Twitter.imageset}/Twitter@2x.png | Bin .../Twitter.imageset}/Twitter@3x.png | Bin LiveContainerSwiftUI/LCAppBanner.swift | 48 +- LiveContainerSwiftUI/LCAppListView.swift | 42 +- LiveContainerSwiftUI/LCAppSettingsView.swift | 74 +- LiveContainerSwiftUI/LCSettingsView.swift | 134 +- LiveContainerSwiftUI/LCTabView.swift | 12 +- LiveContainerSwiftUI/LCTweaksView.swift | 34 +- LiveContainerSwiftUI/LCWebView.swift | 18 +- .../project.pbxproj | 46 +- LiveContainerSwiftUI/Localizable.xcstrings | 2212 +++++++++++++++++ LiveContainerSwiftUI/Shared.swift | 35 +- Makefile | 1 + 17 files changed, 2486 insertions(+), 214 deletions(-) create mode 100644 LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset/Contents.json rename {Resources => LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset}/GitHub@2x.png (100%) rename {Resources => LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset}/GitHub@3x.png (100%) create mode 100644 LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset/Contents.json rename {Resources => LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset}/Twitter@2x.png (100%) rename {Resources => LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset}/Twitter@3x.png (100%) create mode 100644 LiveContainerSwiftUI/Localizable.xcstrings diff --git a/LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset/Contents.json b/LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset/Contents.json new file mode 100644 index 0000000..79ee99e --- /dev/null +++ b/LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "GitHub@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "GitHub@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/GitHub@2x.png b/LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset/GitHub@2x.png similarity index 100% rename from Resources/GitHub@2x.png rename to LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset/GitHub@2x.png diff --git a/Resources/GitHub@3x.png b/LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset/GitHub@3x.png similarity index 100% rename from Resources/GitHub@3x.png rename to LiveContainerSwiftUI/Assets.xcassets/GitHub.imageset/GitHub@3x.png diff --git a/LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset/Contents.json b/LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset/Contents.json new file mode 100644 index 0000000..65a02fa --- /dev/null +++ b/LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset/Contents.json @@ -0,0 +1,22 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "Twitter@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "Twitter@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Resources/Twitter@2x.png b/LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset/Twitter@2x.png similarity index 100% rename from Resources/Twitter@2x.png rename to LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset/Twitter@2x.png diff --git a/Resources/Twitter@3x.png b/LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset/Twitter@3x.png similarity index 100% rename from Resources/Twitter@3x.png rename to LiveContainerSwiftUI/Assets.xcassets/Twitter.imageset/Twitter@3x.png diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 3eecab6..3705a43 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -73,7 +73,7 @@ struct LCAppBanner : View, LCAppSettingDelegate { HStack { Text(appInfo.displayName()).font(.system(size: 16)).bold() if model.uiIsShared { - Text("SHARED").font(.system(size: 8)).bold().padding(2) + Text("lc.appBanner.shared".loc).font(.system(size: 8)).bold().padding(2) .frame(width: 50, height:16) .background( Capsule().fill(Color("BadgeColor")) @@ -89,7 +89,7 @@ struct LCAppBanner : View, LCAppSettingDelegate { } Text("\(appInfo.version()) - \(appInfo.bundleIdentifier())").font(.system(size: 12)).foregroundColor(Color("FontColor")) - Text(model.uiDataFolder == nil ? "Data folder not created yet" : model.uiDataFolder!).font(.system(size: 8)).foregroundColor(Color("FontColor")) + Text(LocalizedStringKey(model.uiDataFolder == nil ? "lc.appBanner.noDataFolder".loc : model.uiDataFolder!)).font(.system(size: 8)).foregroundColor(Color("FontColor")) }) } Spacer() @@ -97,7 +97,7 @@ struct LCAppBanner : View, LCAppSettingDelegate { Task{ await runApp() } } label: { if !isSingingInProgress { - Text("Run").bold().foregroundColor(.white) + Text("lc.appBanner.run".loc).bold().foregroundColor(.white) } else { ProgressView().progressViewStyle(.circular) } @@ -150,7 +150,7 @@ struct LCAppBanner : View, LCAppSettingDelegate { Button { openDataFolder() } label: { - Label("Open Data Folder", systemImage: "folder") + Label("lc.appBanner.openDataFolder".loc, systemImage: "folder") } } } @@ -159,28 +159,28 @@ struct LCAppBanner : View, LCAppSettingDelegate { Button { openSafariViewToCreateAppClip() } label: { - Label("Create App Clip", systemImage: "appclip") + Label("lc.appBanner.createAppClip".loc, systemImage: "appclip") } Button { copyLaunchUrl() } label: { - Label("Copy Launch Url", systemImage: "link") + Label("lc.appBanner.copyLaunchUrl".loc, systemImage: "link") } Button { saveIcon() } label: { - Label("Save App Icon", systemImage: "square.and.arrow.down") + Label("lc.appBanner.saveAppIcon".loc, systemImage: "square.and.arrow.down") } } label: { - Label("Add to Home Screen", systemImage: "plus.app") + Label("lc.appBanner.addToHomeScreen".loc, systemImage: "plus.app") } Button { openSettings() } label: { - Label("Settings", systemImage: "gear") + Label("lc.tabView.settings".loc, systemImage: "gear") } @@ -188,7 +188,7 @@ struct LCAppBanner : View, LCAppSettingDelegate { Button(role: .destructive) { Task{ await uninstall() } } label: { - Label("Uninstall", systemImage: "trash") + Label("lc.appBanner.uninstall".loc, systemImage: "trash") } } @@ -208,51 +208,51 @@ struct LCAppBanner : View, LCAppSettingDelegate { Task { await handleURLSchemeLaunch() } } - .alert("Confirm Uninstallation", isPresented: $confirmAppRemovalShow) { + .alert("lc.appBanner.confirmUninstallTitle".loc, isPresented: $confirmAppRemovalShow) { Button(role: .destructive) { self.confirmAppRemoval = true self.appRemovalContinuation?.resume() } label: { - Text("Uninstall") + Text("lc.appBanner.uninstall".loc) } - Button("Cancel", role: .cancel) { + Button("lc.common.cancel".loc, role: .cancel) { self.confirmAppRemoval = false self.appRemovalContinuation?.resume() } } message: { - Text("Are you sure you want to uninstall \(appInfo.displayName()!)?") + Text("lc.appBanner.confirmUninstallMsg %@".localizeWithFormat(appInfo.displayName()!)) } - .alert("Delete Data Folder", isPresented: $confirmAppFolderRemovalShow) { + .alert("lc.appBanner.deleteDataTitle".loc, isPresented: $confirmAppFolderRemovalShow) { Button(role: .destructive) { self.confirmAppFolderRemoval = true self.appFolderRemovalContinuation?.resume() } label: { - Text("Delete") + Text("lc.common.delete".loc) } - Button("Cancel", role: .cancel) { + Button("lc.common.cancel".loc, role: .cancel) { self.confirmAppFolderRemoval = false self.appFolderRemovalContinuation?.resume() } } message: { - Text("Do you also want to delete data folder of \(appInfo.displayName()!)? You can keep it for future use.") + Text("lc.appBanner.deleteDataMsg \(appInfo.displayName()!)") } - .alert("Waiting for JIT", isPresented: $enablingJITShow) { + .alert("lc.appBanner.waitForJitTitle".loc, isPresented: $enablingJITShow) { Button { self.confirmEnablingJIT = true self.confirmEnablingJITContinuation?.resume() } label: { - Text("Launch Now") + Text("lc.appBanner.jitLaunchNow".loc) } - Button("Cancel", role: .cancel) { + Button("lc.common.cancel", role: .cancel) { self.confirmEnablingJIT = false self.confirmEnablingJITContinuation?.resume() } } message: { - Text("Please use your favourite way to enable jit for current LiveContainer.") + Text("lc.appBanner.waitForJitMsg".loc) } - .alert("Error", isPresented: $errorShow) { - Button("OK", action: { + .alert("lc.common.error".loc, isPresented: $errorShow) { + Button("lc.common.ok".loc, action: { }) } message: { Text(errorInfo) diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 1956f36..8d9955c 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -99,7 +99,7 @@ struct LCAppListView : View, LCAppBannerDelegate { .animation(.easeInOut, value: apps) if !sharedModel.isHiddenAppUnlocked { - Text(apps.count > 0 ? "\(apps.count) Apps in Total" : "Press the Plus Button to Install Apps.").foregroundStyle(.gray) + Text(apps.count > 0 ? "lc.appList.appCounter %lld".localizeWithFormat(apps.count) : "lc.appList.installTip".loc).foregroundStyle(.gray) .onTapGesture(count: 3) { Task { await authenticateUser() } } @@ -109,7 +109,7 @@ struct LCAppListView : View, LCAppBannerDelegate { if sharedModel.isHiddenAppUnlocked { LazyVStack { HStack { - Text("Hidden Apps") + Text("lc.appList.hiddenApps".loc) .font(.system(.title2).bold()) .border(Color.black) Spacer() @@ -123,14 +123,14 @@ struct LCAppListView : View, LCAppBannerDelegate { .animation(.easeInOut, value: apps) if hiddenApps.count == 0 { - Text("Long Press on a App to Make it Hidden.") + Text("lc.appList.hideAppTip".loc) .foregroundStyle(.gray) } - Text(apps.count + hiddenApps.count > 0 ? "\(apps.count + hiddenApps.count) Apps in Total" : "Press the Plus Button to Install Apps.").foregroundStyle(.gray) + Text(apps.count + hiddenApps.count > 0 ? "lc.appList.appCounter %lld".localizeWithFormat(apps.count + hiddenApps.count) : "lc.appList.installTip".loc).foregroundStyle(.gray) } if LCUtils.multiLCStatus == 2 { - Text("Manage apps in the primary LiveContainer").foregroundStyle(.gray).padding() + Text("lc.appList.manageInPrimaryTip".loc).foregroundStyle(.gray).padding() } } @@ -146,12 +146,12 @@ struct LCAppListView : View, LCAppBannerDelegate { onLaunchBundleIdChange() } - .navigationTitle("My Apps") + .navigationTitle("lc.appList.myApps".loc) .toolbar { ToolbarItem(placement: .topBarLeading) { if LCUtils.multiLCStatus != 2 { if !installprogressVisible { - Button("Add", systemImage: "plus", action: { + Button("Add".loc, systemImage: "plus", action: { if choosingIPA { choosingIPA = false DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { @@ -169,7 +169,7 @@ struct LCAppListView : View, LCAppBannerDelegate { } } ToolbarItem(placement: .topBarTrailing) { - Button("Open Link", systemImage: "link", action: { + Button("lc.appList.openLink".loc, systemImage: "link", action: { Task { await onOpenWebViewTapped() } }) } @@ -179,18 +179,18 @@ struct LCAppListView : View, LCAppBannerDelegate { } .navigationViewStyle(StackNavigationViewStyle()) .alert(isPresented: $errorShow){ - Alert(title: Text("Error"), message: Text(errorInfo)) + Alert(title: Text("lc.common.error".loc), message: Text(errorInfo)) } .fileImporter(isPresented: $choosingIPA, allowedContentTypes: [.ipa]) { result in Task { await startInstallApp(result) } } - .alert("Installation", isPresented: $installReplaceComfirmVisible) { + .alert("lc.appList.installation".loc, isPresented: $installReplaceComfirmVisible) { ForEach(installOptions, id: \.self) { installOption in Button(role: installOption.isReplace ? .destructive : nil, action: { self.installOptionChosen = installOption self.installOptionContinuation?.resume() }, label: { - Text(installOption.isReplace ? installOption.nameOfFolderToInstall : "Install as new") + Text(installOption.isReplace ? installOption.nameOfFolderToInstall : "lc.appList.installAsNew".loc) }) } @@ -198,14 +198,14 @@ struct LCAppListView : View, LCAppBannerDelegate { self.installOptionChosen = nil self.installOptionContinuation?.resume() }, label: { - Text("Abort Installation") + Text("lc.appList.abortInstallation".loc) }) } message: { - Text("There is an existing application with the same bundle identifier. Replace one or install as new.") + Text("lc.appList.installReplaceTip".loc) } .textFieldAlert( isPresented: $webViewUrlInputOpened, - title: "Enter Url or Url Scheme", + title: "lc.appList.enterUrlTip".loc, text: $webViewUrlInputContent, placeholder: "scheme://", action: { newText in @@ -252,7 +252,7 @@ struct LCAppListView : View, LCAppBannerDelegate { func openWebView(urlString: String) async { guard var urlToOpen = URLComponents(string: urlString), urlToOpen.url != nil else { - errorInfo = "The input url is invalid. Please check and try again" + errorInfo = "lc.appList.urlInvalidError".loc errorShow = true webViewUrlInputContent = "" return @@ -283,7 +283,7 @@ struct LCAppListView : View, LCAppBannerDelegate { guard let appToLaunch = appToLaunch else { - errorInfo = "Scheme \"\(urlToOpen.scheme!)\" cannot be opened by any app installed in LiveContainer." + errorInfo = "lc.appList.schemeCannotOpenError %@".localizeWithFormat(urlToOpen.scheme!) errorShow = true return } @@ -337,7 +337,7 @@ struct LCAppListView : View, LCAppBannerDelegate { func installIpaFile(_ url:URL) async throws { if(!url.startAccessingSecurityScopedResource()) { - throw "Failed to access IPA"; + throw "lc.appList.ipaAccessError".loc; } let fm = FileManager() @@ -368,13 +368,13 @@ struct LCAppListView : View, LCAppBannerDelegate { } } guard let appBundleName = appBundleName else { - throw "App bundle not found" + throw "lc.appList.bundleNotFondError".loc } let appFolderPath = payloadPath.appendingPathComponent(appBundleName) guard let newAppInfo = LCAppInfo(bundlePath: appFolderPath.path) else { - throw "Failed to read app's Info.plist." + throw "lc.appList.infoPlistCannotReadError".loc } var appRelativePath = "\(newAppInfo.bundleIdentifier()!).app" @@ -422,7 +422,7 @@ struct LCAppListView : View, LCAppBannerDelegate { // patch it guard let finalNewApp else { - errorInfo = "Failed to Initialize AppInfo!" + errorInfo = "lc.appList.appInfoInitError".loc errorShow = true return } @@ -513,7 +513,7 @@ struct LCAppListView : View, LCAppBannerDelegate { } if !appFound { - errorInfo = "App not Found" + errorInfo = "lc.appList.appNotFoundError".loc errorShow = true } } diff --git a/LiveContainerSwiftUI/LCAppSettingsView.swift b/LiveContainerSwiftUI/LCAppSettingsView.swift index 033dd22..a103d84 100644 --- a/LiveContainerSwiftUI/LCAppSettingsView.swift +++ b/LiveContainerSwiftUI/LCAppSettingsView.swift @@ -85,7 +85,7 @@ struct LCAppSettingsView : View{ Form { Section { HStack { - Text("Bundle Identifier") + Text("lc.appSettings.bundleId".loc) Spacer() Text(appInfo.relativeBundlePath) .foregroundColor(.gray) @@ -96,13 +96,13 @@ struct LCAppSettingsView : View{ Button { Task{ await createFolder() } } label: { - Label("New data folder", systemImage: "plus") + Label("lc.appSettings.newDataFolder".loc, systemImage: "plus") } if model.uiDataFolder != nil { Button { Task{ await renameDataFolder() } } label: { - Label("Rename data folder", systemImage: "pencil") + Label("lc.appSettings.renameDataFolder".loc, systemImage: "pencil") } } @@ -115,10 +115,10 @@ struct LCAppSettingsView : View{ } } label: { HStack { - Text("Data Folder") + Text("lc.appSettings.dataFolder".loc) .foregroundColor(.primary) Spacer() - Text(model.uiDataFolder == nil ? "Not created yet" : model.uiDataFolder!) + Text(model.uiDataFolder == nil ? "lc.appSettings.noDataFolder".loc : model.uiDataFolder!) .multilineTextAlignment(.trailing) } } @@ -130,14 +130,14 @@ struct LCAppSettingsView : View{ Menu { Picker(selection: $uiPickerTweakFolder , label: Text("")) { - Label("None", systemImage: "nosign").tag(Optional(nil)) + Label("lc.common.none".loc, systemImage: "nosign").tag(Optional(nil)) ForEach(tweakFolders, id:\.self) { folderName in Text(folderName).tag(Optional(folderName)) } } } label: { HStack { - Text("Tweak Folder") + Text("lc.appSettings.tweakFolder".loc) .foregroundColor(.primary) Spacer() Text(model.uiTweakFolder == nil ? "None" : model.uiTweakFolder!) @@ -153,103 +153,103 @@ struct LCAppSettingsView : View{ } else { HStack { - Text("Data Folder") + Text("lc.appSettings.dataFolder".loc) .foregroundColor(.primary) Spacer() - Text(model.uiDataFolder == nil ? "Data folder not created yet" : model.uiDataFolder!) + Text(model.uiDataFolder == nil ? "lc.appSettings.noDataFolder".loc : model.uiDataFolder!) .foregroundColor(.gray) .multilineTextAlignment(.trailing) } HStack { - Text("Tweak Folder") + Text("lc.appSettings.tweakFolder".loc) .foregroundColor(.primary) Spacer() - Text(model.uiTweakFolder == nil ? "None" : model.uiTweakFolder!) + Text(model.uiTweakFolder == nil ? "lc.common.none".loc : model.uiTweakFolder!) .foregroundColor(.gray) .multilineTextAlignment(.trailing) } } if !model.uiIsShared { - Button("Convert to Shared App") { + Button("lc.appSettings.toSharedApp".loc) { Task { await moveToAppGroup()} } } else if LCUtils.multiLCStatus != 2 { - Button("Convert to Private App") { + Button("lc.appSettings.toPrivateApp".loc) { Task { await movePrivateDoc() } } } } header: { - Text("Data") + Text("lc.common.data".loc) } Section { Toggle(isOn: $model.uiIsJITNeeded) { - Text("Launch with JIT") + Text("lc.appSettings.launchWithJit".loc) } .onChange(of: model.uiIsJITNeeded, perform: { newValue in Task { await setJITNeeded(newValue) } }) } footer: { - Text("LiveContainer will try to acquire JIT permission before launching the app.") + Text("lc.appSettings.launchWithJitDesc".loc) } if sharedModel.isHiddenAppUnlocked { Section { Toggle(isOn: $model.uiIsHidden) { - Text("Hide App") + Text("lc.appSettings.hideApp".loc) } .onChange(of: model.uiIsHidden, perform: { newValue in Task { await toggleHidden() } }) } footer: { - Text("To completely hide apps, enable Strict Hiding mode in settings.") + Text("lc.appSettings.hideAppDesc".loc) } } Section { Toggle(isOn: $model.uiDoSymlinkInbox) { - Text("Fix File Picker") + Text("lc.appSettings.fixFilePicker".loc) } .onChange(of: model.uiDoSymlinkInbox, perform: { newValue in Task { await setSimlinkInbox(newValue) } }) } header: { - Text("Fixes") + Text("lc.appSettings.fixes".loc) } footer: { - Text("Fix file picker not responding when hitting 'open' by forcing this app to copy selected files to its inbox. You may view imported files in the 'Inbox' folder in app's data folder.") + Text("lc.appSettings.fixFilePickerDesc".loc) } Section { Toggle(isOn: $model.uiBypassAssertBarrierOnQueue) { - Text("Bypass AssertBarrierOnQueue") + Text("lc.appSettings.bypassAssert".loc) } .onChange(of: model.uiBypassAssertBarrierOnQueue, perform: { newValue in Task { await setBypassAssertBarrierOnQueue(newValue) } }) } footer: { - Text("Might prevent some games from crashing, but may cause them to be unstable.") + Text("lc.appSettings.bypassAssertDesc".loc) } Section { - Button("Force Sign") { + Button("lc.appSettings.forceSign".loc) { Task { await forceResign() } } .disabled(model.isAppRunning) } footer: { - Text("Try to sign again if this app failed to launch with error like 'Invalid Signature'. It this still don't work, renew JIT-Less certificate.") + Text("lc.appSettings.forceSignDesc".loc) } } .navigationTitle(appInfo.displayName()) - .alert("Error", isPresented: $errorShow) { - Button("OK", action: { + .alert("lc.common.error".loc, isPresented: $errorShow) { + Button("lc.common.ok".loc, action: { }) } message: { Text(errorInfo) @@ -257,7 +257,7 @@ struct LCAppSettingsView : View{ .textFieldAlert( isPresented: $renameFolderShow, - title: "Enter the name of new folder", + title: "lc.common.enterNewFolderName".loc, text: $renameFolderContent, placeholder: "", action: { newText in @@ -269,33 +269,33 @@ struct LCAppSettingsView : View{ renameFolerContinuation?.resume() } ) - .alert("Move to App Group", isPresented: $confirmMoveToAppGroupShow) { + .alert("lc.appSettings.toSharedApp".loc, isPresented: $confirmMoveToAppGroupShow) { Button { self.confirmMoveToAppGroup = true self.confirmMoveToAppGroupContinuation?.resume() } label: { - Text("Move") + Text("lc.common.move".loc) } - Button("Cancel", role: .cancel) { + Button("lc.common.cancel".loc, role: .cancel) { self.confirmMoveToAppGroup = false self.confirmMoveToAppGroupContinuation?.resume() } } message: { - Text("Moving this app to App Group allows other LiveContainers to run this app with all its data and tweak preserved. If you want to access its data and tweak again from the file app, move it back.") + Text("lc.appSettings.toSharedAppDesc".loc) } - .alert("Move to Private Document Folder", isPresented: $confirmMoveToPrivateDocShow) { + .alert("lc.appSettings.toPrivateApp".loc, isPresented: $confirmMoveToPrivateDocShow) { Button { self.confirmMoveToPrivateDoc = true self.confirmMoveToPrivateDocContinuation?.resume() } label: { - Text("Move") + Text("lc.common.move".loc) } - Button("Cancel", role: .cancel) { + Button("lc.common.cancel".loc, role: .cancel) { self.confirmMoveToPrivateDoc = false self.confirmMoveToPrivateDocContinuation?.resume() } } message: { - Text("Moving this app to Private Document Folder allows you to access its data and tweaks in the Files app, but it can not be run by other LiveContainers.") + Text("lc.appSettings.toPrivateAppDesc".loc) } } @@ -412,7 +412,7 @@ struct LCAppSettingsView : View{ func movePrivateDoc() async { let runningLC = LCUtils.getAppRunningLCScheme(bundleId: appInfo.relativeBundlePath!) if runningLC != nil { - errorInfo = "Data of this app is currently in \(runningLC!). Open \(runningLC!) and launch it to 'My Apps' screen and try again." + errorInfo = "lc.appSettings.appOpenInOtherLc %@ %@".localizeWithFormat(runningLC!, runningLC!) errorShow = true return } diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index 7eb6d85..94a8feb 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -75,15 +75,15 @@ struct LCSettingsView: View { setupJitLess() } label: { if isJitLessEnabled { - Text("Renew JIT-less certificate") + Text("lc.settings.renewJitLess".loc) } else { - Text("Setup JIT-less certificate") + Text("lc.settings.setupJitLess".loc) } } } header: { - Text("JIT-Less") + Text("lc.settings.jitLess".loc) } footer: { - Text("JIT-less allows you to use LiveContainer without having to enable JIT. Requires AltStore or SideStore.") + Text("lc.settings.jitLessDesc".loc) } } @@ -92,48 +92,39 @@ struct LCSettingsView: View { installAnotherLC() } label: { if LCUtils.multiLCStatus == 0 { - Text("Install another LiveContainer") + Text("lc.settings.multiLCInstall".loc) } else if LCUtils.multiLCStatus == 1 { - Text("Reinstall another LiveContainer") + Text("lc.settings.multiLCReinstall".loc) } else if LCUtils.multiLCStatus == 2 { - Text("This is the second LiveContainer") + Text("lc.settings.multiLCIsSecond".loc) } } .disabled(LCUtils.multiLCStatus == 2) } header: { - Text("Multiple LiveContainers") + Text("lc.settings.multiLC".loc) } footer: { - Text("By installing multiple LiveContainers, and converting apps to Shared Apps, you can open one app between all LiveContainers with most of its data and settings.") + Text("lc.settings.multiLCDesc".loc) } Section { Toggle(isOn: $isAltCertIgnored) { - Text("Ignore ALTCertificate.p12") + Text("lc.settings.ignoreAltCert".loc) } } footer: { - Text("If you see frequent re-sign, enable this option.") + Text("lc.settings.ignoreAltCertDesc".loc) } - Section{ - Toggle(isOn: $frameShortIcon) { - Text("Frame Short Icon") - } - } header: { - Text("Miscellaneous") - } footer: { - Text("Frame shortcut icons with LiveContainer icon.") - } Section { HStack { - Text("Address") + Text("lc.settings.JitAddress".loc) Spacer() TextField("http://x.x.x.x:8080", text: $sideJITServerAddress) .multilineTextAlignment(.trailing) } HStack { - Text("UDID") + Text("lc.settings.JitUDID".loc) Spacer() TextField("", text: $deviceUDID) .multilineTextAlignment(.trailing) @@ -141,32 +132,42 @@ struct LCSettingsView: View { } header: { Text("JIT") } footer: { - Text("Set up your SideJITServer/JITStreamer server. Local Network permission is required.") + Text("lc.settings.JitDesc".loc) + } + + Section{ + Toggle(isOn: $frameShortIcon) { + Text("lc.settings.FrameIcon".loc) + } + } header: { + Text("lc.common.miscellaneous".loc) + } footer: { + Text("lc.settings.FrameIconDesc".loc) } Section { Toggle(isOn: $silentSwitchApp) { - Text("Switch App Without Asking") + Text("lc.settings.silentSwitchApp".loc) } } footer: { - Text("By default, LiveContainer asks you before switching app. Enable this to switch app immediately. Any unsaved data will be lost.") + Text("lc.settings.silentSwitchAppDesc".loc) } Section { Toggle(isOn: $injectToLCItelf) { - Text("Load Tweaks to LiveContainer Itself") + Text("lc.settings.injectLCItself".loc) } } footer: { - Text("Place your tweaks into the global “Tweaks” folder and LiveContainer will pick them up.") + Text("lc.settings.injectLCItselfDesc".loc) } if sharedModel.isHiddenAppUnlocked { Section { Toggle(isOn: $strictHiding) { - Text("Strict Hiding Mode") + Text("lc.settings.strictHiding".loc) } } footer: { - Text("Enabling this mode will only allow hidden apps to be launched by triple clicking the installed app counter.") + Text("lc.settings.strictHidingDesc".loc) } } @@ -175,31 +176,50 @@ struct LCSettingsView: View { Button { moveAppGroupFolderFromPrivateToAppGroup() } label: { - Text("Move Private App Group to Shared Documents Folder") + Text("lc.settings.appGroupPrivateToShare".loc) } Button { moveAppGroupFolderFromAppGroupToPrivate() } label: { - Text("Move Shared App Group Files to Private Documents Folder") + Text("lc.settings.appGroupShareToPrivate".loc) } Button { Task { await moveDanglingFolders() } } label: { - Text("Move Dangling Folders Out of App Group") + Text("lc.settings.moveDanglingFolderOut".loc) } Button(role:.destructive) { Task { await cleanUpUnusedFolders() } } label: { - Text("Clean Unused Data Folders") + Text("lc.settings.cleanDataFolder".loc) } } Button(role:.destructive) { Task { await removeKeyChain() } } label: { - Text("Clean Up Keychain") + Text("lc.settings.cleanKeychain".loc) + } + } + + Section { + HStack { + Image("GitHub") + Button("khanhduytran0/LiveContainer") { + openGitHub() + } + } + HStack { + Image("Twitter") + Button("@TranKha50277352") { + openTwitter() + } } + } header: { + Text("lc.settings.about".loc) + } footer: { + Text("lc.settings.warning".loc) } VStack{ @@ -210,51 +230,51 @@ struct LCSettingsView: View { .background(Color(UIColor.systemGroupedBackground)) .listRowInsets(EdgeInsets()) } - .navigationBarTitle("Settings") - .alert("Error", isPresented: $errorShow){ + .navigationBarTitle("lc.tabView.settings".loc) + .alert("lc.common.error".loc, isPresented: $errorShow){ } message: { Text(errorInfo) } - .alert("Success", isPresented: $successShow){ + .alert("lc.common.success".loc, isPresented: $successShow){ } message: { Text(successInfo) } - .alert("Data Folder Clean Up", isPresented: $confirmAppFolderRemovalShow) { + .alert("lc.settings.cleanDataFolder".loc, isPresented: $confirmAppFolderRemovalShow) { if folderRemoveCount > 0 { Button(role: .destructive) { self.confirmAppFolderRemoval = true self.appFolderRemovalContinuation?.resume() } label: { - Text("Delete") + Text("lc.common.delete".loc) } } - Button("Cancel", role: .cancel) { + Button("lc.common.cancel".loc, role: .cancel) { self.confirmAppFolderRemoval = false self.appFolderRemovalContinuation?.resume() } } message: { if folderRemoveCount > 0 { - Text("Do you want to delete \(folderRemoveCount) unused data folder(s)?") + Text("lc.settings.cleanDataFolderConfirm".localizeWithFormat(folderRemoveCount)) } else { - Text("No data folder to remove. All data folders are in use.") + Text("lc.settings.noDataFolderToClean".loc) } } - .alert("Keychain Clean Up", isPresented: $confirmKeyChainRemovalShow) { + .alert("lc.settings.cleanKeychain".loc, isPresented: $confirmKeyChainRemovalShow) { Button(role: .destructive) { self.confirmKeyChainRemoval = true self.confirmKeyChainContinuation?.resume() } label: { - Text("Delete") + Text("lc.common.delete".loc) } - Button("Cancel", role: .cancel) { + Button("lc.common.cancel".loc, role: .cancel) { self.confirmKeyChainRemoval = false self.confirmKeyChainContinuation?.resume() } } message: { - Text("If some app's account can not be synced between LiveContainers, it's may because it is still stored in current LiveContainer's private keychain. Cleaning up keychain may solve this issue, but it may sign you out of some of your accounts. Continue?") + Text("lc.settings.cleanKeychainDesc".loc) } .onChange(of: isAltCertIgnored) { newValue in saveItem(key: "LCIgnoreALTCertificate", val: newValue) @@ -292,7 +312,7 @@ struct LCSettingsView: View { func setupJitLess() { if !LCUtils.isAppGroupAltStoreLike() { - errorInfo = "Unsupported installation method. Please use AltStore or SideStore to setup this feature." + errorInfo = "lc.settings.unsupportedInstallMethod".loc errorShow = true return; } @@ -309,7 +329,7 @@ struct LCSettingsView: View { func installAnotherLC() { if !LCUtils.isAppGroupAltStoreLike() { - errorInfo = "Unsupported installation method. Please use AltStore or SideStore to setup this feature." + errorInfo = "lc.settings.unsupportedInstallMethod".loc errorShow = true return; } @@ -449,7 +469,7 @@ struct LCSettingsView: View { try fm.moveItem(at: LCPath.lcGroupTweakPath.appendingPathComponent(tweakFolderInUse), to: LCPath.tweakPath.appendingPathComponent(tweakFolderInUse)) movedTweakFolderCount += 1 } - successInfo = "Moved \(movedDataFolderCount) data folder(s) and \(movedTweakFolderCount) tweak folders." + successInfo = "lc.settings.moveDanglingFolderComplete %lld %lld".localizeWithFormat(movedDataFolderCount,movedTweakFolderCount) successShow = true } catch { @@ -471,14 +491,14 @@ struct LCSettingsView: View { let privateFolderContents = try fm.contentsOfDirectory(at: LCPath.appGroupPath, includingPropertiesForKeys: nil) let sharedFolderContents = try fm.contentsOfDirectory(at: LCPath.lcGroupAppGroupPath, includingPropertiesForKeys: nil) if privateFolderContents.count > 0 { - errorInfo = "There are files in the private app group folder. Clean it up and try again." + errorInfo = "lc.settings.appGroupExistPrivate".loc errorShow = true return } for file in sharedFolderContents { try fm.moveItem(at: file, to: LCPath.appGroupPath.appendingPathComponent(file.lastPathComponent)) } - successInfo = "Move success." + successInfo = "lc.settings.appGroup.moveSuccess".loc successShow = true } catch { @@ -500,14 +520,14 @@ struct LCSettingsView: View { let privateFolderContents = try fm.contentsOfDirectory(at: LCPath.appGroupPath, includingPropertiesForKeys: nil) let sharedFolderContents = try fm.contentsOfDirectory(at: LCPath.lcGroupAppGroupPath, includingPropertiesForKeys: nil) if sharedFolderContents.count > 0 { - errorInfo = "There are files in the shared app group folder. Move it out first and try again." + errorInfo = "lc.settings.appGroupExist Shared".loc errorShow = true return } for file in privateFolderContents { try fm.moveItem(at: file, to: LCPath.lcGroupAppGroupPath.appendingPathComponent(file.lastPathComponent)) } - successInfo = "Move success." + successInfo = "lc.settings.appGroup.moveSuccess".loc successShow = true } catch { @@ -515,4 +535,12 @@ struct LCSettingsView: View { errorShow = true } } + + func openGitHub() { + UIApplication.shared.open(URL(string: "https://github.com/khanhduytran0/LiveContainer")!) + } + + func openTwitter() { + UIApplication.shared.open(URL(string: "https://twitter.com/TranKha50277352")!) + } } diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index b2e2f81..274f578 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -92,24 +92,24 @@ struct LCTabView: View { TabView { LCAppListView(apps: $apps, hiddenApps: $hiddenApps, appDataFolderNames: $appDataFolderNames, tweakFolderNames: $tweakFolderNames) .tabItem { - Label("Apps", systemImage: "square.stack.3d.up.fill") + Label("lc.tabView.apps".loc, systemImage: "square.stack.3d.up.fill") } if LCUtils.multiLCStatus != 2 { LCTweaksView(tweakFolders: $tweakFolderNames) .tabItem{ - Label("Tweaks", systemImage: "wrench.and.screwdriver") + Label("lc.tabView.tweaks".loc, systemImage: "wrench.and.screwdriver") } } LCSettingsView(apps: $apps, hiddenApps: $hiddenApps, appDataFolderNames: $appDataFolderNames) .tabItem { - Label("Settings", systemImage: "gearshape.fill") + Label("lc.tabView.settings".loc, systemImage: "gearshape.fill") } } - .alert("Error", isPresented: $errorShow){ - Button("OK", action: { + .alert("lc.common.error".loc, isPresented: $errorShow){ + Button("lc.common.ok".loc, action: { }) - Button("Copy", action: { + Button("lc.common.copy".loc, action: { copyError() }) } message: { diff --git a/LiveContainerSwiftUI/LCTweaksView.swift b/LiveContainerSwiftUI/LCTweaksView.swift index f6ab917..8f16af6 100644 --- a/LiveContainerSwiftUI/LCTweaksView.swift +++ b/LiveContainerSwiftUI/LCTweaksView.swift @@ -87,13 +87,13 @@ struct LCTweakFolderView : View { Button { Task { await renameTweakItem(tweakItem: tweakItem)} } label: { - Label("Rename", systemImage: "pencil") + Label("lc.common.rename".loc, systemImage: "pencil") } Button(role: .destructive) { deleteTweakItem(tweakItem: tweakItem) } label: { - Label("Delete", systemImage: "trash") + Label("lc.common.delete".loc, systemImage: "trash") } } @@ -104,11 +104,11 @@ struct LCTweakFolderView : View { Section { VStack{ if isRoot { - Text("This is the global folder. All tweaks put here will be injected to all guest apps. Create a new folder if you use app-specific tweaks.") + Text("lc.tweakView.globalFolderDesc".loc) .foregroundStyle(.gray) .font(.system(size: 12)) } else { - Text("This is the app-specific folder. Set the tweak folder and the guest app will pick them up recursively.") + Text("lc.tweakView.appFolderDesc".loc) .foregroundStyle(.gray) .font(.system(size: 12)) } @@ -120,14 +120,14 @@ struct LCTweakFolderView : View { } } - .navigationTitle(baseUrl.lastPathComponent) + .navigationTitle(isRoot ? "lc.tabView.tweaks".loc : baseUrl.lastPathComponent) .toolbar { ToolbarItem(placement: .topBarTrailing) { if !isTweakSigning && LCUtils.certificatePassword() != nil { Button { Task { await signAllTweaks() } } label: { - Label("sign", systemImage: "signature") + Label("sign".loc, systemImage: "signature") } } @@ -145,13 +145,13 @@ struct LCTweakFolderView : View { choosingTweak = true } } label: { - Label("Import Tweak", systemImage: "square.and.arrow.down") + Label("lc.tweakView.importTweak".loc, systemImage: "square.and.arrow.down") } Button { Task { await createNewFolder() } } label: { - Label("New folder", systemImage: "folder.badge.plus") + Label("lc.tweakView.newFolder".loc, systemImage: "folder.badge.plus") } } label: { Label("add", systemImage: "plus") @@ -162,15 +162,15 @@ struct LCTweakFolderView : View { } } - .alert("Error", isPresented: $errorShow) { - Button("OK", action: { + .alert("lc.common.error".loc, isPresented: $errorShow) { + Button("lc.common.ok".loc, action: { }) } message: { Text(errorInfo) } .textFieldAlert( isPresented: $newFolderShow, - title: "Enter the name of new folder", + title: "lc.common.enterNewFolderName".loc, text: $newFolderContent, placeholder: "", action: { newText in @@ -184,7 +184,7 @@ struct LCTweakFolderView : View { ) .textFieldAlert( isPresented: $renameFileShow, - title: "Enter New Name", + title: "lc.common.enterNewName".loc, text: $renameFileContent, placeholder: "", action: { newText in @@ -199,12 +199,6 @@ struct LCTweakFolderView : View { .fileImporter(isPresented: $choosingTweak, allowedContentTypes: [.dylib, .lcFramework, .deb], allowsMultipleSelection: true) { result in Task { await startInstallTweak(result) } } - .alert("Error", isPresented: $errorShow) { - Button("OK", action: { - }) - } message: { - Text(errorInfo) - } } func deleteTweakItem(indexSet: IndexSet) { @@ -387,10 +381,10 @@ struct LCTweakFolderView : View { for fileUrl in urls { // handle deb file if(!fileUrl.startAccessingSecurityScopedResource()) { - throw "Cannot open \(fileUrl.lastPathComponent), permission denied." + throw "lc.tweakView.permissionDenied %@".localizeWithFormat(fileUrl.lastPathComponent) } if(!fileUrl.isFileURL) { - throw "\(fileUrl.absoluteString), is not a file." + throw "lc.tweakView.notFileError %@".localizeWithFormat(fileUrl.lastPathComponent) } let toPath = tmpDir.appendingPathComponent(fileUrl.lastPathComponent) try fm.copyItem(at: fileUrl, to: toPath) diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index 41f2640..ae89465 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -69,7 +69,7 @@ struct LCWebView: View { Button(action: { isPresent = false }, label: { - Text("Done") + Text("lc.common.done".loc) }) } @@ -101,20 +101,20 @@ struct LCWebView: View { } } - .alert("Run App", isPresented: $runAppAlertShow) { - Button("Run", action: { + .alert("lc.webView.runApp".loc, isPresented: $runAppAlertShow) { + Button("lc.appBanner.run".loc, action: { self.doRunApp = true self.doRunAppContinuation?.resume() }) - Button("Cancel", role: .cancel, action: { + Button("lc.common.cancel".loc, role: .cancel, action: { self.doRunApp = false self.doRunAppContinuation?.resume() }) } message: { Text(runAppAlertMsg) } - .alert("Error", isPresented: $errorShow) { - Button("OK", action: { + .alert("lc.common.error".loc, isPresented: $errorShow) { + Button("lc.common.ok".loc, action: { }) } message: { Text(errorInfo) @@ -169,7 +169,7 @@ struct LCWebView: View { guard let appToLaunch = appToLaunch else { - errorInfo = "Scheme \"\(url.scheme!)\" cannot be opened by any app installed in LiveContainer." + errorInfo = "lc.appList.schemeCannotOpenError %@".localizeWithFormat(url.scheme!) errorShow = true return } @@ -187,7 +187,7 @@ struct LCWebView: View { } } - runAppAlertMsg = "This web page is trying to launch \"\(appToLaunch.displayName()!)\", continue?" + runAppAlertMsg = "lc.webView.pageLaunch %@".localizeWithFormat(appToLaunch.displayName()!) await withCheckedContinuation { c in self.doRunAppContinuation = c @@ -236,7 +236,7 @@ struct LCWebView: View { } } - runAppAlertMsg = "This web page can be opened in \"\(appToLaunch.displayName()!)\" according to its Associated Domains, continue?" + runAppAlertMsg = "lc.webView.pageCanBeOpenIn %@".localizeWithFormat(appToLaunch.displayName()!) runAppAlertShow = true await withCheckedContinuation { c in self.doRunAppContinuation = c diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj index 2dbc0fc..c05bbad 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 170C3DF92C99A489007F86FB /* Localizable.xcstrings in Resources */ = {isa = PBXBuildFile; fileRef = 170C3DF82C99A489007F86FB /* Localizable.xcstrings */; }; 173564C92C76FE3500C6C918 /* LCAppListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564BC2C76FE3500C6C918 /* LCAppListView.swift */; }; 173564CA2C76FE3500C6C918 /* LCTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564BD2C76FE3500C6C918 /* LCTabView.swift */; }; 173564CC2C76FE3500C6C918 /* LCTweaksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173564C02C76FE3500C6C918 /* LCTweaksView.swift */; }; @@ -22,6 +23,7 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + 170C3DF82C99A489007F86FB /* Localizable.xcstrings */ = {isa = PBXFileReference; lastKnownFileType = text.json.xcstrings; path = Localizable.xcstrings; sourceTree = ""; }; 173564BC2C76FE3500C6C918 /* LCAppListView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCAppListView.swift; sourceTree = ""; }; 173564BD2C76FE3500C6C918 /* LCTabView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCTabView.swift; sourceTree = ""; }; 173564C02C76FE3500C6C918 /* LCTweaksView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LCTweaksView.swift; sourceTree = ""; }; @@ -67,6 +69,7 @@ 178B4C3D2C77654400DD1F74 /* Shared.swift */, 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */, 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */, + 170C3DF82C99A489007F86FB /* Localizable.xcstrings */, ); name = LiveContainerSwiftUI; sourceTree = ""; @@ -129,44 +132,8 @@ knownRegions = ( en, Base, - de, - he, - en_AU, - ar, - el, - ja, - uk, - es_419, zh_CN, - es, - pt_BR, - da, - it, - sk, - pt_PT, - ms, - sv, - cs, - ko, no, - hu, - zh_HK, - tr, - pl, - zh_TW, - en_GB, - vi, - ru, - fr_CA, - fr, - fi, - id, - nl, - th, - ro, - hr, - hi, - ca, ); mainGroup = 17B9B8842C760678009D079E; productRefGroup = 17B9B88E2C760678009D079E /* Products */; @@ -183,6 +150,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 170C3DF92C99A489007F86FB /* Localizable.xcstrings in Resources */, 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -263,7 +231,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; @@ -320,7 +288,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 17.5; + IPHONEOS_DEPLOYMENT_TARGET = 15.0; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; @@ -337,7 +305,6 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"LiveContainerSwiftUI/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; @@ -371,7 +338,6 @@ ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_ASSET_PATHS = "\"LiveContainerSwiftUI/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; diff --git a/LiveContainerSwiftUI/Localizable.xcstrings b/LiveContainerSwiftUI/Localizable.xcstrings new file mode 100644 index 0000000..4a8a083 --- /dev/null +++ b/LiveContainerSwiftUI/Localizable.xcstrings @@ -0,0 +1,2212 @@ +{ + "sourceLanguage" : "en", + "strings" : { + "lc.appBanner.addToHomeScreen" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Add to Home Screen" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "添加到主屏幕" + } + } + } + }, + "lc.appBanner.confirmUninstallMsg %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Are you sure you want to uninstall %@?" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你确定要卸载%@吗?" + } + } + } + }, + "lc.appBanner.confirmUninstallTitle" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Confirm Uninstallation" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "确认卸载" + } + } + } + }, + "lc.appBanner.copyLaunchUrl" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy Launch Url" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制启动链接" + } + } + } + }, + "lc.appBanner.createAppClip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Create App Clip" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建AppClip" + } + } + } + }, + "lc.appBanner.deleteDataMsg %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do you also want to delete data folder of %@? You can keep it for future use." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "你希望删除%@的数据吗?你可以保留以便以后使用" + } + } + } + }, + "lc.appBanner.deleteDataTitle" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete Data Folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除数据" + } + } + } + }, + "lc.appBanner.jitLaunchNow" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launch Now" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "立即启动" + } + } + } + }, + "lc.appBanner.noDataFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data folder not created yet" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据文件夹未创建" + } + } + } + }, + "lc.appBanner.openDataFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Data Folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开数据文件夹" + } + } + } + }, + "lc.appBanner.run" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Run" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "启动" + } + } + } + }, + "lc.appBanner.saveAppIcon" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Save App Icon" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "保存App图标" + } + } + } + }, + "lc.appBanner.shared" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "SHARED" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "共享" + } + } + } + }, + "lc.appBanner.uninstall" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Uninstall" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "卸载" + } + } + } + }, + "lc.appBanner.waitForJitMsg" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please use your favourite way to enable jit for current LiveContainer." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请为LiveContainer启用JIT。" + } + } + } + }, + "lc.appBanner.waitForJitTitle" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Waiting for JIT" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "等待JIT" + } + } + } + }, + "lc.appList.abortInstallation" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Abort Installation" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "终止安装" + } + } + } + }, + "lc.appList.appCounter %lld" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld App in total" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%lld Apps in total" + } + } + } + } + }, + "zh_CN" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "共%lld个App" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "共%lld个App" + } + } + } + } + } + } + }, + "lc.appList.appInfoInitError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to Initialize AppInfo!" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "初始化AppInfo失败!" + } + } + } + }, + "lc.appList.appNotFoundError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "App not Found." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到App!" + } + } + } + }, + "lc.appList.bundleNotFondError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "App bundle not found." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "未找到App包。" + } + } + } + }, + "lc.appList.enterUrlTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter Url or Url Scheme" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请输入链接" + } + } + } + }, + "lc.appList.hiddenApps" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hidden Apps" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐藏App" + } + } + } + }, + "lc.appList.hideAppTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hide app in app settings." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在App设置中可隐藏App" + } + } + } + }, + "lc.appList.infoPlistCannotReadError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to read app's Info.plist." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "读取Info.plist失败。" + } + } + } + }, + "lc.appList.installAsNew" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install as new" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装为新App" + } + } + } + }, + "lc.appList.installation" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Installation" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装" + } + } + } + }, + "lc.appList.installReplaceTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "There is an existing application with the same bundle identifier. Replace one or install as new." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "已有相同包名的App存在。请从下列App中选择一个替换或安装为新App。" + } + } + } + }, + "lc.appList.installTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Press the Plus Button to Install Apps." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "点击加号按钮安装App" + } + } + } + }, + "lc.appList.ipaAccessError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to access IPA" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法读取IPA。" + } + } + } + }, + "lc.appList.manageInPrimaryTip" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Manage apps in the primary LiveContainer" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请在主LiveContainer中管理App" + } + } + } + }, + "lc.appList.myApps" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "My Apps" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "App" + } + } + } + }, + "lc.appList.openLink" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Open Link" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "打开链接" + } + } + } + }, + "lc.appList.schemeCannotOpenError %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Scheme \"%@\" cannot be opened by any app installed in LiveContainer." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "协议“%@”无法被LiveContainer中安装的任何App打开。" + } + } + } + }, + "lc.appList.urlInvalidError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "The input url is invalid. Please check and try again" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入的链接无效,请检查后重试。" + } + } + } + }, + "lc.appSettings.appOpenInOtherLc %@ %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data of this app is currently in %@. Open %@ and launch it to 'My Apps' screen and try again." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "此App的数据当前在%@中。打开%@至App界面活后再试。" + } + } + } + }, + "lc.appSettings.bundleId" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bundle Identifier" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "包名" + } + } + } + }, + "lc.appSettings.bypassAssert" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Bypass AssertBarrierOnQueue" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "绕过AssertBarrierOnQueue" + } + } + } + }, + "lc.appSettings.bypassAssertDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Might prevent some games from crashing, but may cause them to be unstable." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "可能解决一些游戏崩溃的问题,但可能使得游戏变得不稳定。" + } + } + } + }, + "lc.appSettings.dataFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data Folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据文件夹" + } + } + } + }, + "lc.appSettings.fixes" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fixes" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "修复" + } + } + } + }, + "lc.appSettings.fixFilePicker" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fix File Picker" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "修复文件导入" + } + } + } + }, + "lc.appSettings.fixFilePickerDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Fix file picker not responding when hitting 'open' by forcing this app to copy selected files to its inbox. You may view imported files in the 'Inbox' folder in app's data folder." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "若导入文件时点击“打开”没有反应,可启用本选项。启用后所有导入的文件会被复制到App数据文件夹的Inbox目录下。" + } + } + } + }, + "lc.appSettings.forceSign" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Force Sign" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "强制重新签名" + } + } + } + }, + "lc.appSettings.forceSignDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Try to sign again if this app failed to launch with error like 'Invalid Signature'. If this still don't work, renew JIT-Less certificate." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "如果App启动时的报错包含“Invalid Signature”,则可以尝试强制重新签名。如果仍然报错,则可能需要刷新免JIT证书。" + } + } + } + }, + "lc.appSettings.hideApp" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Hide App" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "隐藏App" + } + } + } + }, + "lc.appSettings.hideAppDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "To completely hide apps, enable Strict Hiding mode in settings." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "要完全隐藏App,请在设置中启用严格隐藏模式。" + } + } + } + }, + "lc.appSettings.launchWithJit" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Launch with JIT" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "带JIT启动" + } + } + } + }, + "lc.appSettings.launchWithJitDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "LiveContainer will try to acquire JIT permission before launching the app." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在启动App前LiveContainer会尝试获取JIT权限。" + } + } + } + }, + "lc.appSettings.newDataFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New data folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "创建新的数据文件夹" + } + } + } + }, + "lc.appSettings.noDataFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Not Created Yet" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "未创建" + } + } + } + }, + "lc.appSettings.renameDataFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rename data folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "重命名数据文件夹" + } + } + } + }, + "lc.appSettings.toPrivateApp" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Convert to Private App" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "转换为私有App" + } + } + } + }, + "lc.appSettings.toPrivateAppDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moving this app to Private Document Folder allows you to access its data and tweaks in the Files app, but it can not be run by other LiveContainers." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "将App此转换为私有App允许您在文件App中访问其数据和模块,但它不能由其他LiveContainers运行。" + } + } + } + }, + "lc.appSettings.toSharedApp" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Convert to Shared App" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "转换为共享App" + } + } + } + }, + "lc.appSettings.toSharedAppDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moving this app to App Group allows other LiveContainers to run this app with all its data and tweak preserved. If you want to access its data and tweak again from the file app, move it back." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "将此App移动到AppGroup以允许其他LiveContainers运行此App,并保留其所有数据和模块。如果你想访问它的数据和模块,请将其转换为私有App。" + } + } + } + }, + "lc.appSettings.tweakFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tweak Folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "模块文件夹" + } + } + } + }, + "lc.common.cancel" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cancel" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "取消" + } + } + } + }, + "lc.common.copy" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Copy" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "复制" + } + } + } + }, + "lc.common.data" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Data" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "数据" + } + } + } + }, + "lc.common.delete" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Delete" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "删除" + } + } + } + }, + "lc.common.done" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Done" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "完成" + } + } + } + }, + "lc.common.enterNewFolderName" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter the name of new folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入新文件夹的名称" + } + } + } + }, + "lc.common.enterNewName" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enter New Name" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "输入新名称" + } + } + } + }, + "lc.common.error" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Error" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "错误" + } + } + } + }, + "lc.common.miscellaneous" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Miscellaneous" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "杂项" + } + } + } + }, + "lc.common.move" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Move" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "移动" + } + } + } + }, + "lc.common.none" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "None" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "无" + } + } + } + }, + "lc.common.ok" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "OK" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "好" + } + } + } + }, + "lc.common.rename" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Rename" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "重命名" + } + } + } + }, + "lc.common.success" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Success" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "成功" + } + } + } + }, + "lc.settings.about" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "About Me" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "关于" + } + } + } + }, + "lc.settings.appGroup.moveSuccess" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Move success." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "移动完成。" + } + } + } + }, + "lc.settings.appGroupExist Shared" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "There are files in the shared app group folder. Move it out first and try again." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "共享的AppGroup文件夹中不为空。将其中文件移动出来后再试。" + } + } + } + }, + "lc.settings.appGroupExistPrivate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "There are files in the private app group folder. Move it out first and try again." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "私有的AppGroup文件夹中不为空。将其中文件移动出来后再试。" + } + } + } + }, + "lc.settings.appGroupPrivateToShare" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Move Private App Group to Shared Documents Folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "移动私有AppGroup文件到共享文件夹" + } + } + } + }, + "lc.settings.appGroupShareToPrivate" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Move Shared App Group Files to Private Documents Folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "移动共享AppGroup文件到私有文件夹" + } + } + } + }, + "lc.settings.cleanDataFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clean Unused Data Folders" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "清理未使用的数据文件夹" + } + } + } + }, + "lc.settings.cleanDataFolderConfirm %lld" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do you want to delete %lld unused data folder?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "Do you want to delete %lld unused data folders?" + } + } + } + } + }, + "zh_CN" : { + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "你确定要删除%lld个未使用的文件夹吗?" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "你确定要删除%lld个未使用的文件夹吗?" + } + } + } + } + } + } + }, + "lc.settings.cleanKeychain" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Clean Up Keychain" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "清理Keychain" + } + } + } + }, + "lc.settings.cleanKeychainDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If some app's account can not be synced between LiveContainers, it's may because it is still stored in current LiveContainer's private keychain. Cleaning up keychain may solve this issue, but it may sign you out of some of your accounts. Continue?" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "某些App的账户不能在LiveContainer间同步的原因可能是其Keychain还存储在本地。清理Keychain可能能解决此问题,但可能会导致你登出某些账户。要继续吗?" + } + } + } + }, + "lc.settings.FrameIcon" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frame Short Icon" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在App图标外加边框" + } + } + } + }, + "lc.settings.FrameIconDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Frame shortcut icons with LiveContainer icon." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "在App图标外加LiveContainer图标边框。" + } + } + } + }, + "lc.settings.ignoreAltCert" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ignore ALTCertificate.p12" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "忽略ALTCertificate.p12" + } + } + } + }, + "lc.settings.ignoreAltCertDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "If you see frequent re-sign, enable this option." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "如果App重新签名过于频繁,可尝试启用此项。" + } + } + } + }, + "lc.settings.injectLCItself" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Load Tweaks to LiveContainer Itself" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "向LiveContainer加载模块" + } + } + } + }, + "lc.settings.injectLCItselfDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Place your tweaks into the global “Tweaks” folder and LiveContainer will pick them up." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用后,LiveContainer自身会加载全局模块文件夹中的模块。" + } + } + } + }, + "lc.settings.JitAddress" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Address" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "地址" + } + } + } + }, + "lc.settings.JitDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Set up your SideJITServer/JITStreamer server. Local Network permission is required." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "配置SideJITServer/JITStreamer。需要本地网络权限。" + } + } + } + }, + "lc.settings.jitLess" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "JIT-Less" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "免JIT模式" + } + } + } + }, + "lc.settings.jitLessDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "JIT-less allows you to use LiveContainer without having to enable JIT. Requires AltStore or SideStore." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "配置免JIT模式后,LiveContainer无需JIT即可使用。需要SideStore或AltStore。" + } + } + } + }, + "lc.settings.JitUDID" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "UDID" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "UDID" + } + } + } + }, + "lc.settings.moveDanglingFolderComplete %lld %lld" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Moved %#@arg1@ and %#@arg2@." + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%arg data folder" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%arg data folders" + } + } + } + } + }, + "arg2" : { + "argNum" : 2, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%arg tweak folder" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%arg tweak folders" + } + } + } + } + } + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "移动了 %#@arg1@和 %#@arg2@。" + }, + "substitutions" : { + "arg1" : { + "argNum" : 1, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%arg个数据文件夹" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%arg个数据文件夹" + } + } + } + } + }, + "arg2" : { + "argNum" : 2, + "formatSpecifier" : "lld", + "variations" : { + "plural" : { + "one" : { + "stringUnit" : { + "state" : "translated", + "value" : "%arg个模块文件夹" + } + }, + "other" : { + "stringUnit" : { + "state" : "translated", + "value" : "%arg个模块文件夹" + } + } + } + } + } + } + } + } + }, + "lc.settings.moveDanglingFolderOut" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Move Dangling Folders Out of App Group" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "将未使用的文件夹移出AppGroup" + } + } + } + }, + "lc.settings.multiLC" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Multiple LiveContainers" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "多LiveContainer" + } + } + } + }, + "lc.settings.multiLCDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "By installing multiple LiveContainers, and converting apps to Shared Apps, you can open one app between all LiveContainers with most of its data and settings." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "通过安装多个LiveContainer,并将App变为共享App,你可以在这些LiveContainer间共享安装的App及其数据,并同时运行多个App。" + } + } + } + }, + "lc.settings.multiLCInstall" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Install another LiveContainer" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装第二个LiveContainer" + } + } + } + }, + "lc.settings.multiLCIsSecond" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This is the second LiveContainer" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "这是第二个LiveContainer" + } + } + } + }, + "lc.settings.multiLCReinstall" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Reinstall another LiveContainer" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "重新安装第二个LiveContainer" + } + } + } + }, + "lc.settings.noDataFolderToClean" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "No data folder to remove. All data folders are in use." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "未删除任何数据文件夹,所有文件夹都已被使用。" + } + } + } + }, + "lc.settings.renewJitLess" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Renew JIT-less certificate" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "刷新免JIT模式证书" + } + } + } + }, + "lc.settings.setupJitLess" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Setup JIT-less certificate" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置免JIT模式证书" + } + } + } + }, + "lc.settings.silentSwitchApp" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Switch App Without Asking" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "静默切换App" + } + } + } + }, + "lc.settings.silentSwitchAppDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "By default, LiveContainer asks you before switching app. Enable this to switch app immediately. Any unsaved data will be lost." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "LiveContainer在切换App时默认会向你确认。启用此选项来静默切换,所有未保存的数将会丢失。" + } + } + } + }, + "lc.settings.strictHiding" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Strict Hiding Mode" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "严格隐藏模式" + } + } + } + }, + "lc.settings.strictHidingDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Enabling this mode will only allow hidden apps to be launched by triple clicking the installed app counter." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "启用后,将只能从App选择界面启动App。" + } + } + } + }, + "lc.settings.unsupportedInstallMethod" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Unsupported installation method. Please use AltStore or SideStore to setup this feature." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "安装方式不受支持。请使用SideStore或AltStore安装来使用此功能。" + } + } + } + }, + "lc.settings.warning" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Please Don't use LiveContainer for piracy." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "请勿将LiveContainer用于盗版。" + } + } + } + }, + "lc.tabView.apps" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Apps" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "App" + } + } + } + }, + "lc.tabView.settings" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Settings" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "设置" + } + } + } + }, + "lc.tabView.tweaks" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Tweaks" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "模块" + } + } + } + }, + "lc.tweakView.appFolderDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This is the app-specific folder. Set the tweak folder and the guest app will pick them up recursively." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "这是App专用文件夹。将App的模块文件夹设置为本文件夹,则App在启动时会递归地加载。" + } + } + } + }, + "lc.tweakView.globalFolderDesc" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This is the global folder. All tweaks put here will be injected to all guest apps. Create a new folder if you use app-specific tweaks." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "这是全局文件夹,这里的全部模块会被加载到所有App中。要为App单独加载模块,请新建文件夹。" + } + } + } + }, + "lc.tweakView.importTweak" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Import Tweak" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "导入模块" + } + } + } + }, + "lc.tweakView.newFolder" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "New folder" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "新建文件夹" + } + } + } + }, + "lc.tweakView.notFileError %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@ is not a file." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "%@不是一个文件。" + } + } + } + }, + "lc.tweakView.permissionDeniedError %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Cannot open %@, permission denied." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "无法打开%@,权限不足。" + } + } + } + }, + "lc.utils.initSigningError" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Failed to initiate bundle signing." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "初始化签名失败" + } + } + } + }, + "lc.utils.requireAuthentication" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Authentication Required." + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "需要授权。" + } + } + } + }, + "lc.webView.pageCanBeOpenIn %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This web page can be opened in \"%@\", continue?" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "此页面可以在“%@”中打开,要继续吗?" + } + } + } + }, + "lc.webView.pageLaunch %@" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "This web page is trying to launch \"%@\", continue?" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "此页面想要打开“%@”,要继续吗?" + } + } + } + }, + "lc.webView.runApp" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Run App" + } + }, + "zh_CN" : { + "stringUnit" : { + "state" : "translated", + "value" : "启动App" + } + } + } + } + }, + "version" : "1.0" +} \ No newline at end of file diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 61635a0..1575202 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -64,8 +64,35 @@ class DataManager { extension String: LocalizedError { public var errorDescription: String? { return self } + + private static var enBundle : Bundle? { + let language = "en" + let path = Bundle.main.path(forResource:language, ofType: "lproj") + let bundle = Bundle(path: path!) + return bundle + } + + var loc: String { + let message = NSLocalizedString(self, comment: "") + if message != self { + return message + } + + if let forcedString = String.enBundle?.localizedString(forKey: self, value: nil, table: nil){ + return forcedString + }else { + return self + } + } + + func localizeWithFormat(_ arguments: CVarArg...) -> String{ + String.localizedStringWithFormat(self.loc, arguments) + } + } + + extension UTType { static let ipa = UTType(filenameExtension: "ipa")! static let dylib = UTType(filenameExtension: "dylib")! @@ -134,11 +161,11 @@ public struct TextFieldAlertModifier: ViewModifier { $0.text = self.text.wrappedValue $0.clearButtonMode = .always } - controller.addAction(UIAlertAction(title: "Cancel", style: .cancel) { _ in + controller.addAction(UIAlertAction(title: "lc.common.cancel".loc, style: .cancel) { _ in self.actionCancel(nil) shutdown() }) - controller.addAction(UIAlertAction(title: "OK", style: .default) { _ in + controller.addAction(UIAlertAction(title: "lc.common.ok".loc, style: .default) { _ in self.action(controller.textFields?.first?.text) shutdown() }) @@ -260,7 +287,7 @@ extension LCUtils { c.resume() } guard let progress = progress else { - ans = "Failed to initiate bundle signing." + ans = "lc.utils.initSigningError".loc c.resume() return } @@ -298,7 +325,7 @@ extension LCUtils { // Check if the device supports biometric authentication if context.canEvaluatePolicy(.deviceOwnerAuthentication, error: &error) { // Determine the reason for the authentication request - let reason = "Authentication Required." + let reason = "lc.utils.requireAuthentication".loc // Evaluate the authentication policy context.evaluatePolicy(.deviceOwnerAuthentication, localizedReason: reason) { success, evaluationError in diff --git a/Makefile b/Makefile index 7764a7e..22f74d4 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ include $(THEOS_MAKE_PATH)/aggregate.mk # Make the executable name longer so we have space to overwrite it with the guest app's name before-package:: + @/Applications/Xcode.app/Contents/Developer/usr/bin/xcstringstool compile ./LiveContainerSwiftUI/Localizable.xcstrings --output-directory $(THEOS_STAGING_DIR)/Applications/LiveContainer.app @/Applications/Xcode.app/Contents/Developer/usr/bin/actool LiveContainerSwiftUI/Assets.xcassets --compile $(THEOS_STAGING_DIR)/Applications/LiveContainer.app --platform iphoneos --minimum-deployment-target 14.0 @cp $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/LiveContainer $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/JITLessSetup @ldid -Sentitlements_setup.xml $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/JITLessSetup From a58e557092fa7ff1c1bb6ea727af3aade5feb202 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Fri, 20 Sep 2024 16:33:26 +0800 Subject: [PATCH 36/36] bugfix & code style improvement --- LCSharedUtils.m | 5 +- LiveContainerSwiftUI/LCAppBanner.swift | 210 ++++-------------- LiveContainerSwiftUI/LCAppListView.swift | 166 +++++++------- LiveContainerSwiftUI/LCAppModel.swift | 136 ++++++++++++ LiveContainerSwiftUI/LCAppSettingsView.swift | 127 +++-------- LiveContainerSwiftUI/LCSettingsView.swift | 65 ++---- LiveContainerSwiftUI/LCTabView.swift | 16 +- LiveContainerSwiftUI/LCTweaksView.swift | 54 ++--- LiveContainerSwiftUI/LCWebView.swift | 57 ++--- .../project.pbxproj | 4 + LiveContainerSwiftUI/ObjcBridge.swift | 31 ++- LiveContainerSwiftUI/Shared.swift | 37 ++- LiveContainerUI/LCUtils.m | 8 +- 13 files changed, 427 insertions(+), 489 deletions(-) create mode 100644 LiveContainerSwiftUI/LCAppModel.swift diff --git a/LCSharedUtils.m b/LCSharedUtils.m index 28c3691..f670d0f 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -24,11 +24,11 @@ + (NSString *)appGroupID { } + (NSString *)certificatePassword { - NSString* ans = [lcUserDefaults objectForKey:@"LCCertificatePassword"]; + NSString* ans = [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificatePassword"]; if(ans) { return ans; } else { - return [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificatePassword"]; + return [lcUserDefaults objectForKey:@"LCCertificatePassword"]; } } @@ -65,6 +65,7 @@ + (BOOL)askForJIT { NSURL *launchURL = [NSURL URLWithString:[NSString stringWithFormat:urlScheme, NSBundle.mainBundle.bundleIdentifier]]; if ([UIApplication.sharedApplication canOpenURL:launchURL]) { [UIApplication.sharedApplication openURL:launchURL options:@{} completionHandler:nil]; + [LCSharedUtils launchToGuestApp]; return YES; } } else { diff --git a/LiveContainerSwiftUI/LCAppBanner.swift b/LiveContainerSwiftUI/LCAppBanner.swift index 3705a43..17445d9 100644 --- a/LiveContainerSwiftUI/LCAppBanner.swift +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -10,34 +10,23 @@ import SwiftUI import UniformTypeIdentifiers protocol LCAppBannerDelegate { - func removeApp(app: LCAppInfo) - func changeAppVisibility(app: LCAppInfo) + func removeApp(app: LCAppModel) func installMdm(data: Data) func openNavigationView(view: AnyView) - func closeNavigationView() } -struct LCAppBanner : View, LCAppSettingDelegate { +struct LCAppBanner : View { @State var appInfo: LCAppInfo var delegate: LCAppBannerDelegate - @StateObject var model : LCAppModel + @ObservedObject var model : LCAppModel @Binding var appDataFolders: [String] @Binding var tweakFolders: [String] - - @State private var confirmAppRemovalShow = false - @State private var confirmAppFolderRemovalShow = false - - @State private var confirmAppRemoval = false - @State private var confirmAppFolderRemoval = false - @State private var appRemovalContinuation : CheckedContinuation? = nil - @State private var appFolderRemovalContinuation : CheckedContinuation? = nil - - @State private var enablingJITShow = false - @State private var confirmEnablingJIT = false - @State private var confirmEnablingJITContinuation : CheckedContinuation? = nil + @StateObject private var appRemovalAlert = YesNoHelper() + @StateObject private var appFolderRemovalAlert = YesNoHelper() + @StateObject private var jitAlert = YesNoHelper() @State private var saveIconExporterShow = false @State private var saveIconFile : ImageDocument? @@ -45,19 +34,15 @@ struct LCAppBanner : View, LCAppSettingDelegate { @State private var errorShow = false @State private var errorInfo = "" - @State private var isSingingInProgress = false - @State private var signProgress = 0.0 - - @State private var observer : NSKeyValueObservation? @EnvironmentObject private var sharedModel : SharedModel - init(appInfo: LCAppInfo, delegate: LCAppBannerDelegate, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>) { - _appInfo = State(initialValue: appInfo) + init(appModel: LCAppModel, delegate: LCAppBannerDelegate, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>) { + _appInfo = State(initialValue: appModel.appInfo) _appDataFolders = appDataFolders _tweakFolders = tweakFolders self.delegate = delegate - _model = StateObject(wrappedValue: LCAppModel(appInfo: appInfo)) + _model = ObservedObject(wrappedValue: appModel) } var body: some View { @@ -96,7 +81,7 @@ struct LCAppBanner : View, LCAppSettingDelegate { Button { Task{ await runApp() } } label: { - if !isSingingInProgress { + if !model.isSigningInProgress { Text("lc.appBanner.run".loc).bold().foregroundColor(.white) } else { ProgressView().progressViewStyle(.circular) @@ -108,7 +93,7 @@ struct LCAppBanner : View, LCAppSettingDelegate { .frame(height: 32) .fixedSize() .background(GeometryReader { g in - if !isSingingInProgress { + if !model.isSigningInProgress { Capsule().fill(Color("FontColor")) } else { let w = g.size.width @@ -118,7 +103,7 @@ struct LCAppBanner : View, LCAppSettingDelegate { Circle() .fill(Color("FontColor")) .frame(width: w * 2, height: w * 2) - .offset(x: (signProgress - 2) * w, y: h/2-w) + .offset(x: (model.signProgress - 2) * w, y: h/2-w) } }) @@ -129,6 +114,9 @@ struct LCAppBanner : View, LCAppSettingDelegate { .padding() .frame(height: 88) .background(RoundedRectangle(cornerSize: CGSize(width:22, height: 22)).fill(Color("AppBannerBG"))) + .onAppear() { + handleOnAppear() + } .fileExporter( isPresented: $saveIconExporterShow, @@ -198,54 +186,40 @@ struct LCAppBanner : View, LCAppSettingDelegate { - } - - .onChange(of: sharedModel.bundleIdToLaunch, perform: { newValue in - Task { await handleURLSchemeLaunch() } - }) - - .onAppear() { - Task { await handleURLSchemeLaunch() } } - .alert("lc.appBanner.confirmUninstallTitle".loc, isPresented: $confirmAppRemovalShow) { + .alert("lc.appBanner.confirmUninstallTitle".loc, isPresented: $appRemovalAlert.show) { Button(role: .destructive) { - self.confirmAppRemoval = true - self.appRemovalContinuation?.resume() + appRemovalAlert.close(result: true) } label: { Text("lc.appBanner.uninstall".loc) } Button("lc.common.cancel".loc, role: .cancel) { - self.confirmAppRemoval = false - self.appRemovalContinuation?.resume() + appRemovalAlert.close(result: false) } } message: { Text("lc.appBanner.confirmUninstallMsg %@".localizeWithFormat(appInfo.displayName()!)) } - .alert("lc.appBanner.deleteDataTitle".loc, isPresented: $confirmAppFolderRemovalShow) { + .alert("lc.appBanner.deleteDataTitle".loc, isPresented: $appFolderRemovalAlert.show) { Button(role: .destructive) { - self.confirmAppFolderRemoval = true - self.appFolderRemovalContinuation?.resume() + appFolderRemovalAlert.close(result: true) } label: { Text("lc.common.delete".loc) } Button("lc.common.cancel".loc, role: .cancel) { - self.confirmAppFolderRemoval = false - self.appFolderRemovalContinuation?.resume() + appFolderRemovalAlert.close(result: false) } } message: { Text("lc.appBanner.deleteDataMsg \(appInfo.displayName()!)") } - .alert("lc.appBanner.waitForJitTitle".loc, isPresented: $enablingJITShow) { + .alert("lc.appBanner.waitForJitTitle".loc, isPresented: $jitAlert.show) { Button { - self.confirmEnablingJIT = true - self.confirmEnablingJITContinuation?.resume() + jitAlert.close(result: true) } label: { Text("lc.appBanner.jitLaunchNow".loc) } Button("lc.common.cancel", role: .cancel) { - self.confirmEnablingJIT = false - self.confirmEnablingJITContinuation?.resume() + jitAlert.close(result: false) } } message: { Text("lc.appBanner.waitForJitMsg".loc) @@ -260,96 +234,24 @@ struct LCAppBanner : View, LCAppSettingDelegate { } - func handleURLSchemeLaunch() async { - if self.appInfo.relativeBundlePath == sharedModel.bundleIdToLaunch { - await runApp() - } + func handleOnAppear() { + model.jitAlert = jitAlert } func runApp() async { - if let runningLC = LCUtils.getAppRunningLCScheme(bundleId: self.appInfo.relativeBundlePath) { - let openURL = URL(string: "\(runningLC)://livecontainer-launch?bundle-name=\(self.appInfo.relativeBundlePath!)")! - if UIApplication.shared.canOpenURL(openURL) { - await UIApplication.shared.open(openURL) - return - } - } - model.isAppRunning = true - - var signError : String? = nil - await withCheckedContinuation({ c in - appInfo.patchExecAndSignIfNeed(completionHandler: { error in - signError = error; - c.resume() - }, progressHandler: { signProgress in - guard let signProgress else { - return - } - self.isSingingInProgress = true - self.observer = signProgress.observe(\.fractionCompleted) { p, v in - DispatchQueue.main.async { - self.signProgress = signProgress.fractionCompleted - } - } - }, forceSign: false) - }) - self.isSingingInProgress = false - if let signError { - errorInfo = signError + do { + try await model.runApp() + } catch { + errorInfo = errorInfo errorShow = true - model.isAppRunning = false - return } - - UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") - if appInfo.isJITNeeded { - await self.jitLaunch() - } else { - LCUtils.launchToGuestApp() - } - - model.isAppRunning = false - } + func openSettings() { - delegate.openNavigationView(view: AnyView(LCAppSettingsView(model: model, appDataFolders: $appDataFolders, tweakFolders: $tweakFolders, delegate: self))) + delegate.openNavigationView(view: AnyView(LCAppSettingsView(model: model, appDataFolders: $appDataFolders, tweakFolders: $tweakFolders))) } - func forceResign() async { - if model.isAppRunning { - return - } - - model.isAppRunning = true - var signError : String? = nil - await withCheckedContinuation({ c in - appInfo.patchExecAndSignIfNeed(completionHandler: { error in - signError = error; - c.resume() - }, progressHandler: { signProgress in - guard let signProgress else { - return - } - self.isSingingInProgress = true - self.observer = signProgress.observe(\.fractionCompleted) { p, v in - DispatchQueue.main.async { - self.signProgress = signProgress.fractionCompleted - } - } - }, forceSign: true) - }) - self.isSingingInProgress = false - if let signError { - errorInfo = signError - errorShow = true - model.isAppRunning = false - return - } - model.isAppRunning = false - } - - func openDataFolder() { let url = URL(string:"shareddocuments://\(LCPath.docPath.path)/Data/Application/\(appInfo.dataUUID()!)") @@ -360,29 +262,22 @@ struct LCAppBanner : View, LCAppSettingDelegate { func uninstall() async { do { - await withCheckedContinuation { c in - self.appRemovalContinuation = c - self.confirmAppRemovalShow = true; - } - - if !self.confirmAppRemoval { + if let result = await appRemovalAlert.open(), !result { return } + + var doRemoveAppFolder = false if self.appInfo.getDataUUIDNoAssign() != nil { - self.confirmAppFolderRemovalShow = true; - await withCheckedContinuation { c in - self.appFolderRemovalContinuation = c - self.confirmAppFolderRemovalShow = true; + if let result = await appFolderRemovalAlert.open() { + doRemoveAppFolder = result } - } else { - self.confirmAppFolderRemoval = false; + } - let fm = FileManager() try fm.removeItem(atPath: self.appInfo.bundlePath()!) - self.delegate.removeApp(app: self.appInfo) - if self.confirmAppFolderRemoval { + self.delegate.removeApp(app: self.model) + if doRemoveAppFolder { let dataUUID = appInfo.dataUUID()! let dataFolderPath = LCPath.dataPath.appendingPathComponent(dataUUID) try fm.removeItem(at: dataFolderPath) @@ -399,20 +294,7 @@ struct LCAppBanner : View, LCAppSettingDelegate { errorShow = true } } - - func jitLaunch() async { - LCUtils.askForJIT() - await withCheckedContinuation { c in - self.confirmEnablingJITContinuation = c - enablingJITShow = true - } - if confirmEnablingJIT { - LCUtils.launchToGuestApp() - } else { - UserDefaults.standard.removeObject(forKey: "selected") - } - } func copyLaunchUrl() { UIPasteboard.general.string = "livecontainer://livecontainer-launch?bundle-name=\(appInfo.relativeBundlePath!)" @@ -429,18 +311,6 @@ struct LCAppBanner : View, LCAppSettingDelegate { } - func toggleHidden() async { - delegate.closeNavigationView() - if appInfo.isHidden { - appInfo.isHidden = false - model.uiIsHidden = false - } else { - appInfo.isHidden = true - model.uiIsHidden = true - } - delegate.changeAppVisibility(app: appInfo) - } - func saveIcon() { let img = appInfo.icon()! self.saveIconFile = ImageDocument(uiImage: img) diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift index 8d9955c..17ee1e5 100644 --- a/LiveContainerSwiftUI/LCAppListView.swift +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -11,12 +11,14 @@ import UniformTypeIdentifiers struct AppReplaceOption : Hashable { var isReplace: Bool var nameOfFolderToInstall: String - var appToReplace: LCAppInfo? + var appToReplace: LCAppModel? } -struct LCAppListView : View, LCAppBannerDelegate { - @Binding var apps: [LCAppInfo] - @Binding var hiddenApps: [LCAppInfo] +struct LCAppListView : View, LCAppBannerDelegate, LCAppModelDelegate { + + @Binding var apps: [LCAppModel] + @Binding var hiddenApps: [LCAppModel] + @Binding var appDataFolderNames: [String] @Binding var tweakFolderNames: [String] @@ -32,16 +34,12 @@ struct LCAppListView : View, LCAppBannerDelegate { @State var uiInstallProgressPercentage = 0.0 @State var installObserver : NSKeyValueObservation? - @State var installReplaceComfirmVisible = false @State var installOptions: [AppReplaceOption] - @State var installOptionChosen: AppReplaceOption? - @State var installOptionContinuation : CheckedContinuation? = nil + @StateObject var installReplaceAlert = AlertHelper() @State var webViewOpened = false @State var webViewURL : URL = URL(string: "about:blank")! - @State private var webViewUrlInputOpened = false - @State private var webViewUrlInputContent = "" - @State private var webViewUrlInputContinuation : CheckedContinuation? = nil + @StateObject private var webViewUrlInput = InputHelper() @State var safariViewOpened = false @State var safariViewURL = URL(string: "https://google.com")! @@ -51,13 +49,13 @@ struct LCAppListView : View, LCAppBannerDelegate { @EnvironmentObject private var sharedModel : SharedModel - init(apps: Binding<[LCAppInfo]>, hiddenApps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>, tweakFolderNames: Binding<[String]>) { + init(apps: Binding<[LCAppModel]>, hiddenApps: Binding<[LCAppModel]>, appDataFolderNames: Binding<[String]>, tweakFolderNames: Binding<[String]>) { _installOptions = State(initialValue: []) - _installOptionChosen = State(initialValue: nil) _apps = apps _hiddenApps = hiddenApps _appDataFolderNames = appDataFolderNames _tweakFolderNames = tweakFolderNames + } var body: some View { @@ -90,7 +88,7 @@ struct LCAppListView : View, LCAppBannerDelegate { .zIndex(.infinity) LazyVStack { ForEach(apps, id: \.self) { app in - LCAppBanner(appInfo: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) + LCAppBanner(appModel: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) } .transition(.scale) @@ -115,7 +113,7 @@ struct LCAppListView : View, LCAppBannerDelegate { Spacer() } ForEach(hiddenApps, id: \.self) { app in - LCAppBanner(appInfo: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) + LCAppBanner(appModel: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) } .transition(.scale) } @@ -137,14 +135,9 @@ struct LCAppListView : View, LCAppBannerDelegate { .coordinateSpace(name: "scroll") .onAppear { if !didAppear { - didAppear = true - Task { await checkIfAppDelegateNeedOpenWebPage() } - onLaunchBundleIdChange() + onAppear() } } - .onChange(of: sharedModel.bundleIdToLaunch) { newValue in - onLaunchBundleIdChange() - } .navigationTitle("lc.appList.myApps".loc) .toolbar { @@ -184,19 +177,17 @@ struct LCAppListView : View, LCAppBannerDelegate { .fileImporter(isPresented: $choosingIPA, allowedContentTypes: [.ipa]) { result in Task { await startInstallApp(result) } } - .alert("lc.appList.installation".loc, isPresented: $installReplaceComfirmVisible) { + .alert("lc.appList.installation".loc, isPresented: $installReplaceAlert.show) { ForEach(installOptions, id: \.self) { installOption in Button(role: installOption.isReplace ? .destructive : nil, action: { - self.installOptionChosen = installOption - self.installOptionContinuation?.resume() + installReplaceAlert.close(result: installOption) }, label: { Text(installOption.isReplace ? installOption.nameOfFolderToInstall : "lc.appList.installAsNew".loc) }) } Button(role: .cancel, action: { - self.installOptionChosen = nil - self.installOptionContinuation?.resume() + installReplaceAlert.close(result: nil) }, label: { Text("lc.appList.abortInstallation".loc) }) @@ -204,17 +195,15 @@ struct LCAppListView : View, LCAppBannerDelegate { Text("lc.appList.installReplaceTip".loc) } .textFieldAlert( - isPresented: $webViewUrlInputOpened, + isPresented: $webViewUrlInput.show, title: "lc.appList.enterUrlTip".loc, - text: $webViewUrlInputContent, + text: $webViewUrlInput.initVal, placeholder: "scheme://", action: { newText in - self.webViewUrlInputContent = newText! - webViewUrlInputContinuation?.resume() + webViewUrlInput.close(result: newText) }, actionCancel: {_ in - self.webViewUrlInputContent = "" - webViewUrlInputContinuation?.resume() + webViewUrlInput.close(result: nil) } ) .fullScreenCover(isPresented: $webViewOpened) { @@ -227,42 +216,37 @@ struct LCAppListView : View, LCAppBannerDelegate { } func onOpenWebViewTapped() async { - await withCheckedContinuation { c in - webViewUrlInputOpened = true - webViewUrlInputContinuation = c + guard let urlToOpen = await webViewUrlInput.open(), urlToOpen != "" else { + return } - if webViewUrlInputContent == "" { - return - } - await openWebView(urlString: webViewUrlInputContent) - webViewUrlInputContent = "" + await openWebView(urlString: urlToOpen) } - func checkIfAppDelegateNeedOpenWebPage() async { - LCObjcBridge.openUrlStrFunc = openWebView; - if LCObjcBridge.urlStrToOpen != nil { - await self.openWebView(urlString: LCObjcBridge.urlStrToOpen!) - LCObjcBridge.urlStrToOpen = nil - } else if let urlStr = UserDefaults.standard.string(forKey: "webPageToOpen") { - UserDefaults.standard.removeObject(forKey: "webPageToOpen") - await self.openWebView(urlString: urlStr) + func onAppear() { + for app in apps { + app.delegate = self } + for app in hiddenApps { + app.delegate = self + } + + LCObjcBridge.setLaunchAppFunc(handler: launchAppWithBundleId) + LCObjcBridge.setOpenUrlStrFunc(handler: openWebView) } + func openWebView(urlString: String) async { guard var urlToOpen = URLComponents(string: urlString), urlToOpen.url != nil else { errorInfo = "lc.appList.urlInvalidError".loc errorShow = true - webViewUrlInputContent = "" return } - webViewUrlInputContent = "" if urlToOpen.scheme == nil || urlToOpen.scheme! == "" { urlToOpen.scheme = "https" } if urlToOpen.scheme != "https" && urlToOpen.scheme != "http" { - var appToLaunch : LCAppInfo? = nil + var appToLaunch : LCAppModel? = nil var appListsToConsider = [apps] if sharedModel.isHiddenAppUnlocked || !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") { appListsToConsider.append(hiddenApps) @@ -270,7 +254,7 @@ struct LCAppListView : View, LCAppBannerDelegate { appLoop: for appList in appListsToConsider { for app in appList { - if let schemes = app.urlSchemes() { + if let schemes = app.appInfo.urlSchemes() { for scheme in schemes { if let scheme = scheme as? String, scheme == urlToOpen.scheme { appToLaunch = app @@ -288,7 +272,7 @@ struct LCAppListView : View, LCAppBannerDelegate { return } - if appToLaunch.isHidden && !sharedModel.isHiddenAppUnlocked { + if appToLaunch.appInfo.isHidden && !sharedModel.isHiddenAppUnlocked { do { if !(try await LCUtils.authenticateUser()) { return @@ -300,7 +284,7 @@ struct LCAppListView : View, LCAppBannerDelegate { } } - UserDefaults.standard.setValue(appToLaunch.relativeBundlePath!, forKey: "selected") + UserDefaults.standard.setValue(appToLaunch.appInfo.relativeBundlePath!, forKey: "selected") UserDefaults.standard.setValue(urlToOpen.url!.absoluteString, forKey: "launchAppUrlScheme") LCUtils.launchToGuestApp() @@ -379,10 +363,10 @@ struct LCAppListView : View, LCAppBannerDelegate { var appRelativePath = "\(newAppInfo.bundleIdentifier()!).app" var outputFolder = LCPath.bundlePath.appendingPathComponent(appRelativePath) - var appToReplace : LCAppInfo? = nil + var appToReplace : LCAppModel? = nil // Folder exist! show alert for user to choose which bundle to replace let sameBundleIdApp = self.apps.filter { app in - return app.bundleIdentifier()! == newAppInfo.bundleIdentifier() + return app.appInfo.bundleIdentifier()! == newAppInfo.bundleIdentifier() } if fm.fileExists(atPath: outputFolder.path) || sameBundleIdApp.count > 0 { appRelativePath = "\(newAppInfo.bundleIdentifier()!)_\(Int(CFAbsoluteTimeGetCurrent())).app" @@ -390,17 +374,11 @@ struct LCAppListView : View, LCAppBannerDelegate { self.installOptions = [AppReplaceOption(isReplace: false, nameOfFolderToInstall: appRelativePath)] for app in sameBundleIdApp { - self.installOptions.append(AppReplaceOption(isReplace: true, nameOfFolderToInstall: app.relativeBundlePath, appToReplace: app)) + self.installOptions.append(AppReplaceOption(isReplace: true, nameOfFolderToInstall: app.appInfo.relativeBundlePath, appToReplace: app)) } - await withCheckedContinuation { c in - self.installOptionContinuation = c - self.installReplaceComfirmVisible = true - } - - - // user cancelled - guard let installOptionChosen = self.installOptionChosen else { + guard let installOptionChosen = await installReplaceAlert.open() else { + // user cancelled self.installprogressVisible = false try fm.removeItem(at: payloadPath) return @@ -411,7 +389,7 @@ struct LCAppListView : View, LCAppBannerDelegate { if installOptionChosen.isReplace { try fm.removeItem(at: outputFolder) self.apps.removeAll { appNow in - return appNow.relativeBundlePath == installOptionChosen.nameOfFolderToInstall + return appNow.appInfo.relativeBundlePath == installOptionChosen.nameOfFolderToInstall } } } @@ -441,25 +419,28 @@ struct LCAppListView : View, LCAppBannerDelegate { } // set data folder to the folder of the chosen app if let appToReplace = appToReplace { - finalNewApp.setDataUUID(appToReplace.getDataUUIDNoAssign()) + finalNewApp.setDataUUID(appToReplace.appInfo.getDataUUIDNoAssign()) } DispatchQueue.main.async { - self.apps.append(finalNewApp) + self.apps.append(LCAppModel(appInfo: finalNewApp)) self.installprogressVisible = false } } - func removeApp(app: LCAppInfo) { + func removeApp(app: LCAppModel) { DispatchQueue.main.async { self.apps.removeAll { now in return app == now } + self.hiddenApps.removeAll { now in + return app == now + } } } - func changeAppVisibility(app: LCAppInfo) { + func changeAppVisibility(app: LCAppModel) { DispatchQueue.main.async { - if app.isHidden { + if app.appInfo.isHidden { self.apps.removeAll { now in return app == now } @@ -474,22 +455,22 @@ struct LCAppListView : View, LCAppBannerDelegate { } - func onLaunchBundleIdChange() { - if sharedModel.bundleIdToLaunch == "" { + func launchAppWithBundleId(bundleId : String) async { + if bundleId == "" { return } - var appFound = false + var appFound : LCAppModel? = nil var isFoundAppHidden = false for app in apps { - if app.relativeBundlePath == sharedModel.bundleIdToLaunch { - appFound = true + if app.appInfo.relativeBundlePath == bundleId { + appFound = app break } } - if !appFound && !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") { + if appFound == nil && !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") { for app in hiddenApps { - if app.relativeBundlePath == sharedModel.bundleIdToLaunch { - appFound = true + if app.appInfo.relativeBundlePath == bundleId { + appFound = app isFoundAppHidden = true break } @@ -497,25 +478,30 @@ struct LCAppListView : View, LCAppBannerDelegate { } if isFoundAppHidden && !sharedModel.isHiddenAppUnlocked { - Task { - do { - let result = try await LCUtils.authenticateUser() - if !result { - sharedModel.bundleIdToLaunch = "" - } - } catch { - sharedModel.bundleIdToLaunch = "" - errorInfo = error.localizedDescription - errorShow = true + do { + let result = try await LCUtils.authenticateUser() + if !result { + return } - + } catch { + errorInfo = error.localizedDescription + errorShow = true } } - if !appFound { + guard let appFound else { errorInfo = "lc.appList.appNotFoundError".loc errorShow = true + return + } + + do { + try await appFound.runApp() + } catch { + errorInfo = error.localizedDescription + errorShow = true } + } func authenticateUser() async { diff --git a/LiveContainerSwiftUI/LCAppModel.swift b/LiveContainerSwiftUI/LCAppModel.swift new file mode 100644 index 0000000..f24f815 --- /dev/null +++ b/LiveContainerSwiftUI/LCAppModel.swift @@ -0,0 +1,136 @@ +import Foundation + +protocol LCAppModelDelegate { + func closeNavigationView() + func changeAppVisibility(app : LCAppModel) +} + +class LCAppModel: ObservableObject, Hashable { + + @Published var appInfo : LCAppInfo + + @Published var isAppRunning = false + @Published var isSigningInProgress = false + @Published var signProgress = 0.0 + private var observer : NSKeyValueObservation? + + @Published var uiIsJITNeeded : Bool + @Published var uiIsHidden : Bool + @Published var uiIsShared : Bool + @Published var uiDataFolder : String? + @Published var uiTweakFolder : String? + @Published var uiDoSymlinkInbox : Bool + @Published var uiBypassAssertBarrierOnQueue : Bool + + var jitAlert : YesNoHelper? = nil + + var delegate : LCAppModelDelegate? + + init(appInfo : LCAppInfo, delegate: LCAppModelDelegate? = nil) { + self.appInfo = appInfo + self.delegate = delegate + + self.uiIsJITNeeded = appInfo.isJITNeeded + self.uiIsHidden = appInfo.isHidden + self.uiIsShared = appInfo.isShared + self.uiDataFolder = appInfo.getDataUUIDNoAssign() + self.uiTweakFolder = appInfo.tweakFolder() + self.uiDoSymlinkInbox = appInfo.doSymlinkInbox + self.uiBypassAssertBarrierOnQueue = appInfo.bypassAssertBarrierOnQueue + } + + static func == (lhs: LCAppModel, rhs: LCAppModel) -> Bool { + return lhs === rhs + } + + func hash(into hasher: inout Hasher) { + hasher.combine(ObjectIdentifier(self)) + } + + func runApp() async throws{ + if isAppRunning { + return + } + + if let runningLC = LCUtils.getAppRunningLCScheme(bundleId: self.appInfo.relativeBundlePath) { + let openURL = URL(string: "\(runningLC)://livecontainer-launch?bundle-name=\(self.appInfo.relativeBundlePath!)")! + if await UIApplication.shared.canOpenURL(openURL) { + await UIApplication.shared.open(openURL) + return + } + } + isAppRunning = true + defer { + isAppRunning = false + } + try await signApp(force: false) + + UserDefaults.standard.set(self.appInfo.relativeBundlePath, forKey: "selected") + if appInfo.isJITNeeded { + await self.jitLaunch() + } else { + LCUtils.launchToGuestApp() + } + + isAppRunning = false + + } + + func forceResign() async throws { + if isAppRunning { + return + } + isAppRunning = true + defer { + isAppRunning = false + } + try await signApp(force: true) + } + + func signApp(force: Bool = false) async throws { + var signError : String? = nil + await withCheckedContinuation({ c in + appInfo.patchExecAndSignIfNeed(completionHandler: { error in + signError = error; + c.resume() + }, progressHandler: { signProgress in + guard let signProgress else { + return + } + self.isSigningInProgress = true + self.observer = signProgress.observe(\.fractionCompleted) { p, v in + DispatchQueue.main.async { + self.signProgress = signProgress.fractionCompleted + } + } + }, forceSign: force) + }) + self.isSigningInProgress = false + if let signError { + throw signError + } + } + + func jitLaunch() async { + LCUtils.askForJIT() + + guard let result = await jitAlert?.open(), result else { + UserDefaults.standard.removeObject(forKey: "selected") + return + } + LCUtils.launchToGuestApp() + + } + + func toggleHidden() async { + delegate?.closeNavigationView() + if appInfo.isHidden { + appInfo.isHidden = false + uiIsHidden = false + } else { + appInfo.isHidden = true + uiIsHidden = true + } + delegate?.changeAppVisibility(app: self) + } +} diff --git a/LiveContainerSwiftUI/LCAppSettingsView.swift b/LiveContainerSwiftUI/LCAppSettingsView.swift index a103d84..13d2f53 100644 --- a/LiveContainerSwiftUI/LCAppSettingsView.swift +++ b/LiveContainerSwiftUI/LCAppSettingsView.swift @@ -8,38 +8,6 @@ import Foundation import SwiftUI -protocol LCAppSettingDelegate { - func forceResign() async - func toggleHidden() async -} - -class LCAppModel: ObservableObject { - @Published var appInfo : LCAppInfo - - @Published var isAppRunning = false - - @Published var uiIsJITNeeded : Bool - @Published var uiIsHidden : Bool - @Published var uiIsShared : Bool - @Published var uiDataFolder : String? - @Published var uiTweakFolder : String? - @Published var uiDoSymlinkInbox : Bool - @Published var uiBypassAssertBarrierOnQueue : Bool - - init(appInfo : LCAppInfo) { - self.appInfo = appInfo - - self.uiIsJITNeeded = appInfo.isJITNeeded - self.uiIsHidden = appInfo.isHidden - self.uiIsShared = appInfo.isShared - self.uiDataFolder = appInfo.getDataUUIDNoAssign() - self.uiTweakFolder = appInfo.tweakFolder() - self.uiDoSymlinkInbox = appInfo.doSymlinkInbox - self.uiBypassAssertBarrierOnQueue = appInfo.bypassAssertBarrierOnQueue - } -} - - struct LCAppSettingsView : View{ private var appInfo : LCAppInfo @@ -53,30 +21,20 @@ struct LCAppSettingsView : View{ @State private var uiPickerDataFolder : String? @State private var uiPickerTweakFolder : String? - @State private var renameFolderShow = false - @State private var renameFolderContent = "" - @State private var renameFolerContinuation : CheckedContinuation? = nil - - @State private var confirmMoveToAppGroupShow = false - @State private var confirmMoveToAppGroup = false - @State private var confirmMoveToAppGroupContinuation : CheckedContinuation? = nil - - @State private var confirmMoveToPrivateDocShow = false - @State private var confirmMoveToPrivateDoc = false - @State private var confirmMoveToPrivateDocContinuation : CheckedContinuation? = nil + @StateObject private var renameFolderInput = InputHelper() + @StateObject private var moveToAppGroupAlert = YesNoHelper() + @StateObject private var moveToPrivateDocAlert = YesNoHelper() @State private var errorShow = false @State private var errorInfo = "" - private let delegate : LCAppSettingDelegate @EnvironmentObject private var sharedModel : SharedModel - init(model: LCAppModel, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>, delegate: LCAppSettingDelegate) { + init(model: LCAppModel, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>) { self.appInfo = model.appInfo self._model = ObservedObject(wrappedValue: model) _appDataFolders = appDataFolders _tweakFolders = tweakFolders - self.delegate = delegate self._uiPickerDataFolder = State(initialValue: model.uiDataFolder) self._uiPickerTweakFolder = State(initialValue: model.uiTweakFolder) } @@ -256,43 +214,37 @@ struct LCAppSettingsView : View{ } .textFieldAlert( - isPresented: $renameFolderShow, + isPresented: $renameFolderInput.show, title: "lc.common.enterNewFolderName".loc, - text: $renameFolderContent, + text: $renameFolderInput.initVal, placeholder: "", action: { newText in - self.renameFolderContent = newText! - renameFolerContinuation?.resume() + renameFolderInput.close(result: newText!) }, actionCancel: {_ in - self.renameFolderContent = "" - renameFolerContinuation?.resume() + renameFolderInput.close(result: "") } ) - .alert("lc.appSettings.toSharedApp".loc, isPresented: $confirmMoveToAppGroupShow) { + .alert("lc.appSettings.toSharedApp".loc, isPresented: $moveToAppGroupAlert.show) { Button { - self.confirmMoveToAppGroup = true - self.confirmMoveToAppGroupContinuation?.resume() + self.moveToAppGroupAlert.close(result: true) } label: { Text("lc.common.move".loc) } Button("lc.common.cancel".loc, role: .cancel) { - self.confirmMoveToAppGroup = false - self.confirmMoveToAppGroupContinuation?.resume() + self.moveToAppGroupAlert.close(result: false) } } message: { Text("lc.appSettings.toSharedAppDesc".loc) } - .alert("lc.appSettings.toPrivateApp".loc, isPresented: $confirmMoveToPrivateDocShow) { + .alert("lc.appSettings.toPrivateApp".loc, isPresented: $moveToPrivateDocAlert.show) { Button { - self.confirmMoveToPrivateDoc = true - self.confirmMoveToPrivateDocContinuation?.resume() + self.moveToPrivateDocAlert.close(result: true) } label: { Text("lc.common.move".loc) } Button("lc.common.cancel".loc, role: .cancel) { - self.confirmMoveToPrivateDoc = false - self.confirmMoveToPrivateDocContinuation?.resume() + self.moveToPrivateDocAlert.close(result: false) } } message: { Text("lc.appSettings.toPrivateAppDesc".loc) @@ -306,19 +258,11 @@ struct LCAppSettingsView : View{ } func createFolder() async { - - self.renameFolderContent = NSUUID().uuidString - - await withCheckedContinuation { c in - self.renameFolerContinuation = c - self.renameFolderShow = true - } - - if self.renameFolderContent == "" { + guard let newName = await renameFolderInput.open(initVal: NSUUID().uuidString), newName != "" else { return } let fm = FileManager() - let dest = LCPath.dataPath.appendingPathComponent(self.renameFolderContent) + let dest = LCPath.dataPath.appendingPathComponent(newName) do { try fm.createDirectory(at: dest, withIntermediateDirectories: false) } catch { @@ -327,8 +271,8 @@ struct LCAppSettingsView : View{ return } - self.appDataFolders.append(self.renameFolderContent) - self.setDataFolder(folderName: self.renameFolderContent) + self.appDataFolders.append(newName) + self.setDataFolder(folderName: newName) } @@ -337,17 +281,13 @@ struct LCAppSettingsView : View{ return } - self.renameFolderContent = self.model.uiDataFolder == nil ? "" : self.model.uiDataFolder! - await withCheckedContinuation { c in - self.renameFolerContinuation = c - self.renameFolderShow = true - } - if self.renameFolderContent == "" { + let initVal = self.model.uiDataFolder == nil ? "" : self.model.uiDataFolder! + guard let newName = await renameFolderInput.open(initVal: initVal), newName != "" else { return } let fm = FileManager() let orig = LCPath.dataPath.appendingPathComponent(appInfo.getDataUUIDNoAssign()) - let dest = LCPath.dataPath.appendingPathComponent(self.renameFolderContent) + let dest = LCPath.dataPath.appendingPathComponent(newName) do { try fm.moveItem(at: orig, to: dest) } catch { @@ -361,8 +301,8 @@ struct LCAppSettingsView : View{ return } - self.appDataFolders[i] = self.renameFolderContent - self.setDataFolder(folderName: self.renameFolderContent) + self.appDataFolders[i] = newName + self.setDataFolder(folderName: newName) } @@ -373,11 +313,7 @@ struct LCAppSettingsView : View{ } func moveToAppGroup() async { - await withCheckedContinuation { c in - confirmMoveToAppGroupContinuation = c - confirmMoveToAppGroupShow = true - } - if !confirmMoveToAppGroup { + guard let result = await moveToAppGroupAlert.open(), result else { return } @@ -417,11 +353,7 @@ struct LCAppSettingsView : View{ return } - await withCheckedContinuation { c in - confirmMoveToPrivateDocContinuation = c - confirmMoveToPrivateDocShow = true - } - if !confirmMoveToPrivateDoc { + guard let result = await moveToPrivateDocAlert.open(), result else { return } @@ -467,10 +399,15 @@ struct LCAppSettingsView : View{ model.uiBypassAssertBarrierOnQueue = enabled } func toggleHidden() async { - await delegate.toggleHidden() + await model.toggleHidden() } func forceResign() async { - await delegate.forceResign() + do { + try await model.forceResign() + } catch { + errorInfo = error.localizedDescription + errorShow = true + } } } diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index 94a8feb..29c96e8 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -14,18 +14,14 @@ struct LCSettingsView: View { @State var successShow = false @State var successInfo = "" - @Binding var apps: [LCAppInfo] - @Binding var hiddenApps: [LCAppInfo] + @Binding var apps: [LCAppModel] + @Binding var hiddenApps: [LCAppModel] @Binding var appDataFolderNames: [String] - @State private var confirmAppFolderRemovalShow = false - @State private var confirmAppFolderRemoval = false - @State private var appFolderRemovalContinuation : CheckedContinuation? = nil + @StateObject private var appFolderRemovalAlert = YesNoHelper() @State private var folderRemoveCount = 0 - @State private var confirmKeyChainRemovalShow = false - @State private var confirmKeyChainRemoval = false - @State private var confirmKeyChainContinuation : CheckedContinuation? = nil + @StateObject private var keyChainRemovalAlert = YesNoHelper() @State var isJitLessEnabled = false @@ -40,7 +36,7 @@ struct LCSettingsView: View { @EnvironmentObject private var sharedModel : SharedModel - init(apps: Binding<[LCAppInfo]>, hiddenApps: Binding<[LCAppInfo]>, appDataFolderNames: Binding<[String]>) { + init(apps: Binding<[LCAppModel]>, hiddenApps: Binding<[LCAppModel]>, appDataFolderNames: Binding<[String]>) { _isJitLessEnabled = State(initialValue: LCUtils.certificatePassword() != nil) _isAltCertIgnored = State(initialValue: UserDefaults.standard.bool(forKey: "LCIgnoreALTCertificate")) _frameShortIcon = State(initialValue: UserDefaults.standard.bool(forKey: "LCFrameShortcutIcons")) @@ -239,19 +235,17 @@ struct LCSettingsView: View { } message: { Text(successInfo) } - .alert("lc.settings.cleanDataFolder".loc, isPresented: $confirmAppFolderRemovalShow) { + .alert("lc.settings.cleanDataFolder".loc, isPresented: $appFolderRemovalAlert.show) { if folderRemoveCount > 0 { Button(role: .destructive) { - self.confirmAppFolderRemoval = true - self.appFolderRemovalContinuation?.resume() + appFolderRemovalAlert.close(result: true) } label: { Text("lc.common.delete".loc) } } Button("lc.common.cancel".loc, role: .cancel) { - self.confirmAppFolderRemoval = false - self.appFolderRemovalContinuation?.resume() + appFolderRemovalAlert.close(result: false) } } message: { if folderRemoveCount > 0 { @@ -261,17 +255,15 @@ struct LCSettingsView: View { } } - .alert("lc.settings.cleanKeychain".loc, isPresented: $confirmKeyChainRemovalShow) { + .alert("lc.settings.cleanKeychain".loc, isPresented: $keyChainRemovalAlert.show) { Button(role: .destructive) { - self.confirmKeyChainRemoval = true - self.confirmKeyChainContinuation?.resume() + keyChainRemovalAlert.close(result: true) } label: { Text("lc.common.delete".loc) } Button("lc.common.cancel".loc, role: .cancel) { - self.confirmKeyChainRemoval = false - self.confirmKeyChainContinuation?.resume() + keyChainRemovalAlert.close(result: false) } } message: { Text("lc.settings.cleanKeychainDesc".loc) @@ -350,15 +342,15 @@ struct LCSettingsView: View { func cleanUpUnusedFolders() async { - var folderNameToAppDict : [String:LCAppInfo] = [:] + var folderNameToAppDict : [String:LCAppModel] = [:] for app in apps { - guard let folderName = app.getDataUUIDNoAssign() else { + guard let folderName = app.appInfo.getDataUUIDNoAssign() else { continue } folderNameToAppDict[folderName] = app } for app in hiddenApps { - guard let folderName = app.getDataUUIDNoAssign() else { + guard let folderName = app.appInfo.getDataUUIDNoAssign() else { continue } folderNameToAppDict[folderName] = app @@ -371,13 +363,8 @@ struct LCSettingsView: View { } } folderRemoveCount = foldersToDelete.count - await withCheckedContinuation { c in - self.appFolderRemovalContinuation = c - DispatchQueue.main.async { - confirmAppFolderRemovalShow = true - } - } - if !confirmAppFolderRemoval { + + guard let result = await appFolderRemovalAlert.open(), result else { return } do { @@ -396,13 +383,7 @@ struct LCSettingsView: View { } func removeKeyChain() async { - await withCheckedContinuation { c in - self.confirmKeyChainContinuation = c - DispatchQueue.main.async { - confirmKeyChainRemovalShow = true - } - } - if !confirmKeyChainRemoval { + guard let result = await keyChainRemovalAlert.open(), result else { return } @@ -425,26 +406,26 @@ struct LCSettingsView: View { var appDataFoldersInUse : Set = Set(); var tweakFoldersInUse : Set = Set(); for app in apps { - if !app.isShared { + if !app.appInfo.isShared { continue } - if let folder = app.getDataUUIDNoAssign() { + if let folder = app.appInfo.getDataUUIDNoAssign() { appDataFoldersInUse.update(with: folder); } - if let folder = app.tweakFolder() { + if let folder = app.appInfo.tweakFolder() { tweakFoldersInUse.update(with: folder); } } for app in hiddenApps { - if !app.isShared { + if !app.appInfo.isShared { continue } - if let folder = app.getDataUUIDNoAssign() { + if let folder = app.appInfo.getDataUUIDNoAssign() { appDataFoldersInUse.update(with: folder); } - if let folder = app.tweakFolder() { + if let folder = app.appInfo.tweakFolder() { tweakFoldersInUse.update(with: folder); } diff --git a/LiveContainerSwiftUI/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift index 274f578..ee95df5 100644 --- a/LiveContainerSwiftUI/LCTabView.swift +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -9,8 +9,8 @@ import Foundation import SwiftUI struct LCTabView: View { - @State var apps: [LCAppInfo] - @State var hiddenApps: [LCAppInfo] + @State var apps: [LCAppModel] + @State var hiddenApps: [LCAppModel] @State var appDataFolderNames: [String] @State var tweakFolderNames: [String] @@ -22,8 +22,8 @@ struct LCTabView: View { var tempAppDataFolderNames : [String] = [] var tempTweakFolderNames : [String] = [] - var tempApps: [LCAppInfo] = [] - var tempHiddenApps: [LCAppInfo] = [] + var tempApps: [LCAppModel] = [] + var tempHiddenApps: [LCAppModel] = [] do { // load apps @@ -37,9 +37,9 @@ struct LCTabView: View { newApp.relativeBundlePath = appDir newApp.isShared = false if newApp.isHidden { - tempHiddenApps.append(newApp) + tempHiddenApps.append(LCAppModel(appInfo: newApp)) } else { - tempApps.append(newApp) + tempApps.append(LCAppModel(appInfo: newApp)) } } @@ -53,9 +53,9 @@ struct LCTabView: View { newApp.relativeBundlePath = appDir newApp.isShared = true if newApp.isHidden { - tempHiddenApps.append(newApp) + tempHiddenApps.append(LCAppModel(appInfo: newApp)) } else { - tempApps.append(newApp) + tempApps.append(LCAppModel(appInfo: newApp)) } } // load document folders diff --git a/LiveContainerSwiftUI/LCTweaksView.swift b/LiveContainerSwiftUI/LCTweaksView.swift index 8f16af6..4c79c81 100644 --- a/LiveContainerSwiftUI/LCTweaksView.swift +++ b/LiveContainerSwiftUI/LCTweaksView.swift @@ -25,13 +25,9 @@ struct LCTweakFolderView : View { @State private var errorShow = false @State private var errorInfo = "" - @State private var newFolderShow = false - @State private var newFolderContent = "" - @State private var newFolerContinuation : CheckedContinuation? = nil + @StateObject private var newFolderInput = InputHelper() - @State private var renameFileShow = false - @State private var renameFileContent = "" - @State private var renameFileContinuation : CheckedContinuation? = nil + @StateObject private var renameFileInput = InputHelper() @State private var choosingTweak = false @@ -169,31 +165,27 @@ struct LCTweakFolderView : View { Text(errorInfo) } .textFieldAlert( - isPresented: $newFolderShow, + isPresented: $newFolderInput.show, title: "lc.common.enterNewFolderName".loc, - text: $newFolderContent, + text: $newFolderInput.initVal, placeholder: "", action: { newText in - self.newFolderContent = newText! - newFolerContinuation?.resume() + newFolderInput.close(result: newText) }, actionCancel: {_ in - self.newFolderContent = "" - newFolerContinuation?.resume() + newFolderInput.close(result: "") } ) .textFieldAlert( - isPresented: $renameFileShow, + isPresented: $renameFileInput.show, title: "lc.common.enterNewName".loc, - text: $renameFileContent, + text: $renameFileInput.initVal, placeholder: "", action: { newText in - self.renameFileContent = newText! - renameFileContinuation?.resume() + renameFileInput.close(result: newText) }, actionCancel: {_ in - self.renameFileContent = "" - renameFileContinuation?.resume() + renameFileInput.close(result: "") } ) .fileImporter(isPresented: $choosingTweak, allowedContentTypes: [.dylib, .lcFramework, .deb], allowsMultipleSelection: true) { result in @@ -253,14 +245,7 @@ struct LCTweakFolderView : View { } func renameTweakItem(tweakItem: LCTweakItem) async { - self.renameFileContent = tweakItem.fileUrl.lastPathComponent - - await withCheckedContinuation { c in - self.renameFileContinuation = c - self.renameFileShow = true - } - - if self.renameFileContent == "" { + guard let newName = await renameFileInput.open(initVal: tweakItem.fileUrl.lastPathComponent), newName != "" else { return } @@ -270,7 +255,7 @@ struct LCTweakFolderView : View { guard let indexToRename = indexToRename else { return } - let newUrl = self.baseUrl.appendingPathComponent(self.renameFileContent) + let newUrl = self.baseUrl.appendingPathComponent(newName) let fm = FileManager() do { @@ -290,7 +275,7 @@ struct LCTweakFolderView : View { return } tweakFolders.remove(at: indexToRename2) - tweakFolders.insert(self.renameFileContent, at: indexToRename2) + tweakFolders.insert(newName, at: indexToRename2) } } @@ -341,18 +326,11 @@ struct LCTweakFolderView : View { } func createNewFolder() async { - self.newFolderContent = "" - - await withCheckedContinuation { c in - self.newFolerContinuation = c - self.newFolderShow = true - } - - if self.newFolderContent == "" { + guard let newName = await renameFileInput.open(), newName != "" else { return } let fm = FileManager() - let dest = baseUrl.appendingPathComponent(self.newFolderContent) + let dest = baseUrl.appendingPathComponent(newName) do { try fm.createDirectory(at: dest, withIntermediateDirectories: false) } catch { @@ -362,7 +340,7 @@ struct LCTweakFolderView : View { } tweakItems.append(LCTweakItem(fileUrl: dest, isFolder: true, isFramework: false, isTweak: false)) if isRoot { - tweakFolders.append(self.newFolderContent) + tweakFolders.append(newName) } } diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift index ae89465..3c65239 100644 --- a/LiveContainerSwiftUI/LCWebView.swift +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -17,21 +17,18 @@ struct LCWebView: View { @State private var uiLoadStatus = 0.0 @State private var pageTitle = "" - @Binding var apps : [LCAppInfo] - @Binding var hiddenApps : [LCAppInfo] + @Binding var apps : [LCAppModel] + @Binding var hiddenApps : [LCAppModel] - @State private var runAppAlertShow = false + @State private var runAppAlert = YesNoHelper() @State private var runAppAlertMsg = "" - @State private var doRunApp = false - @State private var renameFolderContent = "" - @State private var doRunAppContinuation : CheckedContinuation? = nil @State private var errorShow = false @State private var errorInfo = "" @EnvironmentObject private var sharedModel : SharedModel - init(url: Binding, apps: Binding<[LCAppInfo]>, hiddenApps: Binding<[LCAppInfo]>, isPresent: Binding) { + init(url: Binding, apps: Binding<[LCAppModel]>, hiddenApps: Binding<[LCAppModel]>, isPresent: Binding) { self.webView = WebView() self._url = url self._apps = apps @@ -101,14 +98,12 @@ struct LCWebView: View { } } - .alert("lc.webView.runApp".loc, isPresented: $runAppAlertShow) { + .alert("lc.webView.runApp".loc, isPresented: $runAppAlert.show) { Button("lc.appBanner.run".loc, action: { - self.doRunApp = true - self.doRunAppContinuation?.resume() + runAppAlert.close(result: true) }) Button("lc.common.cancel".loc, role: .cancel, action: { - self.doRunApp = false - self.doRunAppContinuation?.resume() + runAppAlert.close(result: false) }) } message: { Text(runAppAlertMsg) @@ -148,7 +143,7 @@ struct LCWebView: View { } public func onURLSchemeDetected(url: URL) async { - var appToLaunch : LCAppInfo? = nil + var appToLaunch : LCAppModel? = nil var appListsToConsider = [apps] if sharedModel.isHiddenAppUnlocked || !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") { appListsToConsider.append(hiddenApps) @@ -156,7 +151,7 @@ struct LCWebView: View { appLoop: for appList in appListsToConsider { for app in appList { - if let schemes = app.urlSchemes() { + if let schemes = app.appInfo.urlSchemes() { for scheme in schemes { if let scheme = scheme as? String, scheme == url.scheme { appToLaunch = app @@ -174,7 +169,7 @@ struct LCWebView: View { return } - if appToLaunch.isHidden && !sharedModel.isHiddenAppUnlocked { + if appToLaunch.appInfo.isHidden && !sharedModel.isHiddenAppUnlocked { do { if !(try await LCUtils.authenticateUser()) { @@ -187,33 +182,28 @@ struct LCWebView: View { } } - runAppAlertMsg = "lc.webView.pageLaunch %@".localizeWithFormat(appToLaunch.displayName()!) + runAppAlertMsg = "lc.webView.pageLaunch %@".localizeWithFormat(appToLaunch.appInfo.displayName()!) - await withCheckedContinuation { c in - self.doRunAppContinuation = c - runAppAlertShow = true - } - - if !doRunApp { + if let doRunApp = await runAppAlert.open(), !doRunApp { return } - launchToApp(bundleId: appToLaunch.relativeBundlePath!, url: url) + launchToApp(bundleId: appToLaunch.appInfo.relativeBundlePath!, url: url) } public func onUniversalLinkDetected(url: URL, bundleIDs: [String]) async { - var bundleIDToAppDict: [String: LCAppInfo] = [:] + var bundleIDToAppDict: [String: LCAppModel] = [:] for app in apps { - bundleIDToAppDict[app.bundleIdentifier()!] = app + bundleIDToAppDict[app.appInfo.bundleIdentifier()!] = app } if !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") || sharedModel.isHiddenAppUnlocked { for app in hiddenApps { - bundleIDToAppDict[app.bundleIdentifier()!] = app + bundleIDToAppDict[app.appInfo.bundleIdentifier()!] = app } } - var appToLaunch: LCAppInfo? = nil + var appToLaunch: LCAppModel? = nil for bundleID in bundleIDs { if let app = bundleIDToAppDict[bundleID] { appToLaunch = app @@ -224,7 +214,7 @@ struct LCWebView: View { return } - if appToLaunch.isHidden && !sharedModel.isHiddenAppUnlocked { + if appToLaunch.appInfo.isHidden && !sharedModel.isHiddenAppUnlocked { do { if !(try await LCUtils.authenticateUser()) { return @@ -236,16 +226,11 @@ struct LCWebView: View { } } - runAppAlertMsg = "lc.webView.pageCanBeOpenIn %@".localizeWithFormat(appToLaunch.displayName()!) - runAppAlertShow = true - await withCheckedContinuation { c in - self.doRunAppContinuation = c - runAppAlertShow = true - } - if !doRunApp { + runAppAlertMsg = "lc.webView.pageCanBeOpenIn %@".localizeWithFormat(appToLaunch.appInfo.displayName()!) + if let doRunApp = await runAppAlert.open(), !doRunApp { return } - launchToApp(bundleId: appToLaunch.relativeBundlePath!, url: url) + launchToApp(bundleId: appToLaunch.appInfo.relativeBundlePath!, url: url) } } diff --git a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj index c05bbad..55c7be9 100644 --- a/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -19,6 +19,7 @@ 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 173564C82C76FE3500C6C918 /* Assets.xcassets */; }; 173F18402C7B7B74002953AA /* LCWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173F183F2C7B7B74002953AA /* LCWebView.swift */; }; 178B4C3E2C77654400DD1F74 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178B4C3D2C77654400DD1F74 /* Shared.swift */; }; + 17A7640C2C9D1B6C00456519 /* LCAppModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17A7640B2C9D1B6C00456519 /* LCAppModel.swift */; }; 17C536F42C98529D006C2C75 /* LCAppSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17C536F32C98529D006C2C75 /* LCAppSettingsView.swift */; }; /* End PBXBuildFile section */ @@ -37,6 +38,7 @@ 173F183F2C7B7B74002953AA /* LCWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCWebView.swift; sourceTree = ""; }; 178B4C3D2C77654400DD1F74 /* Shared.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shared.swift; sourceTree = ""; }; 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "LiveContainerSwiftUI-Bridging-Header.h"; sourceTree = ""; }; + 17A7640B2C9D1B6C00456519 /* LCAppModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCAppModel.swift; sourceTree = ""; }; 17B9B88D2C760678009D079E /* LiveContainerSwiftUI.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = LiveContainerSwiftUI.app; sourceTree = BUILT_PRODUCTS_DIR; }; 17C536F32C98529D006C2C75 /* LCAppSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LCAppSettingsView.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -70,6 +72,7 @@ 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */, 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */, 170C3DF82C99A489007F86FB /* Localizable.xcstrings */, + 17A7640B2C9D1B6C00456519 /* LCAppModel.swift */, ); name = LiveContainerSwiftUI; sourceTree = ""; @@ -166,6 +169,7 @@ 173564D22C76FE3500C6C918 /* LCAppBanner.swift in Sources */, 173F18402C7B7B74002953AA /* LCWebView.swift in Sources */, 173564CE2C76FE3500C6C918 /* Makefile in Sources */, + 17A7640C2C9D1B6C00456519 /* LCAppModel.swift in Sources */, 17C536F42C98529D006C2C75 /* LCAppSettingsView.swift in Sources */, 173564CF2C76FE3500C6C918 /* LCSwiftBridge.m in Sources */, 173564C92C76FE3500C6C918 /* LCAppListView.swift in Sources */, diff --git a/LiveContainerSwiftUI/ObjcBridge.swift b/LiveContainerSwiftUI/ObjcBridge.swift index dee7dd0..5a76e13 100644 --- a/LiveContainerSwiftUI/ObjcBridge.swift +++ b/LiveContainerSwiftUI/ObjcBridge.swift @@ -10,8 +10,29 @@ import SwiftUI @objc public class LCObjcBridge: NSObject { - public static var urlStrToOpen: String? = nil - public static var openUrlStrFunc: ((String) async -> Void)? + private static var urlStrToOpen: String? = nil + private static var openUrlStrFunc: ((String) async -> Void)? + private static var bundleToLaunch: String? = nil + private static var launchAppFunc: ((String) async -> Void)? + + public static func setOpenUrlStrFunc(handler: @escaping ((String) async -> Void)){ + self.openUrlStrFunc = handler + if let urlStrToOpen = self.urlStrToOpen { + Task { await handler(urlStrToOpen) } + self.urlStrToOpen = nil + } else if let urlStr = UserDefaults.standard.string(forKey: "webPageToOpen") { + UserDefaults.standard.removeObject(forKey: "webPageToOpen") + Task { await handler(urlStr) } + } + } + + public static func setLaunchAppFunc(handler: @escaping ((String) async -> Void)){ + self.launchAppFunc = handler + if let bundleToLaunch = self.bundleToLaunch { + Task { await handler(bundleToLaunch) } + self.bundleToLaunch = nil + } + } @objc public static func openWebPage(urlStr: String) { if openUrlStrFunc == nil { @@ -22,7 +43,11 @@ import SwiftUI } @objc public static func launchApp(bundleId: String) { - DataManager.shared.model.bundleIdToLaunch = bundleId + if launchAppFunc == nil { + bundleToLaunch = bundleId + } else { + Task { await launchAppFunc!(bundleId) } + } } @objc public static func getRootVC() -> UIViewController { diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index 1575202..80e109d 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -52,7 +52,6 @@ struct LCPath { } class SharedModel: ObservableObject { - @Published var bundleIdToLaunch: String = "" @Published var isHiddenAppUnlocked = false } @@ -61,6 +60,42 @@ class DataManager { let model = SharedModel() } +class AlertHelper : ObservableObject { + @Published var show = false + private var result : T? + private var c : CheckedContinuation? = nil + + func open() async -> T? { + await withCheckedContinuation { c in + self.c = c + DispatchQueue.main.async { + self.show = true + } + } + return self.result + } + + func close(result: T?) { + self.result = result + c?.resume() + } +} + +typealias YesNoHelper = AlertHelper + +class InputHelper : AlertHelper { + @Published var initVal = "" + + func open(initVal: String) async -> String? { + self.initVal = initVal + return await super.open() + } + + override func open() async -> String? { + self.initVal = "" + return await super.open() + } +} extension String: LocalizedError { public var errorDescription: String? { return self } diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index e52dce0..bcb13f8 100644 --- a/LiveContainerUI/LCUtils.m +++ b/LiveContainerUI/LCUtils.m @@ -56,11 +56,11 @@ + (NSData *)certificateDataFile { } + (NSData *)certificateDataProperty { - NSData* ans = [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificateData"]; + NSData* ans = [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificateData"]; if(ans) { return ans; } else { - return [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificateData"]; + return [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificateData"]; } } @@ -72,11 +72,11 @@ + (NSData *)certificateData { + (NSString *)certificatePassword { if (self.certificateDataFile) { - NSString* ans = [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificatePassword"]; + NSString* ans = [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificatePassword"]; if(ans) { return ans; } - return [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificatePassword"]; + return [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificatePassword"]; } else if (self.certificateDataProperty) { return @""; } else {

4y5`Wm>kIpO(9 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%0A+(xEv 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 L1<6VY!{Gk_z8=3{ literal 0 HcmV?d00001 diff --git a/TweakLoader/Makefile b/TweakLoader/Makefile index 1105f0f..6a9f22e 100644 --- a/TweakLoader/Makefile +++ b/TweakLoader/Makefile @@ -6,7 +6,7 @@ include $(THEOS)/makefiles/common.mk LIBRARY_NAME = TweakLoader -TweakLoader_FILES = TweakLoader.m NSBundle+FixCydiaSubstrate.m NSFileManager+GuestHooks.m UIKit+GuestHooks.m utils.m +TweakLoader_FILES = TweakLoader.m NSBundle+FixCydiaSubstrate.m NSFileManager+GuestHooks.m UIKit+GuestHooks.m utils.m NSUserDefaults.m TweakLoader_CFLAGS = -objc-arc TweakLoader_INSTALL_PATH = /Applications/LiveContainer.app/Frameworks diff --git a/TweakLoader/NSUserDefaults.m b/TweakLoader/NSUserDefaults.m new file mode 100644 index 0000000..f99a03d --- /dev/null +++ b/TweakLoader/NSUserDefaults.m @@ -0,0 +1,25 @@ +// +// NSUserDefaults.m +// jump +// +// Created by s s on 2024/9/2. +// + +@import Foundation; +#import "utils.h" + +__attribute__((constructor)) +static void NSUDGuestHooksInit() { + swizzle(NSUserDefaults.class, @selector(_container), @selector(hook__container)); +} + +// NSFileManager simulate app group +@implementation NSUserDefaults(LiveContainerHooks) + +- (CFStringRef)hook__container { + const char *homeDir = getenv("HOME"); + CFStringRef cfHomeDir = CFStringCreateWithCString(NULL, homeDir, kCFStringEncodingUTF8); + return cfHomeDir; +} + +@end diff --git a/main.m b/main.m index 258717a..b246785 100644 --- a/main.m +++ b/main.m @@ -186,12 +186,35 @@ static void overwriteExecPath(NSString *bundlePath) { NSFileManager *fm = NSFileManager.defaultManager; NSString *docPath = [fm URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] .lastObject.path; + + NSURL *appGroupFolder = nil; + NSString *bundlePath = [NSString stringWithFormat:@"%@/Applications/%@", docPath, selectedApp]; NSBundle *appBundle = [[NSBundle alloc] initWithPath:bundlePath]; + bool isSharedBundle = false; + if (!appBundle) { + NSString *appGroupID = [NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"] firstObject]; + NSURL *appGroupPath = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:appGroupID]; + appGroupFolder = [appGroupPath URLByAppendingPathComponent:@"LiveContainer"]; + + NSString *bundlePath2 = [NSString stringWithFormat:@"%@/Applications/%@", appGroupFolder.path, selectedApp]; + appBundle = [[NSBundle alloc] initWithPath:bundlePath2]; + isSharedBundle = true; + } + + if(!appBundle) { + return @"App not found"; + } + NSError *error; // Setup tweak loader - NSString *tweakFolder = [docPath stringByAppendingPathComponent:@"Tweaks"]; + NSString *tweakFolder = nil; + if (isSharedBundle) { + tweakFolder = [appGroupFolder.path stringByAppendingPathComponent:@"Tweaks"]; + } else { + tweakFolder = [docPath stringByAppendingPathComponent:@"Tweaks"]; + } setenv("LC_GLOBAL_TWEAKS_FOLDER", tweakFolder.UTF8String, 1); // Update TweakLoader symlink @@ -249,7 +272,14 @@ static void overwriteExecPath(NSString *bundlePath) { } // Overwrite home and tmp path - NSString *newHomePath = [NSString stringWithFormat:@"%@/Data/Application/%@", docPath, dataUUID]; + NSString *newHomePath = nil; + if(isSharedBundle) { + newHomePath = [NSString stringWithFormat:@"%@/Data/Application/%@", appGroupFolder.path, dataUUID]; + } else { + newHomePath = [NSString stringWithFormat:@"%@/Data/Application/%@", docPath, dataUUID]; + } + + NSString *newTmpPath = [newHomePath stringByAppendingPathComponent:@"tmp"]; remove(newTmpPath.UTF8String); symlink(getenv("TMPDIR"), newTmpPath.UTF8String); From 97894ff20deec7a7d6190b7bc442089840ce81d5 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 3 Sep 2024 13:04:05 +0800 Subject: [PATCH 14/36] open apps in multiple lcs --- LCSharedUtils.m | 3 +- LiveContainerSwiftUI/LCSettingsView.swift | 39 ++++++++++++++++++----- LiveContainerUI/LCAppInfo.m | 2 +- LiveContainerUI/LCUtils.h | 1 + LiveContainerUI/LCUtils.m | 4 +++ Resources/Info.plist | 2 ++ TweakLoader/UIKit+GuestHooks.m | 36 ++++++++++++++++----- TweakLoader/utils.h | 1 + main.m | 7 +++- 9 files changed, 76 insertions(+), 19 deletions(-) diff --git a/LCSharedUtils.m b/LCSharedUtils.m index d3a8ff1..11566df 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -2,6 +2,7 @@ #import "UIKitPrivate.h" extern NSUserDefaults *lcUserDefaults; +extern NSString *lcAppUrlScheme; @implementation LCSharedUtils + (NSString *)certificatePassword { @@ -22,7 +23,7 @@ + (BOOL)launchToGuestApp { urlScheme = @"apple-magnifier://enable-jit?bundle-id=%@"; } else if (self.certificatePassword) { tries = 8; - urlScheme = @"livecontainer://livecontainer-relaunch"; + urlScheme = [NSString stringWithFormat:@"%@://livecontainer-relaunch", lcAppUrlScheme]; } else { urlScheme = @"sidestore://sidejit-enable?bid=%@"; } diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index 9f8a8d7..ff8483d 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -21,6 +21,9 @@ struct LCSettingsView: View { @State private var folderRemoveCount = 0 @State var isJitLessEnabled = false + // 0= not installed, 1= is installed, 2=current liveContainer is the second one + @State var multipleLiveContainerStatus = 0 + @State var isAltCertIgnored = false @State var frameShortIcon = false @State var silentSwitchApp = false @@ -35,6 +38,13 @@ struct LCSettingsView: View { _apps = apps _appDataFolderNames = appDataFolderNames + if LCUtils.appUrlScheme()?.lowercased() != "livecontainer" { + _multipleLiveContainerStatus = State(initialValue: 2) + } else if UIApplication.shared.canOpenURL(URL(string: "livecontainer2://")!) { + _multipleLiveContainerStatus = State(initialValue: 1) + } else { + _multipleLiveContainerStatus = State(initialValue: 0) + } } var body: some View { @@ -51,20 +61,33 @@ struct LCSettingsView: View { Text("Setup JIT-less certificate") } } - if isJitLessEnabled { - Button { - installAnotherLC() - } label: { + } header: { + Text("JIT-Less") + } footer: { + Text("JIT-less allows you to use LiveContainer without having to enable JIT. Requires AltStore or SideStore.") + } + + Section{ + Button { + installAnotherLC() + } label: { + if multipleLiveContainerStatus == 0 { Text("Install another LiveContainer") + } else if multipleLiveContainerStatus == 1 { + Text("Second LiveContainer Already Installed") + } else if multipleLiveContainerStatus == 2 { + Text("This is the second LiveContainer") } - } - + } + .disabled(multipleLiveContainerStatus > 0) } header: { - Text("JIT-Less") + Text("Multiple LiveContainers") } footer: { - Text("JIT-less allows you to use LiveContainer without having to enable JIT. Requires AltStore or SideStore.") + Text("By installing multiple LiveContainers, and converting apps to Shared Apps, you can open one app among all LiveContainers with most of its data and settings.") } + + Section { Toggle(isOn: $isAltCertIgnored) { Text("Ignore ALTCertificate.p12") diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index 1df7017..2b68e3b 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -145,7 +145,7 @@ - (NSDictionary *)generateWebClipConfig { @"PayloadVersion": @(1), @"Precomposed": @NO, @"toPayloadOrganization": @"LiveContainer", - @"URL": [NSString stringWithFormat:@"livecontainer://livecontainer-launch?bundle-name=%@", self.bundlePath.lastPathComponent] + @"URL": [NSString stringWithFormat:@"%@://livecontainer-launch?bundle-name=%@", [LCUtils appUrlScheme], self.bundlePath.lastPathComponent] }; return @{ @"ConsentText": @{ diff --git a/LiveContainerUI/LCUtils.h b/LiveContainerUI/LCUtils.h index 9f0d180..ff824d8 100644 --- a/LiveContainerUI/LCUtils.h +++ b/LiveContainerUI/LCUtils.h @@ -31,6 +31,7 @@ void LCPatchExecSlice(const char *path, struct mach_header_64 *header); + (BOOL)isAppGroupAltStoreLike; + (NSString *)appGroupID; ++ (NSString *)appUrlScheme; + (NSString *)appGroupPath; + (NSString *)storeInstallURLScheme; + (NSString *)getVersionInfo; diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index e6d4cfb..4500c1f 100644 --- a/LiveContainerUI/LCUtils.m +++ b/LiveContainerUI/LCUtils.m @@ -217,6 +217,10 @@ + (NSString *)appGroupID { return [NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"] firstObject]; } ++ (NSString *)appUrlScheme { + return NSBundle.mainBundle.infoDictionary[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0]; +} + + (BOOL)isAppGroupAltStoreLike { if (self.appGroupID.length == 0) return NO; return [NSFileManager.defaultManager fileExistsAtPath:self.storeBundlePath.path]; diff --git a/Resources/Info.plist b/Resources/Info.plist index ad76f29..1f6bb6d 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -65,6 +65,8 @@ LSApplicationQueriesSchemes sidestore + livecontainer2 + livecontainer3 LSRequiresIPhoneOS diff --git a/TweakLoader/UIKit+GuestHooks.m b/TweakLoader/UIKit+GuestHooks.m index 8ccb5af..18a12ac 100644 --- a/TweakLoader/UIKit+GuestHooks.m +++ b/TweakLoader/UIKit+GuestHooks.m @@ -23,6 +23,16 @@ void LCShowSwitchAppConfirmation(NSURL *url) { window.windowScene = nil; }]; [alert addAction:okAction]; + if([NSUserDefaults.lcAppUrlScheme isEqualToString:@"livecontainer"] && [UIApplication.sharedApplication canOpenURL:[NSURL URLWithString: @"livecontainer2://"]]) { + UIAlertAction* openlc2Action = [UIAlertAction actionWithTitle:@"Open In LiveContainer2" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + NSURLComponents* newUrlComp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + [newUrlComp setScheme:@"livecontainer2"]; + [UIApplication.sharedApplication openURL:[newUrlComp URL] options:@{} completionHandler:nil]; + window.windowScene = nil; + }]; + [alert addAction:openlc2Action]; + } + UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { window.windowScene = nil; }]; @@ -66,6 +76,16 @@ void LCOpenWebPage(NSString* webPageUrlString) { openUniversalLink(webPageUrlString); window.windowScene = nil; }]; + if([NSUserDefaults.lcAppUrlScheme isEqualToString:@"livecontainer"] && [UIApplication.sharedApplication canOpenURL:[NSURL URLWithString: @"livecontainer2://"]]) { + UIAlertAction* openlc2Action = [UIAlertAction actionWithTitle:@"Open In LiveContainer2" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { + NSURLComponents* newUrlComp = [NSURLComponents componentsWithString:webPageUrlString]; + [newUrlComp setScheme:@"livecontainer2"]; + [UIApplication.sharedApplication openURL:[newUrlComp URL] options:@{} completionHandler:nil]; + window.windowScene = nil; + }]; + [alert addAction:openlc2Action]; + } + [alert addAction:openNowAction]; UIAlertAction* cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction * action) { window.windowScene = nil; @@ -85,10 +105,10 @@ void LCOpenWebPage(NSString* webPageUrlString) { @implementation UIApplication(LiveContainerHook) - (void)hook__applicationOpenURLAction:(id)action payload:(NSDictionary *)payload origin:(id)origin { NSString *url = payload[UIApplicationLaunchOptionsURLKey]; - if ([url hasPrefix:@"livecontainer://livecontainer-relaunch"]) { + if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-relaunch", NSUserDefaults.lcAppUrlScheme]]) { // Ignore return; - } else if ([url hasPrefix:@"livecontainer://open-web-page?"]) { + } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://open-web-page?", NSUserDefaults.lcAppUrlScheme]]) { // launch to UI and open web page NSURLComponents* lcUrl = [NSURLComponents componentsWithString:url]; NSString* realUrlEncoded = lcUrl.queryItems[0].value; @@ -98,7 +118,7 @@ - (void)hook__applicationOpenURLAction:(id)action payload:(NSDictionary *)payloa NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; LCOpenWebPage(decodedUrl); return; - } else if ([url hasPrefix:@"livecontainer://open-url"]) { + } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://open-url", NSUserDefaults.lcAppUrlScheme]]) { // pass url to guest app NSURLComponents* lcUrl = [NSURLComponents componentsWithString:url]; NSString* realUrlEncoded = lcUrl.queryItems[0].value; @@ -116,7 +136,7 @@ - (void)hook__applicationOpenURLAction:(id)action payload:(NSDictionary *)payloa } return; - } else if ([url hasPrefix:@"livecontainer://livecontainer-launch?"]) { + } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?", NSUserDefaults.lcAppUrlScheme]]) { if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { LCShowSwitchAppConfirmation([NSURL URLWithString:url]); } @@ -147,10 +167,10 @@ - (void)hook_scene:(id)scene didReceiveActions:(NSSet *)actions fromTransitionCo } NSString *url = urlAction.url.absoluteString; - if ([url hasPrefix:@"livecontainer://livecontainer-relaunch"]) { + if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-relaunch", NSUserDefaults.lcAppUrlScheme]]) { // Ignore return; - } else if ([url hasPrefix:@"livecontainer://open-web-page?"]) { + } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://open-web-page?", NSUserDefaults.lcAppUrlScheme]]) { NSURLComponents* lcUrl = [NSURLComponents componentsWithString:url]; NSString* realUrlEncoded = lcUrl.queryItems[0].value; if(!realUrlEncoded) return; @@ -159,7 +179,7 @@ - (void)hook_scene:(id)scene didReceiveActions:(NSSet *)actions fromTransitionCo NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; LCOpenWebPage(decodedUrl); return; - } else if ([url hasPrefix:@"livecontainer://open-url?"]) { + } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://open-url", NSUserDefaults.lcAppUrlScheme]]) { // Open guest app's URL scheme NSURLComponents* lcUrl = [NSURLComponents componentsWithString:url]; NSString* realUrlEncoded = lcUrl.queryItems[0].value; @@ -180,7 +200,7 @@ - (void)hook_scene:(id)scene didReceiveActions:(NSSet *)actions fromTransitionCo } return; - } else if ([url hasPrefix:@"livecontainer://livecontainer-launch?"]){ + } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?", NSUserDefaults.lcAppUrlScheme]]){ // If it's not current app, then switch if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { LCShowSwitchAppConfirmation(urlAction.url); diff --git a/TweakLoader/utils.h b/TweakLoader/utils.h index 36e7ca5..fd58f5a 100644 --- a/TweakLoader/utils.h +++ b/TweakLoader/utils.h @@ -6,4 +6,5 @@ void swizzle(Class class, SEL originalAction, SEL swizzledAction); // Exported from the main executable @interface NSUserDefaults(LiveContainer) + (instancetype)lcUserDefaults; ++ (NSString *)lcAppUrlScheme; @end diff --git a/main.m b/main.m index b246785..d2e3b4c 100644 --- a/main.m +++ b/main.m @@ -18,11 +18,15 @@ static int (*appMain)(int, char**); static const char *dyldImageName; NSUserDefaults *lcUserDefaults; +NSString* lcAppUrlScheme; @implementation NSUserDefaults(LiveContainer) + (instancetype)lcUserDefaults { return lcUserDefaults; } ++ (NSString *)lcAppUrlScheme { + return lcAppUrlScheme; +} @end static BOOL checkJITEnabled() { @@ -347,6 +351,7 @@ int LiveContainerMain(int argc, char *argv[]) { NSLog(@"Ignore this: %@", UIScreen.mainScreen); lcUserDefaults = NSUserDefaults.standardUserDefaults; + lcAppUrlScheme = NSBundle.mainBundle.infoDictionary[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0]; NSString *selectedApp = [lcUserDefaults stringForKey:@"selected"]; if (selectedApp) { NSString *launchUrl = [lcUserDefaults stringForKey:@"launchAppUrlScheme"]; @@ -360,7 +365,7 @@ int LiveContainerMain(int argc, char *argv[]) { NSData *data = [launchUrl dataUsingEncoding:NSUTF8StringEncoding]; NSString *encodedUrl = [data base64EncodedStringWithOptions:0]; - NSString* finalUrl = [NSString stringWithFormat:@"livecontainer://open-url?url=%@", encodedUrl]; + NSString* finalUrl = [NSString stringWithFormat:@"%@://open-url?url=%@", lcAppUrlScheme, encodedUrl]; NSURL* url = [NSURL URLWithString: finalUrl]; [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; From 1f372002ee47dfebae42b43755ead8f18827e015 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 3 Sep 2024 18:30:22 +0800 Subject: [PATCH 15/36] key chain sharing between LCs --- LiveContainerSwiftUI/LCSettingsView.swift | 80 ++++++++++++++++++----- entitlements.xml | 5 ++ entitlements_setup.xml | 5 ++ 3 files changed, 73 insertions(+), 17 deletions(-) diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift index ff8483d..db7fd02 100644 --- a/LiveContainerSwiftUI/LCSettingsView.swift +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -20,6 +20,10 @@ struct LCSettingsView: View { @State private var appFolderRemovalContinuation : CheckedContinuation? = nil @State private var folderRemoveCount = 0 + @State private var confirmKeyChainRemovalShow = false + @State private var confirmKeyChainRemoval = false + @State private var confirmKeyChainContinuation : CheckedContinuation? = nil + @State var isJitLessEnabled = false // 0= not installed, 1= is installed, 2=current liveContainer is the second one @State var multipleLiveContainerStatus = 0 @@ -50,23 +54,24 @@ struct LCSettingsView: View { var body: some View { NavigationView { Form { - - Section{ - Button { - setupJitLess() - } label: { - if isJitLessEnabled { - Text("Renew JIT-less certificate") - } else { - Text("Setup JIT-less certificate") + if multipleLiveContainerStatus != 2 { + Section{ + Button { + setupJitLess() + } label: { + if isJitLessEnabled { + Text("Renew JIT-less certificate") + } else { + Text("Setup JIT-less certificate") + } } + } header: { + Text("JIT-Less") + } footer: { + Text("JIT-less allows you to use LiveContainer without having to enable JIT. Requires AltStore or SideStore.") } - } header: { - Text("JIT-Less") - } footer: { - Text("JIT-less allows you to use LiveContainer without having to enable JIT. Requires AltStore or SideStore.") } - + Section{ Button { installAnotherLC() @@ -84,7 +89,7 @@ struct LCSettingsView: View { } header: { Text("Multiple LiveContainers") } footer: { - Text("By installing multiple LiveContainers, and converting apps to Shared Apps, you can open one app among all LiveContainers with most of its data and settings.") + Text("By installing multiple LiveContainers, and converting apps to Shared Apps, you can open one app between all LiveContainers with most of its data and settings.") } @@ -116,7 +121,7 @@ struct LCSettingsView: View { Section { Toggle(isOn: $injectToLCItelf) { - Text("Load Tewaks to LiveContainer Itself") + Text("Load Tweaks to LiveContainer Itself") } } footer: { Text("Place your tweaks into the global “Tweaks” folder and LiveContainer will pick them up.") @@ -128,6 +133,11 @@ struct LCSettingsView: View { } label: { Text("Clean Unused Data Folders") } + Button(role:.destructive) { + Task { await removeKeyChain() } + } label: { + Text("Clean Up Keychain") + } } VStack{ @@ -164,7 +174,21 @@ struct LCSettingsView: View { } } - + .alert("Keychain Clean Up", isPresented: $confirmKeyChainRemovalShow) { + Button(role: .destructive) { + self.confirmKeyChainRemoval = true + self.confirmKeyChainContinuation?.resume() + } label: { + Text("Delete") + } + + Button("Cancel", role: .cancel) { + self.confirmKeyChainRemoval = false + self.confirmKeyChainContinuation?.resume() + } + } message: { + Text("If some app's account can not be synced between LiveContainers, it's may because it is still stored in current LiveContainer's private keychain. Cleaning up keychain may solve this issue, but it may sign you out of some of your accounts. Continue?") + } .onChange(of: isAltCertIgnored) { newValue in saveItem(key: "LCIgnoreALTCertificate", val: newValue) } @@ -264,5 +288,27 @@ struct LCSettingsView: View { } } + + func removeKeyChain() async { + await withCheckedContinuation { c in + self.confirmKeyChainContinuation = c + DispatchQueue.main.async { + confirmKeyChainRemovalShow = true + } + } + if !confirmKeyChainRemoval { + return + } + + [kSecClassGenericPassword, kSecClassInternetPassword, kSecClassCertificate, kSecClassKey, kSecClassIdentity].forEach { + let status = SecItemDelete([ + kSecClass: $0, + kSecAttrSynchronizable: kSecAttrSynchronizableAny + ] as CFDictionary) + if status != errSecSuccess && status != errSecItemNotFound { + //Error while removing class $0 + } + } + } } diff --git a/entitlements.xml b/entitlements.xml index 5b553f0..5c71a87 100644 --- a/entitlements.xml +++ b/entitlements.xml @@ -14,5 +14,10 @@ get-task-allow + + keychain-access-groups + + $(AppIdentifierPrefix)com.kdt.livecontainer + diff --git a/entitlements_setup.xml b/entitlements_setup.xml index 6f6e198..5217de4 100644 --- a/entitlements_setup.xml +++ b/entitlements_setup.xml @@ -15,5 +15,10 @@ group.* KeychainAccessGroupWillBeWrittenByLiveContainerAAAAAAAAAAAAAAAAAAAA + + keychain-access-groups + + $(AppIdentifierPrefix)com.kdt.livecontainer + From 50ced20cb88c494f70730acd1e87b32dc8a4d2a2 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 3 Sep 2024 20:11:32 +0800 Subject: [PATCH 16/36] fix guesthooks not working --- LCSharedUtils.m | 4 ++-- LiveContainerSwiftUI/LCTweaksView.swift | 1 + LiveContainerUI/LCUtils.m | 2 +- TweakLoader/NSUserDefaults.m | 9 +++++++++ TweakLoader/UIKit+GuestHooks.m | 12 ++++++------ main.m | 2 ++ 6 files changed, 21 insertions(+), 9 deletions(-) diff --git a/LCSharedUtils.m b/LCSharedUtils.m index 11566df..fc2869c 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -3,6 +3,7 @@ extern NSUserDefaults *lcUserDefaults; extern NSString *lcAppUrlScheme; +extern NSString* lcAppGroup; @implementation LCSharedUtils + (NSString *)certificatePassword { @@ -10,8 +11,7 @@ + (NSString *)certificatePassword { if(ans) { return ans; } else { - NSString *appGroupID = [NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"] firstObject]; - return [[[NSUserDefaults alloc] initWithSuiteName:appGroupID] objectForKey:@"LCCertificatePassword"]; + return [[[NSUserDefaults alloc] initWithSuiteName:lcAppGroup] objectForKey:@"LCCertificatePassword"]; } } diff --git a/LiveContainerSwiftUI/LCTweaksView.swift b/LiveContainerSwiftUI/LCTweaksView.swift index f528aad..c63e76d 100644 --- a/LiveContainerSwiftUI/LCTweaksView.swift +++ b/LiveContainerSwiftUI/LCTweaksView.swift @@ -121,6 +121,7 @@ struct LCTweakFolderView : View { } .navigationTitle(baseUrl.lastPathComponent) + .navigationViewStyle(StackNavigationViewStyle()) .toolbar { ToolbarItem(placement: .topBarTrailing) { if !isTweakSigning && LCUtils.certificatePassword() != nil { diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index 4500c1f..5c49a0a 100644 --- a/LiveContainerUI/LCUtils.m +++ b/LiveContainerUI/LCUtils.m @@ -325,7 +325,7 @@ + (NSURL *)archiveIPAWithBundleName:(NSString*)newBundleName error:(NSError **)e infoDict[@"CFBundleDisplayName"] = newBundleName; infoDict[@"CFBundleName"] = newBundleName; infoDict[@"CFBundleIdentifier"] = [NSString stringWithFormat:@"com.kdt.%@", newBundleName]; - infoDict[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0] = newBundleName; + infoDict[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0] = [newBundleName lowercaseString]; infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon2_60x60@2x"; infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][1] = @"AppIcon2_76x76@2x~ipad"; infoDict[@"CFBundleIcons"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon2_60x60@2x"; diff --git a/TweakLoader/NSUserDefaults.m b/TweakLoader/NSUserDefaults.m index f99a03d..db7680d 100644 --- a/TweakLoader/NSUserDefaults.m +++ b/TweakLoader/NSUserDefaults.m @@ -13,12 +13,21 @@ static void NSUDGuestHooksInit() { swizzle(NSUserDefaults.class, @selector(_container), @selector(hook__container)); } +@interface NSUserDefaults(Private) +- (CFStringRef)_identifier; +@end + // NSFileManager simulate app group @implementation NSUserDefaults(LiveContainerHooks) - (CFStringRef)hook__container { const char *homeDir = getenv("HOME"); CFStringRef cfHomeDir = CFStringCreateWithCString(NULL, homeDir, kCFStringEncodingUTF8); + // let LiveContainer it self bypass + CFComparisonResult r = CFStringCompare([self _identifier], kCFPreferencesCurrentApplication, 0); + if(r == kCFCompareEqualTo) { + return nil; + } return cfHomeDir; } diff --git a/TweakLoader/UIKit+GuestHooks.m b/TweakLoader/UIKit+GuestHooks.m index 18a12ac..6cd7a10 100644 --- a/TweakLoader/UIKit+GuestHooks.m +++ b/TweakLoader/UIKit+GuestHooks.m @@ -63,7 +63,7 @@ void openUniversalLink(NSString* decodedUrl) { [uacm handleActivityContinuation:dict isSuspended:nil]; } -void LCOpenWebPage(NSString* webPageUrlString) { +void LCOpenWebPage(NSString* webPageUrlString, NSString* originalUrl) { NSString *message = [NSString stringWithFormat:@"Are you sure you want to open the web page and launch an app? Doing so will terminate this app. You can try to open it in the current app if it supports Universal Links."]; UIWindow *window = [[UIWindow alloc] initWithFrame:UIScreen.mainScreen.bounds]; UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"LiveContainer" message:message preferredStyle:UIAlertControllerStyleAlert]; @@ -78,7 +78,7 @@ void LCOpenWebPage(NSString* webPageUrlString) { }]; if([NSUserDefaults.lcAppUrlScheme isEqualToString:@"livecontainer"] && [UIApplication.sharedApplication canOpenURL:[NSURL URLWithString: @"livecontainer2://"]]) { UIAlertAction* openlc2Action = [UIAlertAction actionWithTitle:@"Open In LiveContainer2" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { - NSURLComponents* newUrlComp = [NSURLComponents componentsWithString:webPageUrlString]; + NSURLComponents* newUrlComp = [NSURLComponents componentsWithString:originalUrl]; [newUrlComp setScheme:@"livecontainer2"]; [UIApplication.sharedApplication openURL:[newUrlComp URL] options:@{} completionHandler:nil]; window.windowScene = nil; @@ -116,7 +116,7 @@ - (void)hook__applicationOpenURLAction:(id)action payload:(NSDictionary *)payloa // Convert the base64 encoded url into String NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:realUrlEncoded options:0]; NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; - LCOpenWebPage(decodedUrl); + LCOpenWebPage(decodedUrl, url); return; } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://open-url", NSUserDefaults.lcAppUrlScheme]]) { // pass url to guest app @@ -136,7 +136,7 @@ - (void)hook__applicationOpenURLAction:(id)action payload:(NSDictionary *)payloa } return; - } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?", NSUserDefaults.lcAppUrlScheme]]) { + } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?bundle-name=", NSUserDefaults.lcAppUrlScheme]]) { if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { LCShowSwitchAppConfirmation([NSURL URLWithString:url]); } @@ -177,7 +177,7 @@ - (void)hook_scene:(id)scene didReceiveActions:(NSSet *)actions fromTransitionCo // launch to UI and open web page NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:realUrlEncoded options:0]; NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; - LCOpenWebPage(decodedUrl); + LCOpenWebPage(decodedUrl, url); return; } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://open-url", NSUserDefaults.lcAppUrlScheme]]) { // Open guest app's URL scheme @@ -200,7 +200,7 @@ - (void)hook_scene:(id)scene didReceiveActions:(NSSet *)actions fromTransitionCo } return; - } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?", NSUserDefaults.lcAppUrlScheme]]){ + } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?bundle-name=", NSUserDefaults.lcAppUrlScheme]]){ // If it's not current app, then switch if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { LCShowSwitchAppConfirmation(urlAction.url); diff --git a/main.m b/main.m index d2e3b4c..5a08d44 100644 --- a/main.m +++ b/main.m @@ -19,6 +19,7 @@ static const char *dyldImageName; NSUserDefaults *lcUserDefaults; NSString* lcAppUrlScheme; +NSString* lcAppGroup; @implementation NSUserDefaults(LiveContainer) + (instancetype)lcUserDefaults { @@ -352,6 +353,7 @@ int LiveContainerMain(int argc, char *argv[]) { lcUserDefaults = NSUserDefaults.standardUserDefaults; lcAppUrlScheme = NSBundle.mainBundle.infoDictionary[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0]; + lcAppGroup = [NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"] firstObject]; NSString *selectedApp = [lcUserDefaults stringForKey:@"selected"]; if (selectedApp) { NSString *launchUrl = [lcUserDefaults stringForKey:@"launchAppUrlScheme"]; From 4f4552062cd5a697449b73ecb810f20e834fe486 Mon Sep 17 00:00:00 2001 From: Huge_Black Date: Tue, 3 Sep 2024 21:10:10 +0800 Subject: [PATCH 17/36] fix merge issue & set an icon --- LCSharedUtils.h | 2 +- LCSharedUtils.m | 21 +++++++++++++++++++-- LiveContainerSwiftUI/Shared.swift | 4 ++-- LiveContainerUI/LCUtils.m | 6 +++--- Resources/AppIcon2_60x60@2x.png | Bin 29066 -> 0 bytes Resources/AppIcon2_76x76@2x~ipad.png | Bin 42229 -> 0 bytes Resources/AppIcon60x60_2.png | Bin 0 -> 4812 bytes Resources/AppIcon76x76_2.png | Bin 0 -> 6059 bytes main.m | 8 +++----- 9 files changed, 28 insertions(+), 13 deletions(-) delete mode 100644 Resources/AppIcon2_60x60@2x.png delete mode 100644 Resources/AppIcon2_76x76@2x~ipad.png create mode 100644 Resources/AppIcon60x60_2.png create mode 100644 Resources/AppIcon76x76_2.png diff --git a/LCSharedUtils.h b/LCSharedUtils.h index 74a4ef3..2a07a36 100644 --- a/LCSharedUtils.h +++ b/LCSharedUtils.h @@ -1,7 +1,7 @@ @import Foundation; @interface LCSharedUtils : NSObject - ++ (NSString *)appGroupID; + (NSString *)certificatePassword; + (BOOL)launchToGuestApp; + (BOOL)launchToGuestAppWithURL:(NSURL *)url; diff --git a/LCSharedUtils.m b/LCSharedUtils.m index fc2869c..d941daa 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -3,15 +3,32 @@ extern NSUserDefaults *lcUserDefaults; extern NSString *lcAppUrlScheme; -extern NSString* lcAppGroup; @implementation LCSharedUtils + ++ (NSString *)appGroupID { + static dispatch_once_t once; + static NSString *appGroupID; + dispatch_once(&once, ^{ + for (NSString *group in NSBundle.mainBundle.infoDictionary[@"ALTAppGroups"]) { + NSURL *path = [NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:group]; + NSURL *bundlePath = [path URLByAppendingPathComponent:@"Apps/com.kdt.livecontainer/App.app"]; + if ([NSFileManager.defaultManager fileExistsAtPath:bundlePath.path]) { + // This will fail if LiveContainer is installed in both stores, but it should never be the case + appGroupID = group; + return; + } + } + }); + return appGroupID; +} + + (NSString *)certificatePassword { NSString* ans = [lcUserDefaults objectForKey:@"LCCertificatePassword"]; if(ans) { return ans; } else { - return [[[NSUserDefaults alloc] initWithSuiteName:lcAppGroup] objectForKey:@"LCCertificatePassword"]; + return [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificatePassword"]; } } diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift index c786002..8464687 100644 --- a/LiveContainerSwiftUI/Shared.swift +++ b/LiveContainerSwiftUI/Shared.swift @@ -20,8 +20,8 @@ struct LCPath { public static let lcGroupDocPath = { let fm = FileManager() // it seems that Apple don't want to create one for us, so we just borrow our Store's - if let appGroupPath = LCUtils.appGroupPath() { - return URL(fileURLWithPath: appGroupPath + "/LiveContainer", isDirectory: true) + if let appGroupPathUrl = LCUtils.appGroupPath() { + return appGroupPathUrl.appendingPathComponent("LiveContainer") } else if let appGroupPathUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.SideStore.SideStore") { return appGroupPathUrl.appendingPathComponent("LiveContainer") diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index 9a3ffe4..ea4a413 100644 --- a/LiveContainerUI/LCUtils.m +++ b/LiveContainerUI/LCUtils.m @@ -343,9 +343,9 @@ + (NSURL *)archiveIPAWithBundleName:(NSString*)newBundleName error:(NSError **)e infoDict[@"CFBundleName"] = newBundleName; infoDict[@"CFBundleIdentifier"] = [NSString stringWithFormat:@"com.kdt.%@", newBundleName]; infoDict[@"CFBundleURLTypes"][0][@"CFBundleURLSchemes"][0] = [newBundleName lowercaseString]; - infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon2_60x60@2x"; - infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][1] = @"AppIcon2_76x76@2x~ipad"; - infoDict[@"CFBundleIcons"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon2_60x60@2x"; + infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon60x60_2"; + infoDict[@"CFBundleIcons~ipad"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][1] = @"AppIcon76x76_2"; + infoDict[@"CFBundleIcons"][@"CFBundlePrimaryIcon"][@"CFBundleIconFiles"][0] = @"AppIcon60x60_2"; [infoDict writeToURL:infoPath error:error]; diff --git a/Resources/AppIcon2_60x60@2x.png b/Resources/AppIcon2_60x60@2x.png deleted file mode 100644 index c8688e77829d891cc3065e4ba5eb918867b1bf7a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29066 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 PmlVoN1Egvtj6?nhdV}y2 diff --git a/Resources/AppIcon2_76x76@2x~ipad.png b/Resources/AppIcon2_76x76@2x~ipad.png deleted file mode 100644 index f4d7e0090f49c0ff6289daf7ec2dd6910f544960..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 42229 zcmYIv18`+cuyAa1%CLerw3i9sxdu1 z-E$(96{V07@Daekz@Wq}#AKAgzQZuIdzgRqk!7SMRKIY>e*g#b_5SwzJLwmKvJh1e z1p{l0M|?Ae{_2Cfs7i@})lCtee09E?N~fq7Gbfdz(wfxUbU1s;Qexv_wO{WAgs zGqt3W*u0U9ojHGIl3C51*;a zVRqJFol}Fy&Lx4HS&gS-d|xyu)9GK!w~vJ`SE3D1ekA%m z!U}L5)A{E!Pl%77AmL5~Oy>9Ad|{^TO<{+1;1erER11V`bS?~tsADv4-B!vThwyT6 znpPeH0YuZ$@oXBVEwVx*2f|AG+vcFLx6~ryEzeUO-n=)_&HQb#nBI;bxul|rI2L)I zDhMX|DFfL&$a_9lV@b@TOX&}hJvWbk-TM05H)q5Je1?G|EX-5`5qVXvQ7=_hN{td6 zjmE+-+;{%t}uAHd(7@(>bsXnMPZpzv0p2)F6{xBZK#80^f&o z!W46QI(h|MLwk3TS+V;^h%sM>(MbqrIU8F1owi{4N^&aD&*NqY6IExKF~%Npvrkwp zM8D&}aoQgRVh&OpaGJ@c@RLTF#7GQ_4x_914ZoYA++%%XZK`_@M(!egm#ZP;1+Ve@ z7*+%SQgMiuwGUePw=sCYR+ZuhaJL|j?c`#{{@B=xcrjPNSv8_eK*qZQFvS;FcQ4P-E1sN1^zuWWRaJ3|5 zXfYLKcYIdRI*as6@Vm|ZV>8sKw*~Fx_taW#eEtzE{N1LgNJVBcD$4YyI|x+-`mFV{ zPp~e!;QJF!I2z0uopTyO;|5BRwoO3BWsckiGTr@q)inek*m!K;)FwJJM&pCe@4p}Y zY0Tj_Ds9}~-VRUSo(&yX+ zZDUlji>mG_)&v#jZF2KsI^Zcd1euhJWM***nF%USpWH^VqrSbeA{Jc%1t}ijvTo6s z8wg5aV$pN9;x^^?OJ^Qw;L=z8{U3?U=zRycMck9yb2qdTc$@K&`fFFfN6n{#`D%fW z%XF)Gw{I5A2E$O_EMb0lMXP%RkM2m)t!c_IddA0#wC24g&W5lAS$0$X;{w~(iT>$fa!4x}UxP{c} z)pob_P3-B#s@+#G3$?3fuzX{4dyTmMO5rj6UyTeBp9~qyF{gU}1 ziT(}T;-QkJcHz=qec!dS|7h7HSUQ*R>thLiue2@DtCO7WA)QCqeSgRC3!28=7uLcZgg=_fZB!3iwMQ%IjgzfqsxqWkCf4Twekz)rPqDp|E9hv|hJy0hE{ zSU#tS3&$`BJ#pgskNdkZ==+y?5?QzP19q7*q4U!Wgs;T;561)h1`gMzmt1dr1&rfc zcH_H2^|#vTnpThNh;O&E>7H+4OwFg9IijXitQX_?&re;vzAu03ecv_+sr>nqm3p%W zrMBKC+Nb@uH)ai4TxPwSqGeg(cDG8KSBa)vEix{suHh-((!p$?PLuivqKk%L;bI=p zzxfT38p1mEdW4@DLvp<_$Rh-`TZ1RhASDdJ+qbE`3w!U2i^IP7rbxNNROZ>>&zgysJ zXZ8WHeGx;})B-&*bKaAXq7O6RuUBkjdZc7}d(6wemG+I)I}!85m^qz6!P_E!2ugtrB$1$+ zw5L0=1ptA!Jt{LkD3W{&W#DqHjrK}UY_smiz7JEG(UOU?2Lr_