diff --git a/AppsManager_iOS27.ipa b/AppsManager_iOS27.ipa index f0f81b8..fba8909 100644 Binary files a/AppsManager_iOS27.ipa and b/AppsManager_iOS27.ipa differ diff --git a/README.md b/README.md index f39a34a..e047b76 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ - 源码 / Source:[source/](source/)(iOS 27 Hook、内置文件浏览器集成与构建脚本 / iOS 27 hook, file-browser integration and build scripts) ```text -eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iOS27.ipa +3508be39187cc387c13490a1c2130087172057f931a297bad37df685d754a098 AppsManager_iOS27.ipa ``` ## 功能 / Features @@ -56,14 +56,25 @@ eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iO **中文:** 1. 使用 AltStore、SideStore 或其他签名工具安装 `AppsManager_iOS27.ipa`。 -2. 当前 IPA 使用自用证书签名;其他设备安装时请使用自己的证书和描述文件重签。 -3. 首次启动会执行数据准备,等待进度条完成后再进入应用列表。 +2. 当前 IPA 使用自用证书签名。其他设备请使用自己的证书和描述文件重签;描述文件必须包含 **App Group**,且签名工具需要递归签名 `Frameworks`。 +3. 首次启动会执行数据准备,等待进度完成后再进入应用列表。 **English:** 1. Install `AppsManager_iOS27.ipa` with AltStore, SideStore, or another signing tool. -2. This IPA is signed with a personal certificate. Re-sign it with your own certificate and provisioning profile for other devices. -3. The first launch prepares app data; wait for the progress bar to finish before using the app list. +2. This IPA is signed with a personal certificate. Re-sign it with your own certificate and provisioning profile; the profile must include an **App Group**, and the tool must sign `Frameworks` recursively. +3. The first launch prepares app data; wait for the progress to finish before using the app list. + +## 更新说明 / Updates + +- 修复首次启动空白、一次性「开发者提示」遮挡列表的问题。 + Fixed the blank first launch and the one-time developer alert covering the list. +- iOS 27 Hook 已移入 `Frameworks/AppsManagerHook.framework`,标准签名工具重签时会把它作为嵌套代码一并签名。 + The iOS 27 hook now lives in `Frameworks/AppsManagerHook.framework`, so standard signing tools re-sign it as nested code. +- 备份、还原、抹掉后会重新计算对应 App 的数据大小。 + The affected app's data size is recalculated after backup, restore, and wipe. +- 重签时必须使用包含 App Group 的证书和描述文件。 + Re-signing requires a certificate and provisioning profile with an App Group entitlement. ## 说明与限制 / Notes & Limitations @@ -83,5 +94,5 @@ eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iO ## 校验 / Checksum ```text -eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iOS27.ipa +3508be39187cc387c13490a1c2130087172057f931a297bad37df685d754a098 AppsManager_iOS27.ipa ``` diff --git a/SHA256SUMS.txt b/SHA256SUMS.txt index c36025f..ba40678 100644 --- a/SHA256SUMS.txt +++ b/SHA256SUMS.txt @@ -1 +1 @@ -eebc7b69884aaf302dbd9a0750f9b47f96800867fe5a316b83be9957facbd12c AppsManager_iOS27.ipa +3508be39187cc387c13490a1c2130087172057f931a297bad37df685d754a098 AppsManager_iOS27.ipa diff --git a/source/README.md b/source/README.md index 9486a37..995433e 100644 --- a/source/README.md +++ b/source/README.md @@ -90,6 +90,25 @@ group...01 The exact value must match your own profile. Do not reuse someone else's certificate, profile, or App Group. +The hook is stored as a nested framework at: + +```text +Payload/AppsManager.app/Frameworks/AppsManagerHook.framework +``` + +Standard signing tools must sign `Frameworks` recursively. If a tool signs only the main executable, the app will fail to launch after re-signing. The published build uses the framework layout specifically so tools such as AltStore, SideStore, Sideloadly, and similar signers handle the hook as nested code. + +### 重签名 / Re-signing + +- 必须保留 App Group 权限;如果新的描述文件没有 App Group,App 会提示 `No usable App Group`,无法访问其他 App 容器。 + The new provisioning profile must include an App Group. Without it the app reports `No usable App Group` and cannot access other app containers. +- 修改 Bundle ID 时,必须同时使用与该 Bundle ID 匹配的描述文件和 entitlements。 + When changing the Bundle ID, use a matching provisioning profile and entitlements. +- 签名工具必须递归签名 `Frameworks` 目录;不要只签名主程序。 + The signing tool must recursively sign the `Frameworks` directory, not only the main executable. +- 免费 Apple ID 通常不支持 App Group,因此无法让 worse_query 正常工作。 + Free Apple ID profiles generally do not support App Groups, so worse_query will not work with them. + ## 第三方代码 / Third-party code This source includes and links code from: diff --git a/source/build_app.sh b/source/build_app.sh index 9bb5909..acca28f 100755 --- a/source/build_app.sh +++ b/source/build_app.sh @@ -25,6 +25,7 @@ OUT_DIR=${OUT_DIR:-"$ROOT/out"} BUNDLE_ID=${BUNDLE_ID:-com.jbjf.appsmanager} DYLIB="$ROOT/hook/AppsManagerHook.dylib" APP="$OUT_DIR/AppsManager.app" +FRAMEWORK="$APP/Frameworks/AppsManagerHook.framework" if [ ! -f "$DYLIB" ]; then echo "Missing $DYLIB; run build_dylib.sh first." >&2 @@ -42,17 +43,55 @@ fi rm -rf "$APP" mkdir -p "$OUT_DIR" cp -R "$APP_SOURCE" "$APP" -cp "$DYLIB" "$APP/AppsManagerHook.dylib" +mkdir -p "$FRAMEWORK" +cp "$DYLIB" "$FRAMEWORK/AppsManagerHook" cp "$PROVISION" "$APP/embedded.mobileprovision" plutil -replace CFBundleIdentifier -string "$BUNDLE_ID" "$APP/Info.plist" plutil -replace CFBundleDisplayName -string "Apps Manager" "$APP/Info.plist" plutil -replace CFBundleName -string "AppsManager" "$APP/Info.plist" +cat > "$FRAMEWORK/Info.plist" <<'EOF' + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + AppsManagerHook + CFBundleIdentifier + com.jbjf.appsmanager.hook + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + AppsManagerHook + CFBundlePackageType + FMWK + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + MinimumOSVersion + 15.0 + CFBundleSupportedPlatforms + + iPhoneOS + + + +EOF + +# Keep the hook in Frameworks so standard signing tools treat it as nested +# code and re-sign it together with the app. +install_name_tool -id "@rpath/AppsManagerHook.framework/AppsManagerHook" "$FRAMEWORK/AppsManagerHook" cp "$APP/ADManager" "$APP/ADManager.orig" "$INSERT_DYLIB" --inplace --overwrite --strip-codesig --all-yes \ - "@executable_path/AppsManagerHook.dylib" "$APP/ADManager" + "@rpath/AppsManagerHook.framework/AppsManagerHook" "$APP/ADManager" rm -f "$APP/ADManager.orig" +if ! otool -l "$APP/ADManager" | grep -q "@executable_path/Frameworks"; then + install_name_tool -add_rpath "@executable_path/Frameworks" "$APP/ADManager" +fi set -- -f -s "$SIGN_IDENTITY" if [ -n "$KEYCHAIN" ]; then @@ -62,8 +101,9 @@ if [ -n "$ENTITLEMENTS" ]; then set -- "$@" --entitlements "$ENTITLEMENTS" --generate-entitlement-der fi -codesign "$@" "$APP/AppsManagerHook.dylib" +codesign "$@" "$FRAMEWORK" codesign "$@" "$APP" echo "built $APP" otool -L "$APP/ADManager" | head -8 +otool -l "$APP/ADManager" | grep -A2 LC_RPATH | head -6 diff --git a/source/hook/src/WBHook.m b/source/hook/src/WBHook.m index c2dfd1d..3c7b143 100644 --- a/source/hook/src/WBHook.m +++ b/source/hook/src/WBHook.m @@ -36,6 +36,7 @@ static NSString *WBLocalized(NSString *key) @"scanning_containers_short": @"Scanning system containers…", @"ls_found": @"LaunchServices database located", @"init_failed": @"Initialization Failed", @"ok": @"OK", @"no_group": @"No usable App Group; worse_query cannot run", + @"no_group_hint": @"Re-sign with a certificate and provisioning profile that include an App Group entitlement.", @"no_ls": @"LaunchServices database not found", @"no_containers": @"The LaunchServices database returned no containers", }, @@ -47,6 +48,7 @@ static NSString *WBLocalized(NSString *key) @"scanning_containers_short": @"正在扫描系统容器…", @"ls_found": @"已定位 LaunchServices 数据库", @"init_failed": @"初始化失败", @"ok": @"好", @"no_group": @"未找到可用的 App Group,无法执行 worse_query", + @"no_group_hint": @"请使用包含 App Group 权限的证书和描述文件重新签名。", @"no_ls": @"未找到 LaunchServices 数据库", @"no_containers": @"LaunchServices 数据库没有返回容器列表", }, @@ -780,9 +782,17 @@ static void (*gOrigLoad)(id, SEL); static NSString *(*gOrigFileSizeString)(id, SEL); static void WBStartPreparation(void); static BOOL (*gOrigDidFinishLaunching)(id, SEL, UIApplication *, NSDictionary *); +static CFAbsoluteTime gLaunchStarted; static BOOL WBHookDidFinishLaunching(id self, SEL _cmd, UIApplication *application, NSDictionary *launchOptions) { + gLaunchStarted = CFAbsoluteTimeGetCurrent(); + // The original app shows a one-time "Message From Developer" alert that + // competes with the launch progress sheet and makes the first run look + // blank. The hook has its own launch status UI, so skip it. + NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults; + [defaults setBool:YES forKey:@"FirstLaunch"]; + [defaults synchronize]; BOOL result = gOrigDidFinishLaunching ? gOrigDidFinishLaunching(self, _cmd, application, launchOptions) : YES; WBStartPreparation(); return result; @@ -790,13 +800,29 @@ static BOOL WBHookDidFinishLaunching(id self, SEL _cmd, UIApplication *applicati static __weak id gAppsTableViewController; static WBProgressViewController *gProgressController; -static dispatch_semaphore_t gReadySemaphore; +static NSCondition *gReadyCondition; static BOOL gPrepared; static BOOL gPreparing; +static BOOL gWaitingForRoot; static BOOL gReloadScheduled; +static BOOL WBWaitForRegistry(void) +{ + if (WBRegistry.shared.ready) return YES; + if (!gReadyCondition) return NO; + [gReadyCondition lock]; + NSDate *deadline = [NSDate dateWithTimeIntervalSinceNow:120]; + while (!WBRegistry.shared.ready && !gPrepared && deadline.timeIntervalSinceNow > 0) { + [gReadyCondition waitUntilDate:deadline]; + } + BOOL ready = WBRegistry.shared.ready; + [gReadyCondition unlock]; + return ready; +} + static NSArray *WBHookAllInstalled(id self, SEL _cmd) { + if (!WBRegistry.shared.ready) WBWaitForRegistry(); NSArray *proxies = WBRegistry.shared.proxies; if (WBRegistry.shared.ready && proxies.count) return proxies; return gOrigAllInstalled ? gOrigAllInstalled(self, _cmd) : @[]; @@ -804,6 +830,7 @@ static NSArray *WBHookAllInstalled(id self, SEL _cmd) static NSArray *WBHookAllApplications(id self, SEL _cmd) { + if (!WBRegistry.shared.ready) WBWaitForRegistry(); NSArray *proxies = WBRegistry.shared.proxies; if (WBRegistry.shared.ready && proxies.count) return proxies; return gOrigAllApplications ? gOrigAllApplications(self, _cmd) : @[]; @@ -872,9 +899,6 @@ static void WBHookDetailReload(id self, SEL _cmd) static void WBHookLoad(id self, SEL _cmd) { gAppsTableViewController = self; - if (gReadySemaphore && !WBRegistry.shared.ready) { - dispatch_semaphore_wait(gReadySemaphore, dispatch_time(DISPATCH_TIME_NOW, 120 * NSEC_PER_SEC)); - } gOrigLoad(self, _cmd); } @@ -1467,7 +1491,7 @@ static void WBInstallHooks(void) method_setImplementation(didFinish, (IMP)WBHookDidFinishLaunching); } - gReadySemaphore = dispatch_semaphore_create(0); + gReadyCondition = [NSCondition new]; [[NSNotificationCenter defaultCenter] addObserverForName:@"WBAppSizeDidUpdate" object:nil queue:NSOperationQueue.mainQueue usingBlock:^(NSNotification *note) { if (gReloadScheduled) return; gReloadScheduled = YES; @@ -1661,7 +1685,6 @@ static void WBRunDebugRefresh(void) static void WBStartPreparation(void) { if (gPreparing || gPrepared) return; - gPreparing = YES; UIWindow *window = UIApplication.sharedApplication.keyWindow; if (!window) { @@ -1673,25 +1696,58 @@ static void WBStartPreparation(void) } } UIViewController *root = window.rootViewController; - if (root && !gProgressController) { + if (!root) { + // On a cold launch the window/root view may not exist yet. Wait for it + // instead of running the preparation with no visible progress sheet. + if (!gWaitingForRoot && CFAbsoluteTimeGetCurrent() - gLaunchStarted < 10) { + gWaitingForRoot = YES; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 100 * NSEC_PER_MSEC), dispatch_get_main_queue(), ^{ + gWaitingForRoot = NO; + WBStartPreparation(); + }); + return; + } + if (!gWaitingForRoot) { + // Last-resort fallback: start even without a presenter. + gPreparing = YES; + [WBRegistry.shared prepareWithProgress:nil completion:^(BOOL ok, NSString *message) { + gPrepared = YES; + [gReadyCondition lock]; + [gReadyCondition broadcast]; + [gReadyCondition unlock]; + }]; + } + return; + } + gPreparing = YES; + [root loadViewIfNeeded]; + UIViewController *presenter = root; + while (presenter.presentedViewController) presenter = presenter.presentedViewController; + if (!gProgressController) { gProgressController = [WBProgressViewController new]; gProgressController.modalPresentationStyle = UIModalPresentationOverFullScreen; - [root presentViewController:gProgressController animated:YES completion:nil]; + [presenter presentViewController:gProgressController animated:YES completion:nil]; } [WBRegistry.shared prepareWithProgress:^(double fraction, NSString *status) { [gProgressController updateFraction:fraction status:status]; } completion:^(BOOL ok, NSString *message) { gPrepared = YES; - if (gReadySemaphore) dispatch_semaphore_signal(gReadySemaphore); + [gReadyCondition lock]; + [gReadyCondition broadcast]; + [gReadyCondition unlock]; dispatch_async(dispatch_get_main_queue(), ^{ if (gProgressController) { [gProgressController dismissViewControllerAnimated:YES completion:nil]; gProgressController = nil; } if (!ok && message.length) { + NSString *detail = message; + if ([message containsString:@"App Group"]) { + detail = [message stringByAppendingFormat:@"\n\n%@", WBLocalized(@"no_group_hint")]; + } UIAlertController *alert = [UIAlertController alertControllerWithTitle:WBLocalized(@"init_failed") - message:message + message:detail preferredStyle:UIAlertControllerStyleAlert]; [alert addAction:[UIAlertAction actionWithTitle:WBLocalized(@"ok") style:UIAlertActionStyleDefault handler:nil]]; UIWindow *keyWindow = UIApplication.sharedApplication.keyWindow; @@ -1711,6 +1767,7 @@ __attribute__((constructor)) static void WBHookInit(void) { @autoreleasepool { + gLaunchStarted = CFAbsoluteTimeGetCurrent(); WBInstallHooks(); dispatch_async(dispatch_get_main_queue(), ^{ [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification