diff --git a/.gitignore b/.gitignore index faf8687..a0dbe6d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ .theos/ packages/ .DS_Store +LiveContainer.xcodeproj +project.xcworkspace +xcuserdata \ No newline at end of file diff --git a/LCSharedUtils.h b/LCSharedUtils.h index 74a4ef3..f8d6aa3 100644 --- a/LCSharedUtils.h +++ b/LCSharedUtils.h @@ -1,9 +1,17 @@ @import Foundation; @interface LCSharedUtils : NSObject - ++ (NSString *)appGroupID; + (NSString *)certificatePassword; ++ (BOOL)askForJIT; + (BOOL)launchToGuestApp; + (BOOL)launchToGuestAppWithURL:(NSURL *)url; + (void)setWebPageUrlForNextLaunch:(NSString*)urlString; ++ (NSString*)getAppRunningLCSchemeWithBundleId:(NSString*)bundleId; ++ (void)setAppRunningByThisLC:(NSString*)bundleId; ++ (void)movePreferencesFromPath:(NSString*) plistLocationFrom toPath:(NSString*)plistLocationTo; ++ (void)loadPreferencesFromPath:(NSString*) plistLocationFrom; ++ (void)moveSharedAppFolderBack; ++ (void)removeAppRunningByLC:(NSString*)LCScheme; ++ (NSBundle*)findBundleWithBundleId:(NSString*)bundleId; @end diff --git a/LCSharedUtils.m b/LCSharedUtils.m index 9a4af1a..f670d0f 100644 --- a/LCSharedUtils.m +++ b/LCSharedUtils.m @@ -2,10 +2,34 @@ #import "UIKitPrivate.h" extern NSUserDefaults *lcUserDefaults; +extern NSString *lcAppUrlScheme; @implementation LCSharedUtils + ++ (NSString *)appGroupID { + static dispatch_once_t once; + static NSString *appGroupID = @"group.com.SideStore.SideStore"; + 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 { - return [lcUserDefaults objectForKey:@"LCCertificatePassword"]; + NSString* ans = [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificatePassword"]; + if(ans) { + return ans; + } else { + return [lcUserDefaults objectForKey:@"LCCertificatePassword"]; + } } + (BOOL)launchToGuestApp { @@ -16,7 +40,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=%@"; } @@ -33,19 +57,64 @@ + (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=%@"; + 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 { + 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; +} + + (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; } @@ -53,4 +122,179 @@ + (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]; + +} + +// move all plists file from fromPath to toPath ++ (void)movePreferencesFromPath:(NSString*) plistLocationFrom toPath:(NSString*)plistLocationTo { + NSFileManager* fm = [[NSFileManager alloc] init]; + NSError* error1; + NSArray * plists = [fm contentsOfDirectoryAtPath:plistLocationFrom error:&error1]; + + // 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]; + // 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; + } + NSString* toPlistPath = [NSString stringWithFormat:@"%@/%@", plistLocationTo, item]; + NSString* fromPlistPath = [NSString stringWithFormat:@"%@/%@", plistLocationFrom, item]; + + [fm moveItemAtPath:fromPlistPath toPath:toPlistPath error:&error1]; + if(error1) { + NSLog(@"[LC] 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]; + + // move all plists in fromPath to toPath + for (NSString* item in plists) { + if(![item hasSuffix:@".plist"] || [item containsString:@"livecontainer"]) { + continue; + } + 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 ++ (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 + ]; + } + } + +} + ++ (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/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/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/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/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/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/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 new file mode 100644 index 0000000..17445d9 --- /dev/null +++ b/LiveContainerSwiftUI/LCAppBanner.swift @@ -0,0 +1,321 @@ +// +// LCAppBanner.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation +import SwiftUI +import UniformTypeIdentifiers + +protocol LCAppBannerDelegate { + func removeApp(app: LCAppModel) + func installMdm(data: Data) + func openNavigationView(view: AnyView) +} + +struct LCAppBanner : View { + @State var appInfo: LCAppInfo + var delegate: LCAppBannerDelegate + + @ObservedObject var model : LCAppModel + + @Binding var appDataFolders: [String] + @Binding var tweakFolders: [String] + + @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? + + @State private var errorShow = false + @State private var errorInfo = "" + + @EnvironmentObject private var sharedModel : SharedModel + + init(appModel: LCAppModel, delegate: LCAppBannerDelegate, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>) { + _appInfo = State(initialValue: appModel.appInfo) + _appDataFolders = appDataFolders + _tweakFolders = tweakFolders + self.delegate = delegate + + _model = ObservedObject(wrappedValue: appModel) + } + + 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: { + HStack { + Text(appInfo.displayName()).font(.system(size: 16)).bold() + if model.uiIsShared { + Text("lc.appBanner.shared".loc).font(.system(size: 8)).bold().padding(2) + .frame(width: 50, height:16) + .background( + Capsule().fill(Color("BadgeColor")) + ) + } + if model.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")) + Text(LocalizedStringKey(model.uiDataFolder == nil ? "lc.appBanner.noDataFolder".loc : model.uiDataFolder!)).font(.system(size: 8)).foregroundColor(Color("FontColor")) + }) + } + Spacer() + Button { + Task{ await runApp() } + } label: { + if !model.isSigningInProgress { + Text("lc.appBanner.run".loc).bold().foregroundColor(.white) + } else { + ProgressView().progressViewStyle(.circular) + } + + } + .padding() + .frame(idealWidth: 70) + .frame(height: 32) + .fixedSize() + .background(GeometryReader { g in + if !model.isSigningInProgress { + 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: (model.signProgress - 2) * w, y: h/2-w) + } + + }) + .clipShape(Capsule()) + .disabled(model.isAppRunning) + + } + .padding() + .frame(height: 88) + .background(RoundedRectangle(cornerSize: CGSize(width:22, height: 22)).fill(Color("AppBannerBG"))) + .onAppear() { + handleOnAppear() + } + + .fileExporter( + isPresented: $saveIconExporterShow, + document: saveIconFile, + contentType: .image, + defaultFilename: "\(appInfo.displayName()!) Icon.png", + onCompletion: { result in + + }) + .contextMenu{ + Section(appInfo.relativeBundlePath) { + if #available(iOS 16.0, *){ + + } else { + Text(appInfo.relativeBundlePath) + } + if !model.uiIsShared { + if model.uiDataFolder != nil { + Button { + openDataFolder() + } label: { + Label("lc.appBanner.openDataFolder".loc, systemImage: "folder") + } + } + } + + Menu { + Button { + openSafariViewToCreateAppClip() + } label: { + Label("lc.appBanner.createAppClip".loc, systemImage: "appclip") + } + Button { + copyLaunchUrl() + } label: { + Label("lc.appBanner.copyLaunchUrl".loc, systemImage: "link") + } + Button { + saveIcon() + } label: { + Label("lc.appBanner.saveAppIcon".loc, systemImage: "square.and.arrow.down") + } + + + } label: { + Label("lc.appBanner.addToHomeScreen".loc, systemImage: "plus.app") + } + + Button { + openSettings() + } label: { + Label("lc.tabView.settings".loc, systemImage: "gear") + } + + + if !model.uiIsShared { + Button(role: .destructive) { + Task{ await uninstall() } + } label: { + Label("lc.appBanner.uninstall".loc, systemImage: "trash") + } + + } + + } + + + + + } + + .alert("lc.appBanner.confirmUninstallTitle".loc, isPresented: $appRemovalAlert.show) { + Button(role: .destructive) { + appRemovalAlert.close(result: true) + } label: { + Text("lc.appBanner.uninstall".loc) + } + Button("lc.common.cancel".loc, role: .cancel) { + appRemovalAlert.close(result: false) + } + } message: { + Text("lc.appBanner.confirmUninstallMsg %@".localizeWithFormat(appInfo.displayName()!)) + } + .alert("lc.appBanner.deleteDataTitle".loc, isPresented: $appFolderRemovalAlert.show) { + Button(role: .destructive) { + appFolderRemovalAlert.close(result: true) + } label: { + Text("lc.common.delete".loc) + } + Button("lc.common.cancel".loc, role: .cancel) { + appFolderRemovalAlert.close(result: false) + } + } message: { + Text("lc.appBanner.deleteDataMsg \(appInfo.displayName()!)") + } + .alert("lc.appBanner.waitForJitTitle".loc, isPresented: $jitAlert.show) { + Button { + jitAlert.close(result: true) + } label: { + Text("lc.appBanner.jitLaunchNow".loc) + } + Button("lc.common.cancel", role: .cancel) { + jitAlert.close(result: false) + } + } message: { + Text("lc.appBanner.waitForJitMsg".loc) + } + + .alert("lc.common.error".loc, isPresented: $errorShow) { + Button("lc.common.ok".loc, action: { + }) + } message: { + Text(errorInfo) + } + + } + + func handleOnAppear() { + model.jitAlert = jitAlert + } + + func runApp() async { + do { + try await model.runApp() + } catch { + errorInfo = errorInfo + errorShow = true + } + } + + + func openSettings() { + delegate.openNavigationView(view: AnyView(LCAppSettingsView(model: model, appDataFolders: $appDataFolders, tweakFolders: $tweakFolders))) + } + + + func openDataFolder() { + let url = URL(string:"shareddocuments://\(LCPath.docPath.path)/Data/Application/\(appInfo.dataUUID()!)") + UIApplication.shared.open(url!) + } + + + + func uninstall() async { + do { + if let result = await appRemovalAlert.open(), !result { + return + } + + var doRemoveAppFolder = false + if self.appInfo.getDataUUIDNoAssign() != nil { + if let result = await appFolderRemovalAlert.open() { + doRemoveAppFolder = result + } + + } + + let fm = FileManager() + try fm.removeItem(atPath: self.appInfo.bundlePath()!) + self.delegate.removeApp(app: self.model) + if doRemoveAppFolder { + let dataUUID = appInfo.dataUUID()! + let dataFolderPath = LCPath.dataPath.appendingPathComponent(dataUUID) + try fm.removeItem(at: dataFolderPath) + + DispatchQueue.main.async { + self.appDataFolders.removeAll(where: { f in + return f == dataUUID + }) + } + } + + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + } + + + func copyLaunchUrl() { + 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 saveIcon() { + let img = appInfo.icon()! + self.saveIconFile = ImageDocument(uiImage: img) + self.saveIconExporterShow = true + } + + +} diff --git a/LiveContainerSwiftUI/LCAppListView.swift b/LiveContainerSwiftUI/LCAppListView.swift new file mode 100644 index 0000000..17ee1e5 --- /dev/null +++ b/LiveContainerSwiftUI/LCAppListView.swift @@ -0,0 +1,533 @@ +// +// ContentView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import SwiftUI +import UniformTypeIdentifiers + +struct AppReplaceOption : Hashable { + var isReplace: Bool + var nameOfFolderToInstall: String + var appToReplace: LCAppModel? +} + +struct LCAppListView : View, LCAppBannerDelegate, LCAppModelDelegate { + + @Binding var apps: [LCAppModel] + @Binding var hiddenApps: [LCAppModel] + + @Binding var appDataFolderNames: [String] + @Binding var tweakFolderNames: [String] + + @State var didAppear = false + // ipa choosing stuff + @State var choosingIPA = false + @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 installObserver : NSKeyValueObservation? + + @State var installOptions: [AppReplaceOption] + @StateObject var installReplaceAlert = AlertHelper() + + @State var webViewOpened = false + @State var webViewURL : URL = URL(string: "about:blank")! + @StateObject private var webViewUrlInput = InputHelper() + + @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<[LCAppModel]>, hiddenApps: Binding<[LCAppModel]>, appDataFolderNames: Binding<[String]>, tweakFolderNames: Binding<[String]>) { + _installOptions = State(initialValue: []) + _apps = apps + _hiddenApps = hiddenApps + _appDataFolderNames = appDataFolderNames + _tweakFolderNames = tweakFolderNames + + } + + var body: some View { + NavigationView { + ScrollView { + + NavigationLink( + destination: navigateTo, + isActive: $isNavigationActive, + label: { + EmptyView() + }) + + 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(appModel: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) + } + .transition(.scale) + + } + .padding() + .animation(.easeInOut, value: apps) + + if !sharedModel.isHiddenAppUnlocked { + Text(apps.count > 0 ? "lc.appList.appCounter %lld".localizeWithFormat(apps.count) : "lc.appList.installTip".loc).foregroundStyle(.gray) + .onTapGesture(count: 3) { + Task { await authenticateUser() } + } + } + + + if sharedModel.isHiddenAppUnlocked { + LazyVStack { + HStack { + Text("lc.appList.hiddenApps".loc) + .font(.system(.title2).bold()) + .border(Color.black) + Spacer() + } + ForEach(hiddenApps, id: \.self) { app in + LCAppBanner(appModel: app, delegate: self, appDataFolders: $appDataFolderNames, tweakFolders: $tweakFolderNames) + } + .transition(.scale) + } + .padding() + .animation(.easeInOut, value: apps) + + if hiddenApps.count == 0 { + Text("lc.appList.hideAppTip".loc) + .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("lc.appList.manageInPrimaryTip".loc).foregroundStyle(.gray).padding() + } + + } + .coordinateSpace(name: "scroll") + .onAppear { + if !didAppear { + onAppear() + } + } + + .navigationTitle("lc.appList.myApps".loc) + .toolbar { + ToolbarItem(placement: .topBarLeading) { + if LCUtils.multiLCStatus != 2 { + if !installprogressVisible { + Button("Add".loc, systemImage: "plus", action: { + if choosingIPA { + choosingIPA = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + choosingIPA = true + }) + } else { + choosingIPA = true + } + + + }) + } else { + ProgressView().progressViewStyle(.circular) + } + } + } + ToolbarItem(placement: .topBarTrailing) { + Button("lc.appList.openLink".loc, systemImage: "link", action: { + Task { await onOpenWebViewTapped() } + }) + } + } + + + } + .navigationViewStyle(StackNavigationViewStyle()) + .alert(isPresented: $errorShow){ + Alert(title: Text("lc.common.error".loc), message: Text(errorInfo)) + } + .fileImporter(isPresented: $choosingIPA, allowedContentTypes: [.ipa]) { result in + Task { await startInstallApp(result) } + } + .alert("lc.appList.installation".loc, isPresented: $installReplaceAlert.show) { + ForEach(installOptions, id: \.self) { installOption in + Button(role: installOption.isReplace ? .destructive : nil, action: { + installReplaceAlert.close(result: installOption) + }, label: { + Text(installOption.isReplace ? installOption.nameOfFolderToInstall : "lc.appList.installAsNew".loc) + }) + + } + Button(role: .cancel, action: { + installReplaceAlert.close(result: nil) + }, label: { + Text("lc.appList.abortInstallation".loc) + }) + } message: { + Text("lc.appList.installReplaceTip".loc) + } + .textFieldAlert( + isPresented: $webViewUrlInput.show, + title: "lc.appList.enterUrlTip".loc, + text: $webViewUrlInput.initVal, + placeholder: "scheme://", + action: { newText in + webViewUrlInput.close(result: newText) + }, + actionCancel: {_ in + webViewUrlInput.close(result: nil) + } + ) + .fullScreenCover(isPresented: $webViewOpened) { + LCWebView(url: $webViewURL, apps: $apps, hiddenApps: $hiddenApps, isPresent: $webViewOpened) + } + .fullScreenCover(isPresented: $safariViewOpened) { + SafariView(url: $safariViewURL) + } + + } + + func onOpenWebViewTapped() async { + guard let urlToOpen = await webViewUrlInput.open(), urlToOpen != "" else { + return + } + await openWebView(urlString: urlToOpen) + + } + + 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 + return + } + if urlToOpen.scheme == nil || urlToOpen.scheme! == "" { + urlToOpen.scheme = "https" + } + if urlToOpen.scheme != "https" && urlToOpen.scheme != "http" { + var appToLaunch : LCAppModel? = nil + 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.appInfo.urlSchemes() { + for scheme in schemes { + if let scheme = scheme as? String, scheme == urlToOpen.scheme { + appToLaunch = app + break appLoop + } + } + } + } + } + + + guard let appToLaunch = appToLaunch else { + errorInfo = "lc.appList.schemeCannotOpenError %@".localizeWithFormat(urlToOpen.scheme!) + errorShow = true + return + } + + if appToLaunch.appInfo.isHidden && !sharedModel.isHiddenAppUnlocked { + do { + if !(try await LCUtils.authenticateUser()) { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } + + UserDefaults.standard.setValue(appToLaunch.appInfo.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) async { + do { + let fileUrl = try result.get() + self.installprogressVisible = true + try await installIpaFile(fileUrl) + } catch { + errorInfo = error.localizedDescription + errorShow = true + self.installprogressVisible = false + } + } + + nonisolated func decompress(_ path: String, _ destination: String ,_ progress: Progress) async { + extract(path, destination, progress) + } + + func installIpaFile(_ url:URL) async throws { + if(!url.startAccessingSecurityScopedResource()) { + throw "lc.appList.ipaAccessError".loc; + } + let fm = FileManager() + + let installProgress = Progress.discreteProgress(totalUnitCount: 100) + self.installProgressPercentage = 0.0 + self.installObserver = installProgress.observe(\.fractionCompleted) { p, v in + DispatchQueue.main.async { + self.installProgressPercentage = p.fractionCompleted + } + } + 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 + await decompress(url.path, fm.temporaryDirectory.path, decompressProgress) + url.stopAccessingSecurityScopedResource() + + let payloadContents = try fm.contentsOfDirectory(atPath: payloadPath.path) + var appBundleName : String? = nil + for fileName in payloadContents { + if fileName.hasSuffix(".app") { + appBundleName = fileName + break + } + } + guard let appBundleName = appBundleName else { + throw "lc.appList.bundleNotFondError".loc + } + + let appFolderPath = payloadPath.appendingPathComponent(appBundleName) + + guard let newAppInfo = LCAppInfo(bundlePath: appFolderPath.path) else { + throw "lc.appList.infoPlistCannotReadError".loc + } + + var appRelativePath = "\(newAppInfo.bundleIdentifier()!).app" + var outputFolder = LCPath.bundlePath.appendingPathComponent(appRelativePath) + 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.appInfo.bundleIdentifier()! == newAppInfo.bundleIdentifier() + } + if fm.fileExists(atPath: outputFolder.path) || sameBundleIdApp.count > 0 { + appRelativePath = "\(newAppInfo.bundleIdentifier()!)_\(Int(CFAbsoluteTimeGetCurrent())).app" + + self.installOptions = [AppReplaceOption(isReplace: false, nameOfFolderToInstall: appRelativePath)] + + for app in sameBundleIdApp { + self.installOptions.append(AppReplaceOption(isReplace: true, nameOfFolderToInstall: app.appInfo.relativeBundlePath, appToReplace: app)) + } + + guard let installOptionChosen = await installReplaceAlert.open() else { + // user cancelled + self.installprogressVisible = false + try fm.removeItem(at: payloadPath) + return + } + + outputFolder = LCPath.bundlePath.appendingPathComponent(installOptionChosen.nameOfFolderToInstall) + appToReplace = installOptionChosen.appToReplace + if installOptionChosen.isReplace { + try fm.removeItem(at: outputFolder) + self.apps.removeAll { appNow in + return appNow.appInfo.relativeBundlePath == installOptionChosen.nameOfFolderToInstall + } + } + } + // Move it! + try fm.moveItem(at: appFolderPath, to: outputFolder) + let finalNewApp = LCAppInfo(bundlePath: outputFolder.path) + finalNewApp?.relativeBundlePath = appRelativePath + + // patch it + guard let finalNewApp else { + errorInfo = "lc.appList.appInfoInitError".loc + 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) + }, forceSign: false) + }) + + if let signError { + throw signError + } + // set data folder to the folder of the chosen app + if let appToReplace = appToReplace { + finalNewApp.setDataUUID(appToReplace.appInfo.getDataUUIDNoAssign()) + } + DispatchQueue.main.async { + self.apps.append(LCAppModel(appInfo: finalNewApp)) + self.installprogressVisible = false + } + } + + 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: LCAppModel) { + DispatchQueue.main.async { + if app.appInfo.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 launchAppWithBundleId(bundleId : String) async { + if bundleId == "" { + return + } + var appFound : LCAppModel? = nil + var isFoundAppHidden = false + for app in apps { + if app.appInfo.relativeBundlePath == bundleId { + appFound = app + break + } + } + if appFound == nil && !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") { + for app in hiddenApps { + if app.appInfo.relativeBundlePath == bundleId { + appFound = app + isFoundAppHidden = true + break + } + } + } + + if isFoundAppHidden && !sharedModel.isHiddenAppUnlocked { + do { + let result = try await LCUtils.authenticateUser() + if !result { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + } + + 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 { + do { + if !(try await LCUtils.authenticateUser()) { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } + + func installMdm(data: Data) { + 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/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 new file mode 100644 index 0000000..13d2f53 --- /dev/null +++ b/LiveContainerSwiftUI/LCAppSettingsView.swift @@ -0,0 +1,413 @@ +// +// LCAppSettingsView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/9/16. +// + +import Foundation +import SwiftUI + +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? + + @StateObject private var renameFolderInput = InputHelper() + @StateObject private var moveToAppGroupAlert = YesNoHelper() + @StateObject private var moveToPrivateDocAlert = YesNoHelper() + + @State private var errorShow = false + @State private var errorInfo = "" + + @EnvironmentObject private var sharedModel : SharedModel + + init(model: LCAppModel, appDataFolders: Binding<[String]>, tweakFolders: Binding<[String]>) { + self.appInfo = model.appInfo + self._model = ObservedObject(wrappedValue: model) + _appDataFolders = appDataFolders + _tweakFolders = tweakFolders + self._uiPickerDataFolder = State(initialValue: model.uiDataFolder) + self._uiPickerTweakFolder = State(initialValue: model.uiTweakFolder) + } + + var body: some View { + Form { + Section { + HStack { + Text("lc.appSettings.bundleId".loc) + Spacer() + Text(appInfo.relativeBundlePath) + .foregroundColor(.gray) + .multilineTextAlignment(.trailing) + } + if !model.uiIsShared { + Menu { + Button { + Task{ await createFolder() } + } label: { + Label("lc.appSettings.newDataFolder".loc, systemImage: "plus") + } + if model.uiDataFolder != nil { + Button { + Task{ await renameDataFolder() } + } label: { + Label("lc.appSettings.renameDataFolder".loc, systemImage: "pencil") + } + } + + Picker(selection: $uiPickerDataFolder , label: Text("")) { + ForEach(appDataFolders, id:\.self) { folderName in + Button(folderName) { + setDataFolder(folderName: folderName) + }.tag(Optional(folderName)) + } + } + } label: { + HStack { + Text("lc.appSettings.dataFolder".loc) + .foregroundColor(.primary) + Spacer() + Text(model.uiDataFolder == nil ? "lc.appSettings.noDataFolder".loc : model.uiDataFolder!) + .multilineTextAlignment(.trailing) + } + } + .onChange(of: uiPickerDataFolder, perform: { newValue in + if newValue != model.uiDataFolder { + setDataFolder(folderName: newValue) + } + }) + + Menu { + Picker(selection: $uiPickerTweakFolder , label: Text("")) { + Label("lc.common.none".loc, systemImage: "nosign").tag(Optional(nil)) + ForEach(tweakFolders, id:\.self) { folderName in + Text(folderName).tag(Optional(folderName)) + } + } + } label: { + HStack { + Text("lc.appSettings.tweakFolder".loc) + .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("lc.appSettings.dataFolder".loc) + .foregroundColor(.primary) + Spacer() + Text(model.uiDataFolder == nil ? "lc.appSettings.noDataFolder".loc : model.uiDataFolder!) + .foregroundColor(.gray) + .multilineTextAlignment(.trailing) + } + HStack { + Text("lc.appSettings.tweakFolder".loc) + .foregroundColor(.primary) + Spacer() + Text(model.uiTweakFolder == nil ? "lc.common.none".loc : model.uiTweakFolder!) + .foregroundColor(.gray) + .multilineTextAlignment(.trailing) + } + } + + if !model.uiIsShared { + Button("lc.appSettings.toSharedApp".loc) { + Task { await moveToAppGroup()} + } + + } else if LCUtils.multiLCStatus != 2 { + Button("lc.appSettings.toPrivateApp".loc) { + Task { await movePrivateDoc() } + } + } + } header: { + Text("lc.common.data".loc) + } + + + Section { + Toggle(isOn: $model.uiIsJITNeeded) { + Text("lc.appSettings.launchWithJit".loc) + } + .onChange(of: model.uiIsJITNeeded, perform: { newValue in + Task { await setJITNeeded(newValue) } + }) + } footer: { + Text("lc.appSettings.launchWithJitDesc".loc) + } + + if sharedModel.isHiddenAppUnlocked { + Section { + Toggle(isOn: $model.uiIsHidden) { + Text("lc.appSettings.hideApp".loc) + } + .onChange(of: model.uiIsHidden, perform: { newValue in + Task { await toggleHidden() } + }) + } footer: { + Text("lc.appSettings.hideAppDesc".loc) + } + + } + + Section { + Toggle(isOn: $model.uiDoSymlinkInbox) { + Text("lc.appSettings.fixFilePicker".loc) + } + .onChange(of: model.uiDoSymlinkInbox, perform: { newValue in + Task { await setSimlinkInbox(newValue) } + }) + } header: { + Text("lc.appSettings.fixes".loc) + } footer: { + Text("lc.appSettings.fixFilePickerDesc".loc) + } + + Section { + Toggle(isOn: $model.uiBypassAssertBarrierOnQueue) { + Text("lc.appSettings.bypassAssert".loc) + } + .onChange(of: model.uiBypassAssertBarrierOnQueue, perform: { newValue in + Task { await setBypassAssertBarrierOnQueue(newValue) } + }) + + } footer: { + Text("lc.appSettings.bypassAssertDesc".loc) + } + + + Section { + Button("lc.appSettings.forceSign".loc) { + Task { await forceResign() } + } + .disabled(model.isAppRunning) + } footer: { + Text("lc.appSettings.forceSignDesc".loc) + } + + } + .navigationTitle(appInfo.displayName()) + + .alert("lc.common.error".loc, isPresented: $errorShow) { + Button("lc.common.ok".loc, action: { + }) + } message: { + Text(errorInfo) + } + + .textFieldAlert( + isPresented: $renameFolderInput.show, + title: "lc.common.enterNewFolderName".loc, + text: $renameFolderInput.initVal, + placeholder: "", + action: { newText in + renameFolderInput.close(result: newText!) + }, + actionCancel: {_ in + renameFolderInput.close(result: "") + } + ) + .alert("lc.appSettings.toSharedApp".loc, isPresented: $moveToAppGroupAlert.show) { + Button { + self.moveToAppGroupAlert.close(result: true) + } label: { + Text("lc.common.move".loc) + } + Button("lc.common.cancel".loc, role: .cancel) { + self.moveToAppGroupAlert.close(result: false) + } + } message: { + Text("lc.appSettings.toSharedAppDesc".loc) + } + .alert("lc.appSettings.toPrivateApp".loc, isPresented: $moveToPrivateDocAlert.show) { + Button { + self.moveToPrivateDocAlert.close(result: true) + } label: { + Text("lc.common.move".loc) + } + Button("lc.common.cancel".loc, role: .cancel) { + self.moveToPrivateDocAlert.close(result: false) + } + } message: { + Text("lc.appSettings.toPrivateAppDesc".loc) + } + } + + func setDataFolder(folderName: String?) { + self.appInfo.setDataUUID(folderName!) + self.model.uiDataFolder = folderName + self.uiPickerDataFolder = folderName + } + + func createFolder() async { + guard let newName = await renameFolderInput.open(initVal: NSUUID().uuidString), newName != "" else { + return + } + let fm = FileManager() + let dest = LCPath.dataPath.appendingPathComponent(newName) + do { + try fm.createDirectory(at: dest, withIntermediateDirectories: false) + } catch { + errorShow = true + errorInfo = error.localizedDescription + return + } + + self.appDataFolders.append(newName) + self.setDataFolder(folderName: newName) + + } + + func renameDataFolder() async { + if self.appInfo.getDataUUIDNoAssign() == nil { + return + } + + 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(newName) + 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] = newName + self.setDataFolder(folderName: newName) + + } + + func setTweakFolder(folderName: String?) { + self.appInfo.setTweakFolder(folderName) + self.model.uiTweakFolder = folderName + self.uiPickerTweakFolder = folderName + } + + func moveToAppGroup() async { + guard let result = await moveToAppGroupAlert.open(), result else { + 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 = "lc.appSettings.appOpenInOtherLc %@ %@".localizeWithFormat(runningLC!, runningLC!) + errorShow = true + return + } + + guard let result = await moveToPrivateDocAlert.open(), result else { + 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 model.toggleHidden() + } + + func forceResign() async { + do { + try await model.forceResign() + } catch { + errorInfo = error.localizedDescription + errorShow = true + } + } +} diff --git a/LiveContainerSwiftUI/LCSettingsView.swift b/LiveContainerSwiftUI/LCSettingsView.swift new file mode 100644 index 0000000..29c96e8 --- /dev/null +++ b/LiveContainerSwiftUI/LCSettingsView.swift @@ -0,0 +1,527 @@ +// +// LCSettingsView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation +import SwiftUI + +struct LCSettingsView: View { + @State var errorShow = false + @State var errorInfo = "" + @State var successShow = false + @State var successInfo = "" + + @Binding var apps: [LCAppModel] + @Binding var hiddenApps: [LCAppModel] + @Binding var appDataFolderNames: [String] + + @StateObject private var appFolderRemovalAlert = YesNoHelper() + @State private var folderRemoveCount = 0 + + @StateObject private var keyChainRemovalAlert = YesNoHelper() + + @State var isJitLessEnabled = false + + @State var isAltCertIgnored = false + @State var frameShortIcon = false + @State var silentSwitchApp = false + @State var injectToLCItelf = false + @State var strictHiding = false + + @State var sideJITServerAddress : String + @State var deviceUDID: String + + @EnvironmentObject private var sharedModel : SharedModel + + 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")) + _silentSwitchApp = State(initialValue: UserDefaults.standard.bool(forKey: "LCSwitchAppWithoutAsking")) + _injectToLCItelf = State(initialValue: UserDefaults.standard.bool(forKey: "LCLoadTweaksToSelf")) + + _apps = apps + _hiddenApps = hiddenApps + _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: "") + } + _strictHiding = State(initialValue: LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding")) + + } + + var body: some View { + NavigationView { + Form { + if LCUtils.multiLCStatus != 2 { + Section{ + Button { + setupJitLess() + } label: { + if isJitLessEnabled { + Text("lc.settings.renewJitLess".loc) + } else { + Text("lc.settings.setupJitLess".loc) + } + } + } header: { + Text("lc.settings.jitLess".loc) + } footer: { + Text("lc.settings.jitLessDesc".loc) + } + } + + Section{ + Button { + installAnotherLC() + } label: { + if LCUtils.multiLCStatus == 0 { + Text("lc.settings.multiLCInstall".loc) + } else if LCUtils.multiLCStatus == 1 { + Text("lc.settings.multiLCReinstall".loc) + } else if LCUtils.multiLCStatus == 2 { + Text("lc.settings.multiLCIsSecond".loc) + } + + } + .disabled(LCUtils.multiLCStatus == 2) + } header: { + Text("lc.settings.multiLC".loc) + } footer: { + Text("lc.settings.multiLCDesc".loc) + } + + + Section { + Toggle(isOn: $isAltCertIgnored) { + Text("lc.settings.ignoreAltCert".loc) + } + } footer: { + Text("lc.settings.ignoreAltCertDesc".loc) + } + + Section { + HStack { + Text("lc.settings.JitAddress".loc) + Spacer() + TextField("http://x.x.x.x:8080", text: $sideJITServerAddress) + .multilineTextAlignment(.trailing) + } + HStack { + Text("lc.settings.JitUDID".loc) + Spacer() + TextField("", text: $deviceUDID) + .multilineTextAlignment(.trailing) + } + } header: { + Text("JIT") + } footer: { + 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("lc.settings.silentSwitchApp".loc) + } + } footer: { + Text("lc.settings.silentSwitchAppDesc".loc) + } + + Section { + Toggle(isOn: $injectToLCItelf) { + Text("lc.settings.injectLCItself".loc) + } + } footer: { + Text("lc.settings.injectLCItselfDesc".loc) + } + + if sharedModel.isHiddenAppUnlocked { + Section { + Toggle(isOn: $strictHiding) { + Text("lc.settings.strictHiding".loc) + } + } footer: { + Text("lc.settings.strictHidingDesc".loc) + } + } + + Section { + if LCUtils.multiLCStatus != 2 { + Button { + moveAppGroupFolderFromPrivateToAppGroup() + } label: { + Text("lc.settings.appGroupPrivateToShare".loc) + } + Button { + moveAppGroupFolderFromAppGroupToPrivate() + } label: { + Text("lc.settings.appGroupShareToPrivate".loc) + } + + Button { + Task { await moveDanglingFolders() } + } label: { + Text("lc.settings.moveDanglingFolderOut".loc) + } + Button(role:.destructive) { + Task { await cleanUpUnusedFolders() } + } label: { + Text("lc.settings.cleanDataFolder".loc) + } + } + + Button(role:.destructive) { + Task { await removeKeyChain() } + } label: { + 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{ + Text(LCUtils.getVersionInfo()) + .foregroundStyle(.gray) + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .background(Color(UIColor.systemGroupedBackground)) + .listRowInsets(EdgeInsets()) + } + .navigationBarTitle("lc.tabView.settings".loc) + .alert("lc.common.error".loc, isPresented: $errorShow){ + } message: { + Text(errorInfo) + } + .alert("lc.common.success".loc, isPresented: $successShow){ + } message: { + Text(successInfo) + } + .alert("lc.settings.cleanDataFolder".loc, isPresented: $appFolderRemovalAlert.show) { + if folderRemoveCount > 0 { + Button(role: .destructive) { + appFolderRemovalAlert.close(result: true) + } label: { + Text("lc.common.delete".loc) + } + } + + Button("lc.common.cancel".loc, role: .cancel) { + appFolderRemovalAlert.close(result: false) + } + } message: { + if folderRemoveCount > 0 { + Text("lc.settings.cleanDataFolderConfirm".localizeWithFormat(folderRemoveCount)) + } else { + Text("lc.settings.noDataFolderToClean".loc) + } + + } + .alert("lc.settings.cleanKeychain".loc, isPresented: $keyChainRemovalAlert.show) { + Button(role: .destructive) { + keyChainRemovalAlert.close(result: true) + } label: { + Text("lc.common.delete".loc) + } + + Button("lc.common.cancel".loc, role: .cancel) { + keyChainRemovalAlert.close(result: false) + } + } message: { + Text("lc.settings.cleanKeychainDesc".loc) + } + .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) + } + .onChange(of: strictHiding) { newValue in + saveAppGroupItem(key: "LCStrictHiding", 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: 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 = "lc.settings.unsupportedInstallMethod".loc + 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 + } + + } + + func installAnotherLC() { + if !LCUtils.isAppGroupAltStoreLike() { + errorInfo = "lc.settings.unsupportedInstallMethod".loc + 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:LCAppModel] = [:] + for app in apps { + guard let folderName = app.appInfo.getDataUUIDNoAssign() else { + continue + } + folderNameToAppDict[folderName] = app + } + for app in hiddenApps { + guard let folderName = app.appInfo.getDataUUIDNoAssign() else { + continue + } + folderNameToAppDict[folderName] = app + } + + var foldersToDelete : [String] = [] + for appDataFolderName in appDataFolderNames { + if folderNameToAppDict[appDataFolderName] == nil { + foldersToDelete.append(appDataFolderName) + } + } + folderRemoveCount = foldersToDelete.count + + guard let result = await appFolderRemovalAlert.open(), result else { + 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 + } + + } + + func removeKeyChain() async { + guard let result = await keyChainRemovalAlert.open(), result else { + 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 + errorInfo = status.description + errorShow = true + } + } + } + + func moveDanglingFolders() async { + let fm = FileManager() + do { + var appDataFoldersInUse : Set = Set(); + var tweakFoldersInUse : Set = Set(); + for app in apps { + if !app.appInfo.isShared { + continue + } + if let folder = app.appInfo.getDataUUIDNoAssign() { + appDataFoldersInUse.update(with: folder); + } + if let folder = app.appInfo.tweakFolder() { + tweakFoldersInUse.update(with: folder); + } + + } + + for app in hiddenApps { + if !app.appInfo.isShared { + continue + } + if let folder = app.appInfo.getDataUUIDNoAssign() { + appDataFoldersInUse.update(with: folder); + } + if let folder = app.appInfo.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 = "lc.settings.moveDanglingFolderComplete %lld %lld".localizeWithFormat(movedDataFolderCount,movedTweakFolderCount) + successShow = true + + } catch { + errorInfo = error.localizedDescription + 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 = "lc.settings.appGroupExistPrivate".loc + errorShow = true + return + } + for file in sharedFolderContents { + try fm.moveItem(at: file, to: LCPath.appGroupPath.appendingPathComponent(file.lastPathComponent)) + } + successInfo = "lc.settings.appGroup.moveSuccess".loc + 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 = "lc.settings.appGroupExist Shared".loc + errorShow = true + return + } + for file in privateFolderContents { + try fm.moveItem(at: file, to: LCPath.lcGroupAppGroupPath.appendingPathComponent(file.lastPathComponent)) + } + successInfo = "lc.settings.appGroup.moveSuccess".loc + successShow = true + + } catch { + errorInfo = error.localizedDescription + 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/LCSwiftBridge.h b/LiveContainerSwiftUI/LCSwiftBridge.h new file mode 100644 index 0000000..cc1a567 --- /dev/null +++ b/LiveContainerSwiftUI/LCSwiftBridge.h @@ -0,0 +1,18 @@ +// +// 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; ++ (void)openWebPageWithUrlStr:(NSURL* _Nonnull)url; ++ (void)launchAppWithBundleId:(NSString* _Nonnull)bundleId; +@end diff --git a/LiveContainerSwiftUI/LCSwiftBridge.m b/LiveContainerSwiftUI/LCSwiftBridge.m new file mode 100644 index 0000000..2fd3332 --- /dev/null +++ b/LiveContainerSwiftUI/LCSwiftBridge.m @@ -0,0 +1,34 @@ +// +// ObjcBridge.m +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/22. +// + +#import + +#import "LCSwiftBridge.h" + +@implementation LCSwiftBridge + ++ (UIViewController * _Nonnull)getRootVC { + return [LCObjcBridge getRootVC]; +} + ++ (void)openWebPageWithUrlStr:(NSString* _Nonnull)urlStr { + [LCObjcBridge openWebPageWithUrlStr:urlStr]; +} + ++ (void)launchAppWithBundleId:(NSString* _Nonnull)bundleId { + [LCObjcBridge launchAppWithBundleId: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/LCTabView.swift b/LiveContainerSwiftUI/LCTabView.swift new file mode 100644 index 0000000..ee95df5 --- /dev/null +++ b/LiveContainerSwiftUI/LCTabView.swift @@ -0,0 +1,136 @@ +// +// TabView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation +import SwiftUI + +struct LCTabView: View { + @State var apps: [LCAppModel] + @State var hiddenApps: [LCAppModel] + @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: [LCAppModel] = [] + var tempHiddenApps: [LCAppModel] = [] + + 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 + newApp.isShared = false + if newApp.isHidden { + tempHiddenApps.append(LCAppModel(appInfo: newApp)) + } else { + tempApps.append(LCAppModel(appInfo: 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 + if newApp.isHidden { + tempHiddenApps.append(LCAppModel(appInfo: newApp)) + } else { + tempApps.append(LCAppModel(appInfo: 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) + _hiddenApps = State(initialValue: tempHiddenApps) + } + + var body: some View { + TabView { + LCAppListView(apps: $apps, hiddenApps: $hiddenApps, appDataFolderNames: $appDataFolderNames, tweakFolderNames: $tweakFolderNames) + .tabItem { + Label("lc.tabView.apps".loc, systemImage: "square.stack.3d.up.fill") + } + if LCUtils.multiLCStatus != 2 { + LCTweaksView(tweakFolders: $tweakFolderNames) + .tabItem{ + Label("lc.tabView.tweaks".loc, systemImage: "wrench.and.screwdriver") + } + } + + LCSettingsView(apps: $apps, hiddenApps: $hiddenApps, appDataFolderNames: $appDataFolderNames) + .tabItem { + Label("lc.tabView.settings".loc, systemImage: "gearshape.fill") + } + } + .alert("lc.common.error".loc, isPresented: $errorShow){ + Button("lc.common.ok".loc, action: { + }) + Button("lc.common.copy".loc, action: { + copyError() + }) + } message: { + Text(errorInfo) + } + .onAppear() { + checkLastLaunchError() + } + .environmentObject(DataManager.shared.model) + } + + func checkLastLaunchError() { + guard let errorStr = UserDefaults.standard.string(forKey: "error") else { + return + } + UserDefaults.standard.removeObject(forKey: "error") + errorInfo = errorStr + errorShow = true + } + + func copyError() { + UIPasteboard.general.string = errorInfo + } +} diff --git a/LiveContainerSwiftUI/LCTweaksView.swift b/LiveContainerSwiftUI/LCTweaksView.swift new file mode 100644 index 0000000..4c79c81 --- /dev/null +++ b/LiveContainerSwiftUI/LCTweaksView.swift @@ -0,0 +1,422 @@ +// +// LCTweaksView.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation +import SwiftUI +import UniformTypeIdentifiers + +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 = "" + + @StateObject private var newFolderInput = InputHelper() + + @StateObject private var renameFileInput = InputHelper() + + @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 + 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 { + 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") + } + } + .contextMenu { + Button { + Task { await renameTweakItem(tweakItem: tweakItem)} + } label: { + Label("lc.common.rename".loc, systemImage: "pencil") + } + + Button(role: .destructive) { + deleteTweakItem(tweakItem: tweakItem) + } label: { + Label("lc.common.delete".loc, systemImage: "trash") + } + } + + }.onDelete { indexSet in + deleteTweakItem(indexSet: indexSet) + } + } + Section { + VStack{ + if isRoot { + Text("lc.tweakView.globalFolderDesc".loc) + .foregroundStyle(.gray) + .font(.system(size: 12)) + } else { + Text("lc.tweakView.appFolderDesc".loc) + .foregroundStyle(.gray) + .font(.system(size: 12)) + } + + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .center) + .background(Color(UIColor.systemGroupedBackground)) + .listRowInsets(EdgeInsets()) + } + + } + .navigationTitle(isRoot ? "lc.tabView.tweaks".loc : baseUrl.lastPathComponent) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + if !isTweakSigning && LCUtils.certificatePassword() != nil { + Button { + Task { await signAllTweaks() } + } label: { + Label("sign".loc, systemImage: "signature") + } + } + + } + ToolbarItem(placement: .topBarTrailing) { + if !isTweakSigning { + Menu { + Button { + if choosingTweak { + choosingTweak = false + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1, execute: { + choosingTweak = true + }) + } else { + choosingTweak = true + } + } label: { + Label("lc.tweakView.importTweak".loc, systemImage: "square.and.arrow.down") + } + + Button { + Task { await createNewFolder() } + } label: { + Label("lc.tweakView.newFolder".loc, systemImage: "folder.badge.plus") + } + } label: { + Label("add", systemImage: "plus") + } + } else { + ProgressView().progressViewStyle(.circular) + } + + } + } + .alert("lc.common.error".loc, isPresented: $errorShow) { + Button("lc.common.ok".loc, action: { + }) + } message: { + Text(errorInfo) + } + .textFieldAlert( + isPresented: $newFolderInput.show, + title: "lc.common.enterNewFolderName".loc, + text: $newFolderInput.initVal, + placeholder: "", + action: { newText in + newFolderInput.close(result: newText) + }, + actionCancel: {_ in + newFolderInput.close(result: "") + } + ) + .textFieldAlert( + isPresented: $renameFileInput.show, + title: "lc.common.enterNewName".loc, + text: $renameFileInput.initVal, + placeholder: "", + action: { newText in + renameFileInput.close(result: newText) + }, + actionCancel: {_ in + renameFileInput.close(result: "") + } + ) + .fileImporter(isPresented: $choosingTweak, allowedContentTypes: [.dylib, .lcFramework, .deb], allowsMultipleSelection: true) { result in + Task { await startInstallTweak(result) } + } + } + + 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 + return + } + if isRoot { + for iToRemove in indexToRemove { + tweakFolders.removeAll(where: { s in + return s == tweakItems[iToRemove].fileUrl.lastPathComponent + }) + } + } + + 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 { + guard let newName = await renameFileInput.open(initVal: tweakItem.fileUrl.lastPathComponent), newName != "" else { + return + } + + let indexToRename = tweakItems.firstIndex(where: { s in + return s == tweakItem + }) + guard let indexToRename = indexToRename else { + return + } + let newUrl = self.baseUrl.appendingPathComponent(newName) + + 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(newName, at: indexToRename2) + + } + } + + 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 { + guard let newName = await renameFileInput.open(), newName != "" else { + return + } + let fm = FileManager() + let dest = baseUrl.appendingPathComponent(newName) + 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(newName) + } + } + + 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 "lc.tweakView.permissionDenied %@".localizeWithFormat(fileUrl.lastPathComponent) + } + if(!fileUrl.isFileURL) { + throw "lc.tweakView.notFileError %@".localizeWithFormat(fileUrl.lastPathComponent) + } + 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 + 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) + 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 + } + + + + } +} + +struct LCTweaksView: View { + @Binding var tweakFolders : [String] + + var body: some View { + NavigationView { + LCTweakFolderView(baseUrl: LCPath.tweakPath, isRoot: true, tweakFolders: $tweakFolders) + } + .navigationViewStyle(StackNavigationViewStyle()) + + } +} diff --git a/LiveContainerSwiftUI/LCWebView.swift b/LiveContainerSwiftUI/LCWebView.swift new file mode 100644 index 0000000..3c65239 --- /dev/null +++ b/LiveContainerSwiftUI/LCWebView.swift @@ -0,0 +1,397 @@ +// +// SwiftUIView.swift +// +// 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 : [LCAppModel] + @Binding var hiddenApps : [LCAppModel] + + @State private var runAppAlert = YesNoHelper() + @State private var runAppAlertMsg = "" + + @State private var errorShow = false + @State private var errorInfo = "" + + @EnvironmentObject private var sharedModel : SharedModel + + init(url: Binding, apps: Binding<[LCAppModel]>, hiddenApps: Binding<[LCAppModel]>, isPresent: Binding) { + self.webView = WebView() + self._url = url + self._apps = apps + self._isPresent = isPresent + self._hiddenApps = hiddenApps + } + + 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("lc.common.done".loc) + }) + + } + .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("lc.webView.runApp".loc, isPresented: $runAppAlert.show) { + Button("lc.appBanner.run".loc, action: { + runAppAlert.close(result: true) + }) + Button("lc.common.cancel".loc, role: .cancel, action: { + runAppAlert.close(result: false) + }) + } message: { + Text(runAppAlertMsg) + } + .alert("lc.common.error".loc, isPresented: $errorShow) { + Button("lc.common.ok".loc, action: { + }) + } message: { + Text(errorInfo) + } + + } + + func onViewAppear() { + let observer = WebViewLoadObserver(loadStatus: $loadStatus, webView: self.webView.webView) + let webViewDelegate = WebViewDelegate(pageTitle: $pageTitle, urlSchemeHandler:onURLSchemeDetected, universalLinkHandler: onUniversalLinkDetected) + webView.setDelegate(delegete: webViewDelegate) + 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("[LC] 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 : LCAppModel? = nil + 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.appInfo.urlSchemes() { + for scheme in schemes { + if let scheme = scheme as? String, scheme == url.scheme { + appToLaunch = app + break appLoop + } + } + } + } + } + + + guard let appToLaunch = appToLaunch else { + errorInfo = "lc.appList.schemeCannotOpenError %@".localizeWithFormat(url.scheme!) + errorShow = true + return + } + + if appToLaunch.appInfo.isHidden && !sharedModel.isHiddenAppUnlocked { + + do { + if !(try await LCUtils.authenticateUser()) { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } + + runAppAlertMsg = "lc.webView.pageLaunch %@".localizeWithFormat(appToLaunch.appInfo.displayName()!) + + if let doRunApp = await runAppAlert.open(), !doRunApp { + return + } + + launchToApp(bundleId: appToLaunch.appInfo.relativeBundlePath!, url: url) + + } + + public func onUniversalLinkDetected(url: URL, bundleIDs: [String]) async { + var bundleIDToAppDict: [String: LCAppModel] = [:] + for app in apps { + bundleIDToAppDict[app.appInfo.bundleIdentifier()!] = app + } + if !LCUtils.appGroupUserDefault.bool(forKey: "LCStrictHiding") || sharedModel.isHiddenAppUnlocked { + for app in hiddenApps { + bundleIDToAppDict[app.appInfo.bundleIdentifier()!] = app + } + } + + var appToLaunch: LCAppModel? = nil + for bundleID in bundleIDs { + if let app = bundleIDToAppDict[bundleID] { + appToLaunch = app + break + } + } + guard let appToLaunch = appToLaunch else { + return + } + + if appToLaunch.appInfo.isHidden && !sharedModel.isHiddenAppUnlocked { + do { + if !(try await LCUtils.authenticateUser()) { + return + } + } catch { + errorInfo = error.localizedDescription + errorShow = true + return + } + } + + runAppAlertMsg = "lc.webView.pageCanBeOpenIn %@".localizeWithFormat(appToLaunch.appInfo.displayName()!) + if let doRunApp = await runAppAlert.open(), !doRunApp { + return + } + launchToApp(bundleId: appToLaunch.appInfo.relativeBundlePath!, url: url) + } +} + +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) 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) async -> Void, universalLinkHandler: @escaping (URL , [String]) async -> Void) { + self.pageTitle = pageTitle + self.urlSchemeHandler = urlSchemeHandler + self.universalLinkHandler = universalLinkHandler + super.init() + } + + func webView(_ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + decisionHandler((WKNavigationActionPolicy)(rawValue: WKNavigationActionPolicy.allow.rawValue + 2)!) + guard let url = navigationAction.request.url, let scheme = navigationAction.request.url?.scheme else { + return + } + if(scheme == "https") { + Task { + await self.loadDomainAssociations(url: url) + if let host = url.host, let appIDs = self.domainBundleIdDict[host] { + Task{ await self.universalLinkHandler(url, appIDs) } + } + } + return + } + if(scheme == "http" || scheme == "about" || scheme == "itms-appss") { + return; + } + Task{ await urlSchemeHandler(url) } + + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + self.pageTitle.wrappedValue = webView.title! + } + + + func loadDomainAssociations(url: URL) async { + if url.scheme != "https" || url.host == nil { + return + } + if self.domainBundleIdDict[url.host!] != nil { + return + } + guard let host = url.host else { + return + } + + // 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")! + ] + + 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() + } + } + } + } + + } +} + +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-Bridging-Header.h b/LiveContainerSwiftUI/LiveContainerSwiftUI-Bridging-Header.h new file mode 100644 index 0000000..4e9c47c --- /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" +#include "unarchive.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..55c7be9 --- /dev/null +++ b/LiveContainerSwiftUI/LiveContainerSwiftUI.xcodeproj/project.pbxproj @@ -0,0 +1,395 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + 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 */; }; + 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 */; }; + 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 */; }; + 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 */ + +/* 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 = ""; }; + 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 = ""; }; + 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 = ""; }; + 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 */ + +/* Begin PBXFrameworksBuildPhase section */ + 17B9B88A2C760678009D079E /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 173564BB2C76FE1500C6C918 /* LiveContainerSwiftUI */ = { + isa = PBXGroup; + children = ( + 173F183F2C7B7B74002953AA /* LCWebView.swift */, + 173564C82C76FE3500C6C918 /* Assets.xcassets */, + 173564C72C76FE3500C6C918 /* LCAppBanner.swift */, + 173564BC2C76FE3500C6C918 /* LCAppListView.swift */, + 173564C12C76FE3500C6C918 /* LCSettingsView.swift */, + 173564BD2C76FE3500C6C918 /* LCTabView.swift */, + 17C536F32C98529D006C2C75 /* LCAppSettingsView.swift */, + 173564C02C76FE3500C6C918 /* LCTweaksView.swift */, + 173564C32C76FE3500C6C918 /* Makefile */, + 173564C62C76FE3500C6C918 /* ObjcBridge.swift */, + 178B4C3F2C7766A300DD1F74 /* LiveContainerSwiftUI-Bridging-Header.h */, + 178B4C3D2C77654400DD1F74 /* Shared.swift */, + 173564C22C76FE3500C6C918 /* LCSwiftBridge.h */, + 173564C42C76FE3500C6C918 /* LCSwiftBridge.m */, + 170C3DF82C99A489007F86FB /* Localizable.xcstrings */, + 17A7640B2C9D1B6C00456519 /* LCAppModel.swift */, + ); + name = LiveContainerSwiftUI; + 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, + zh_CN, + no, + ); + mainGroup = 17B9B8842C760678009D079E; + productRefGroup = 17B9B88E2C760678009D079E /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 17B9B88C2C760678009D079E /* LiveContainerSwiftUI */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 17B9B88B2C760678009D079E /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 170C3DF92C99A489007F86FB /* Localizable.xcstrings in Resources */, + 173564D32C76FE3500C6C918 /* Assets.xcassets in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 17B9B8892C760678009D079E /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 178B4C3E2C77654400DD1F74 /* Shared.swift in Sources */, + 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 */, + 173564CC2C76FE3500C6C918 /* LCTweaksView.swift in Sources */, + 173564CD2C76FE3500C6C918 /* LCSettingsView.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 = 15.0; + 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 = 15.0; + 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_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 = 15.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_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 = 15.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/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/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/Makefile b/LiveContainerSwiftUI/Makefile new file mode 100644 index 0000000..7668c0c --- /dev/null +++ b/LiveContainerSwiftUI/Makefile @@ -0,0 +1,20 @@ +TARGET := iphone:clang:latest:15.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 \ +../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 \ No newline at end of file diff --git a/LiveContainerSwiftUI/ObjcBridge.swift b/LiveContainerSwiftUI/ObjcBridge.swift new file mode 100644 index 0000000..5a76e13 --- /dev/null +++ b/LiveContainerSwiftUI/ObjcBridge.swift @@ -0,0 +1,58 @@ +// +// text.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/21. +// + +import Foundation +import SwiftUI + + +@objc public class LCObjcBridge: NSObject { + 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 { + urlStrToOpen = urlStr + } else { + Task { await openUrlStrFunc!(urlStr) } + } + } + + @objc public static func launchApp(bundleId: String) { + if launchAppFunc == nil { + bundleToLaunch = bundleId + } else { + Task { await launchAppFunc!(bundleId) } + } + } + + @objc public static func getRootVC() -> UIViewController { + let rootView = LCTabView() + let rootVC = UIHostingController(rootView: rootView) + return rootVC + } +} diff --git a/LiveContainerSwiftUI/Resources/Info.plist b/LiveContainerSwiftUI/Resources/Info.plist new file mode 100644 index 0000000..905a258 Binary files /dev/null and b/LiveContainerSwiftUI/Resources/Info.plist differ diff --git a/LiveContainerSwiftUI/Shared.swift b/LiveContainerSwiftUI/Shared.swift new file mode 100644 index 0000000..80e109d --- /dev/null +++ b/LiveContainerSwiftUI/Shared.swift @@ -0,0 +1,415 @@ +// +// Shared.swift +// LiveContainerSwiftUI +// +// Created by s s on 2024/8/22. +// + +import SwiftUI +import UniformTypeIdentifiers +import LocalAuthentication +import SafariServices + +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 appGroupPath = docPath.appendingPathComponent("Data/AppGroup") + 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 appGroupPathUrl = LCUtils.appGroupPath() { + return appGroupPathUrl.appendingPathComponent("LiveContainer") + } 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 lcGroupAppGroupPath = lcGroupDocPath.appendingPathComponent("Data/AppGroup") + 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) + } + } +} + +class SharedModel: ObservableObject { + @Published var isHiddenAppUnlocked = false +} + +class DataManager { + static let shared = 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 } + + 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")! + static let deb = UTType(filenameExtension: "deb")! + 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 { + + 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: "lc.common.cancel".loc, style: .cancel) { _ in + self.actionCancel(nil) + shutdown() + }) + controller.addAction(UIAlertAction(title: "lc.common.ok".loc, style: .default) { _ in + self.action(controller.textFields?.first?.text) + shutdown() + }) + return controller + } + + private func shutdown() { + isPresented = false + alertController = nil + } + +} + +struct ImageDocument: FileDocument { + var data: Data + + static var readableContentTypes: [UTType] { + [UTType.image] // Specify that the document supports image files + } + + // Initialize with data + init(uiImage: UIImage) { + self.data = uiImage.pngData()! + } + + // Function to read the data from the file + init(configuration: ReadConfiguration) throws { + guard let data = configuration.file.regularFileContents else { + throw CocoaError(.fileReadCorruptFile) + } + self.data = data + } + + // Write data to the file + func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { + return FileWrapper(regularFileWithContents: data) + } +} + + + +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? +} + +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" { + 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 + 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 = "lc.utils.initSigningError".loc + c.resume() + return + } + onProgressCreated(progress) + } + 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 + } + + 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 = "lc.utils.requireAuthentication".loc + + // 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.userCancel || 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 + } + DispatchQueue.main.async { + DataManager.shared.model.isHiddenAppUnlocked = true + } + return true + } +} diff --git a/LiveContainerUI/LCAppDelegateSwiftUI.h b/LiveContainerUI/LCAppDelegateSwiftUI.h new file mode 100644 index 0000000..2b304af --- /dev/null +++ b/LiveContainerUI/LCAppDelegateSwiftUI.h @@ -0,0 +1,14 @@ +#import +#import +@interface LCSwiftBridge : NSObject ++ (UIViewController * _Nonnull)getRootVC; ++ (void)openWebPageWithUrlStr:(NSString* _Nonnull)urlStr; ++ (void)launchAppWithBundleId:(NSString* _Nonnull)bundleId; +@end + +@interface LCAppDelegateSwiftUI : UIResponder + +@property (nonatomic, strong) UIWindow * _Nullable window; +@property (nonatomic, strong) UIViewController * _Nonnull rootViewController; + +@end diff --git a/LiveContainerUI/LCAppDelegateSwiftUI.m b/LiveContainerUI/LCAppDelegateSwiftUI.m new file mode 100644 index 0000000..a9a6e91 --- /dev/null +++ b/LiveContainerUI/LCAppDelegateSwiftUI.m @@ -0,0 +1,40 @@ +#import "LCAppDelegateSwiftUI.h" +#import +#import "LCUtils.h" +#import "LCSharedUtils.h" + +@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]; + [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"]) { + [NSClassFromString(@"LCSwiftBridge") launchAppWithBundleId:queryItem.value]; + break; + } + } + } + + return NO; +} + +@end diff --git a/LiveContainerUI/LCAppInfo.h b/LiveContainerUI/LCAppInfo.h index 145c775..329f241 100644 --- a/LiveContainerUI/LCAppInfo.h +++ b/LiveContainerUI/LCAppInfo.h @@ -6,6 +6,13 @@ NSString* _bundlePath; } @property NSString* relativeBundlePath; +@property bool isShared; +@property bool isJITNeeded; +@property bool isHidden; +@property bool doSymlinkInbox; +@property bool bypassAssertBarrierOnQueue; + +- (void)setBundlePath:(NSString*)newBundlePath; - (NSMutableDictionary*)info; - (UIImage*)icon; - (NSString*)displayName; @@ -13,6 +20,7 @@ - (NSString*)bundleIdentifier; - (NSString*)version; - (NSString*)dataUUID; +- (NSString*)getDataUUIDNoAssign; - (NSString*)tweakFolder; - (NSMutableArray*) urlSchemes; - (void)setDataUUID:(NSString *)uuid; @@ -20,4 +28,5 @@ - (instancetype)initWithBundlePath:(NSString*)bundlePath; - (NSDictionary *)generateWebClipConfig; - (void)save; +- (void)patchExecAndSignIfNeedWithCompletionHandler:(void(^)(NSString* errorInfo))completetionHandler progressHandler:(void(^)(NSProgress* errorInfo))progressHandler forceSign:(BOOL)forceSign; @end diff --git a/LiveContainerUI/LCAppInfo.m b/LiveContainerUI/LCAppInfo.m index cd6f85b..e88b306 100644 --- a/LiveContainerUI/LCAppInfo.m +++ b/LiveContainerUI/LCAppInfo.m @@ -1,11 +1,14 @@ +@import CommonCrypto; + #import #import #import "LCAppInfo.h" +#import "LCUtils.h" @implementation LCAppInfo - (instancetype)initWithBundlePath:(NSString*)bundlePath { - self = [super init]; - + self = [super init]; + self.isShared = false; if(self) { _bundlePath = bundlePath; _info = [NSMutableDictionary dictionaryWithContentsOfFile:[NSString stringWithFormat:@"%@/Info.plist", bundlePath]]; @@ -14,6 +17,10 @@ - (instancetype)initWithBundlePath:(NSString*)bundlePath { return self; } +- (void)setBundlePath:(NSString*)newBundlePath { + _bundlePath = newBundlePath; +} + - (NSMutableArray*)urlSchemes { // find all url schemes NSMutableArray* urlSchemes = [[NSMutableArray alloc] init]; @@ -68,6 +75,10 @@ - (NSString*)dataUUID { return _info[@"LCDataUUID"]; } +- (NSString*)getDataUUIDNoAssign { + return _info[@"LCDataUUID"]; +} + - (NSString*)tweakFolder { return _info[@"LCTweakFolder"]; } @@ -92,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"]; } @@ -127,7 +142,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", @@ -152,4 +167,168 @@ - (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 +- (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) { + completetionHandler(@"Info.plist not found"); + return; + } + + // 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) { + completetionHandler(error); + return; + } + info[@"LCPatchRevision"] = @(currentPatchRev); + [info writeToFile:infoPath atomically:YES]; + } + + if (!LCUtils.certificatePassword) { + completetionHandler(nil); + return; + } + + 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 { + 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 || forceSign) { + NSURL *appPathURL = [NSURL fileURLWithPath:appPath]; + [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"]; + + __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); + } + }]; + + } else { + // no need to sign again + completetionHandler(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]; + +} + +- (bool)isHidden { + if(_info[@"isHidden"] != nil) { + return [_info[@"isHidden"] boolValue]; + } else { + return NO; + } +} +- (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]; + +} + +- (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/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/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 07c7ea1..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 @@ -15,6 +16,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; @@ -22,6 +24,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; @@ -29,7 +32,9 @@ 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 *)appUrlScheme; + (NSURL *)appGroupPath; + (NSString *)storeInstallURLScheme; - ++ (NSString *)getVersionInfo; @end diff --git a/LiveContainerUI/LCUtils.m b/LiveContainerUI/LCUtils.m index 293ed73..bcb13f8 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 @@ -55,7 +56,13 @@ + (NSData *)certificateDataFile { } + (NSData *)certificateDataProperty { - return [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificateData"]; + NSData* ans = [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificateData"]; + if(ans) { + return ans; + } else { + return [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificateData"]; + } + } + (NSData *)certificateData { @@ -65,7 +72,11 @@ + (NSData *)certificateData { + (NSString *)certificatePassword { if (self.certificateDataFile) { - return [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificatePassword"];; + NSString* ans = [[[NSUserDefaults alloc] initWithSuiteName:[self appGroupID]] objectForKey:@"LCCertificatePassword"]; + if(ans) { + return ans; + } + return [NSUserDefaults.standardUserDefaults objectForKey:@"LCCertificatePassword"]; } else if (self.certificateDataProperty) { return @""; } else { @@ -75,6 +86,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 @@ -82,6 +94,10 @@ + (BOOL)launchToGuestApp { return [NSClassFromString(@"LCSharedUtils") launchToGuestApp]; } ++ (BOOL)askForJIT { + return [NSClassFromString(@"LCSharedUtils") askForJIT]; +} + + (BOOL)launchToGuestAppWithURL:(NSURL *)url { return [NSClassFromString(@"LCSharedUtils") launchToGuestAppWithURL:url]; } @@ -210,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]; @@ -225,6 +241,10 @@ + (NSString *)appGroupID { return appGroupID; } ++ (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]; @@ -299,4 +319,80 @@ + (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 lowercaseString]; + 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]; + + 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]; +} + @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 55e5bb4..22f74d4 100644 --- a/Makefile +++ b/Makefile @@ -19,11 +19,13 @@ $(APPLICATION_NAME)_FRAMEWORKS = UIKit include $(THEOS_MAKE_PATH)/application.mk -SUBPROJECTS += LiveContainerUI TweakLoader TestJITLess +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 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 @mv $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/LiveContainer $(THEOS_STAGING_DIR)/Applications/LiveContainer.app/LiveContainer_PleaseDoNotShortenTheExecutableNameBecauseItIsUsedToReserveSpaceForOverwritingThankYou diff --git a/Resources/AppIcon60x60_2.png b/Resources/AppIcon60x60_2.png new file mode 100644 index 0000000..078c34d Binary files /dev/null and b/Resources/AppIcon60x60_2.png differ diff --git a/Resources/AppIcon76x76_2.png b/Resources/AppIcon76x76_2.png new file mode 100644 index 0000000..fb80be3 Binary files /dev/null and b/Resources/AppIcon76x76_2.png differ diff --git a/Resources/Info.plist b/Resources/Info.plist index 754ce2a..d8d4c04 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -67,6 +67,9 @@ LSApplicationQueriesSchemes sidestore + livecontainer + livecontainer2 + livecontainer3 LSRequiresIPhoneOS diff --git a/TweakLoader/DocumentPicker.m b/TweakLoader/DocumentPicker.m new file mode 100644 index 0000000..b1ffbac --- /dev/null +++ b/TweakLoader/DocumentPicker.m @@ -0,0 +1,74 @@ +@import UniformTypeIdentifiers; +#import "LCSharedUtils.h" +#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 { + + // prevent crash when selecting only folder + BOOL shouldMultiselect = NO; + if (fixFilePicker && [contentTypes count] == 1 && contentTypes[0] == 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) { + [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 + + +@implementation UIDocumentBrowserViewController(LiveContainerHook) + +- (instancetype)hook_initForOpeningContentTypes:(NSArray *)contentTypes { + NSArray * contentTypesNew = @[UTTypeItem, UTTypeFolder]; + return [self hook_initForOpeningContentTypes:contentTypesNew]; +} + +@end + + +@implementation NSURL(LiveContainerHook) + +- (BOOL)hook_startAccessingSecurityScopedResource { + [self hook_startAccessingSecurityScopedResource]; + return YES; +} + +@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 1105f0f..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 +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 diff --git a/TweakLoader/NSFileManager+GuestHooks.m b/TweakLoader/NSFileManager+GuestHooks.m index e25cf87..8fecacb 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,7 +11,11 @@ static void NSFMGuestHooksInit() { @implementation NSFileManager(LiveContainerHooks) - (nullable NSURL *)hook_containerURLForSecurityApplicationGroupIdentifier:(NSString *)groupIdentifier { - NSURL *result = [NSURL fileURLWithPath:[NSString stringWithFormat:@"%s/Documents/Data/AppGroup/%@", getenv("LC_HOME_PATH"), groupIdentifier]]; + if([groupIdentifier isEqualToString:[NSClassFromString(@"LCSharedUtils") appGroupID]]) { + return [NSURL fileURLWithPath: NSUserDefaults.lcAppGroupPath]; + } + + 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/UIKit+GuestHooks.m b/TweakLoader/UIKit+GuestHooks.m index 56954e5..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) { @@ -23,6 +24,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; }]; @@ -35,8 +46,42 @@ void LCShowSwitchAppConfirmation(NSURL *url) { objc_setAssociatedObject(alert, @"window", window, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } -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."]; +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]; + 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* 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]; UIAlertAction* okAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction * action) { @@ -44,6 +89,21 @@ 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; + }]; + 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:originalUrl]; + [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; }]; @@ -58,14 +118,91 @@ void LCOpenWebPage(NSString* webPageUrlString) { } +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 + 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; + } + + 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); + } + } +} + // Handler for AppDelegate @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; @@ -73,9 +210,9 @@ - (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:@"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; @@ -83,15 +220,18 @@ - (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]; - return; - } else if ([url hasPrefix:@"livecontainer://livecontainer-launch?"]) { - if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { - LCShowSwitchAppConfirmation([NSURL URLWithString:url]); + // 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:[NSString stringWithFormat: @"%@://livecontainer-launch?bundle-name=", NSUserDefaults.lcAppUrlScheme]]) { + handleLiveContainerLaunch([NSURL URLWithString:url]); // Not what we're looking for, pass it } @@ -118,40 +258,38 @@ - (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 - - } 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) { - // 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); - } - - } else if ([url hasPrefix:@"livecontainer://open-url?"]) { + if(!realUrlEncoded) return; + // 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, 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; - if(realUrlEncoded) { - // Convert the base64 encoded url into String - NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:realUrlEncoded options:0]; - NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; - + if(!realUrlEncoded) return; + // Convert the base64 encoded url into String + NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:realUrlEncoded options:0]; + NSString *decodedUrl = [[NSString alloc] initWithData:decodedData encoding:NSUTF8StringEncoding]; + + // 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 - if (![url hasSuffix:NSBundle.mainBundle.bundlePath.lastPathComponent]) { - LCShowSwitchAppConfirmation(urlAction.url); } + + } else if ([url hasPrefix:[NSString stringWithFormat: @"%@://livecontainer-launch?bundle-name=", NSUserDefaults.lcAppUrlScheme]]){ + handleLiveContainerLaunch(urlAction.url); } diff --git a/TweakLoader/utils.h b/TweakLoader/utils.h index 36e7ca5..a193b99 100644 --- a/TweakLoader/utils.h +++ b/TweakLoader/utils.h @@ -5,5 +5,8 @@ void swizzle(Class class, SEL originalAction, SEL swizzledAction); // Exported from the main executable @interface NSUserDefaults(LiveContainer) ++ (instancetype)lcSharedDefaults; + (instancetype)lcUserDefaults; ++ (NSString *)lcAppUrlScheme; ++ (NSString *)lcAppGroupPath; @end 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) diff --git a/entitlements.xml b/entitlements.xml index 82f567f..61b5e0f 100644 --- a/entitlements.xml +++ b/entitlements.xml @@ -15,5 +15,10 @@ get-task-allow + + keychain-access-groups + + $(AppIdentifierPrefix)com.kdt.livecontainer + diff --git a/entitlements_setup.xml b/entitlements_setup.xml index 6f6e198..d55fdf4 100644 --- a/entitlements_setup.xml +++ b/entitlements_setup.xml @@ -13,6 +13,7 @@ keychain-access-groups group.* + $(AppIdentifierPrefix)com.kdt.livecontainer KeychainAccessGroupWillBeWrittenByLiveContainerAAAAAAAAAAAAAAAAAAAA diff --git a/main.m b/main.m index 3705399..40fb96f 100644 --- a/main.m +++ b/main.m @@ -18,11 +18,23 @@ static int (*appMain)(int, char**); static const char *dyldImageName; NSUserDefaults *lcUserDefaults; +NSUserDefaults *lcSharedDefaults; +NSString *lcAppGroupPath; +NSString* lcAppUrlScheme; @implementation NSUserDefaults(LiveContainer) + (instancetype)lcUserDefaults { return lcUserDefaults; } ++ (instancetype)lcSharedDefaults { + return lcSharedDefaults; +} ++ (NSString *)lcAppGroupPath { + return lcAppGroupPath; +} ++ (NSString *)lcAppUrlScheme { + return lcAppUrlScheme; +} @end static BOOL checkJITEnabled() { @@ -186,12 +198,38 @@ 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; + // 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, selectedApp]; + appBundle = [[NSBundle alloc] initWithPath:bundlePath]; + isSharedBundle = true; + } + + if(!appBundle) { + return @"App not found"; + } + if(isSharedBundle) { + [LCSharedUtils setAppRunningByThisLC:selectedApp]; + } + 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 @@ -225,25 +263,65 @@ 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"]; + 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 = 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]; + } + + 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); @@ -255,7 +333,27 @@ static void overwriteExecPath(NSString *bundlePath) { NSString *dirPath = [newHomePath stringByAppendingPathComponent:dir]; [fm createDirectoryAtPath:dirPath withIntermediateDirectories:YES attributes:nil error:nil]; } + [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); @@ -308,20 +406,81 @@ 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]; + lcAppGroupPath = [[NSFileManager.defaultManager containerURLForSecurityApplicationGroupIdentifier:[NSClassFromString(@"LCSharedUtils") appGroupID]] path]; + // 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"]; + NSString* runningLC = [LCSharedUtils getAppRunningLCSchemeWithBundleId: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]; 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]; @@ -332,18 +491,24 @@ 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"]) { + 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); } - return UIApplicationMain(argc, argv, nil, @"LCAppDelegate"); + return UIApplicationMain(argc, argv, nil, @"LCAppDelegateSwiftUI"); } }