mirror of
https://github.com/Lakr233/vphone-cli.git
synced 2026-10-01 23:54:35 +08:00
Let the VM window skip Setup Assistant on an activated guest
vphoned reports `setup_pending` in /v1/health and serves `setup.status` and `setup.skip`. Device › Skip Setup Assistant… is enabled while the guest reports it pending, and asks before it runs. SpringBoard decides whether to run Setup once, when it starts. It does while `SetupDone` in com.apple.purplebuddy is not true, and runs the flow shown after a software update while `SetupVersion` is below SetupAssistant.framework's BYBuddyIOSCurrentVersion (an int32, 11 on 26.4 and 27.0). Writing the keys while Setup is on screen does not dismiss it, and killing Setup only makes SpringBoard start it again. So the skip writes `SetupDone`, `SetupFinishedAllSteps` and `SetupVersion` through cfprefsd for user mobile, then restarts SpringBoard. With every other key in the domain removed, those three still reach the Home Screen, and no later panes appear. The experiments are in Research/Guest/setup_assistant_skip.md, including how to send a guest back to Setup for testing. MCInstall's SetCloudConfiguration, which pymobiledevice3, go-ios and cfgutil use, works only on an erased device, so it does not fit here. Verified on test-26.4 with this bundle: after deleting `SetupDone`, Setup's hello screen appeared and setup.status reported pending true and running true. setup.skip without force was refused. With force it returned pending false, SpringBoard restarted and unlocked to the Home Screen. Deleting `SetupVersion` then skipping also reached the Home Screen. Afterwards the domain matched its state before the test. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# Skipping Setup Assistant on an activated guest
|
||||
|
||||
Device › Skip Setup Assistant… and vphoned's `setup.status` / `setup.skip`
|
||||
(`VPhoneDaemon/Daemon/GuestAPI+SetupAssistant.swift`) rest on the experiments
|
||||
below, run on 2026-09-30 against `test-26.4` (iOS 26.4, already activated)
|
||||
over `vphone.sock` with `settings.get/set/delete`, `system.respring`,
|
||||
`processes.list` and screenshots.
|
||||
|
||||
## Where the state lives
|
||||
|
||||
Setup.app keeps its state in the `com.apple.purplebuddy` domain for user
|
||||
mobile (`/var/mobile/Library/Preferences/com.apple.purplebuddy.plist`). The
|
||||
key names are SetupAssistant.framework exports that Setup.app imports:
|
||||
|
||||
| Export | Key |
|
||||
| --- | --- |
|
||||
| `BYBuddyDoneKey` | `SetupDone` |
|
||||
| `BYBuddyFinishedInitialRunKey` | `SetupFinishedAllSteps` |
|
||||
| `BYBuddyIOSVersionKey` | `SetupVersion` |
|
||||
| `BYBuddyLastExitKey` | `SetupLastExit` |
|
||||
| `BYBuddyIOSCurrentVersion` | the current `SetupVersion`, an `int32` (Setup.app loads it with `ldr w2`); 11 on 26.4 and 27.0 |
|
||||
|
||||
A finished guest also carries `*Presented` pane flags, `*MiniBuddy*Ran`
|
||||
flags, `Language`, `chronicle` and Setup's own `lastPrepareLaunchSentinel`.
|
||||
No `/var/Managed Preferences/mobile/com.apple.purplebuddy.plist` override
|
||||
exists on these guests.
|
||||
|
||||
## What each key does
|
||||
|
||||
Each row changes one thing from a finished guest, then restarts SpringBoard
|
||||
(`system.respring`) and unlocks.
|
||||
|
||||
| Change | Result |
|
||||
| --- | --- |
|
||||
| delete `SetupDone` | full Setup (“hello”) before the Lock Screen |
|
||||
| delete `SetupFinishedAllSteps` | Home Screen; Setup does not run |
|
||||
| delete `SetupVersion` | the flow after a software update, after unlocking (“外观” pane, or straight to “软件更新已完成” when every `*Presented` flag is set) |
|
||||
| `SetupVersion = 11` again | Home Screen |
|
||||
| delete every key in the domain | full Setup; Setup writes only `lastPrepareLaunchSentinel` on launch |
|
||||
| from that empty domain, set only `SetupDone`, `SetupFinishedAllSteps`, `SetupVersion = 11` | Home Screen after the restart, again after a second restart and a 25 s wait; no later panes |
|
||||
|
||||
## When SpringBoard decides
|
||||
|
||||
SpringBoard decides whether to run Setup once, when it starts.
|
||||
|
||||
- Setting `SetupDone = true` while Setup is on screen does not dismiss it.
|
||||
Setup switches to its “software update finished” pane, and the idle timer
|
||||
now applies, so the screen locks.
|
||||
- Killing Setup then only makes SpringBoard launch it again (“hello”, swipe up
|
||||
to open).
|
||||
- Restarting SpringBoard with the keys in place goes to the Lock Screen and
|
||||
then the Home Screen.
|
||||
|
||||
So a skip is: write the three keys through cfprefsd, then restart
|
||||
SpringBoard. The write has to go through cfprefsd (`CFPreferencesSetValue`
|
||||
for user mobile, then synchronize). Editing the plist file directly leaves
|
||||
cfprefsd holding the old values.
|
||||
|
||||
## Detection
|
||||
|
||||
`setup_pending` in `/v1/health` is `SetupDone != true || SetupVersion <
|
||||
BYBuddyIOSCurrentVersion`: two cfprefsd reads per probe. `setup.status` also
|
||||
reports whether `/Applications/Setup.app/Setup` is running.
|
||||
`apps.foreground` is not a usable signal. On `test-27.0` it reported
|
||||
SpringBoard with `source: unavailable` while an app was in front.
|
||||
|
||||
## Not covered
|
||||
|
||||
- Mini flows triggered by other keys (`*MiniBuddy*Ran`, missing `*Presented`
|
||||
flags on a later build) are not detected or skipped. None appeared with
|
||||
only the three keys set.
|
||||
- The skip does not check activation. On an unactivated guest SpringBoard
|
||||
still needs activation.
|
||||
- MCInstall `SetCloudConfiguration` with `SkipSetup` keys, which
|
||||
pymobiledevice3 `profile supervise`, go-ios `prepare` and `cfgutil prepare`
|
||||
use, applies only to an erased device. The device refuses a second
|
||||
configuration (error 14002), so it does not fit a guest that is already
|
||||
set up or activated.
|
||||
|
||||
## Returning a guest to Setup Assistant
|
||||
|
||||
To see Setup again for testing, delete `SetupDone` with `settings.delete` and
|
||||
restart SpringBoard. Delete `SetupVersion` instead to see the flow after a
|
||||
software update. Save the domain first with `settings.get`, because deleting
|
||||
every key loses the pane flags and `Language`.
|
||||
@@ -287,6 +287,7 @@ request carries `"force": true`.
|
||||
| Bootstrap | `bootstrap.install {layout}`, `bootstrap.status`, `bootstrap.inspect`, `bootstrap.uninstall {jbroot, force}`, `bootstrap.firmware` (see above) |
|
||||
| Environment | `environment.status` (SHA-256 of each vphone library in `/usr/lib`, or null when absent, plus the staging directory), `environment.install {libraries: [{name, sha256}]}` (see below) |
|
||||
| Profile UDID | `udid.get`, `udid.set {udid}`, `udid.clear` — each returns `{udid, path}`, the UDID libmisfix gives misagent's profile check (null: the guest's own) and the settings file it came from; `set` and `clear` also return `restarted_pids` (see below) |
|
||||
| Setup Assistant | `setup.status` (`{pending, running, pid, setup_done, setup_version, current_version}`), `setup.skip` **force** (sets `SetupDone`, `SetupFinishedAllSteps` and `SetupVersion` in `com.apple.purplebuddy`, restarts SpringBoard, returns the status plus `respring`); `/v1/health` carries `setup_pending` — see `Research/Guest/setup_assistant_skip.md` |
|
||||
|
||||
`processes.list` joins icli's kernel process list with `proc_pid_rusage`
|
||||
footprint, resident size and CPU time (`VPhoneDaemon/Native/vphoned_process.m`),
|
||||
@@ -295,7 +296,7 @@ Account passwords, boot logo rendering and package installation, removal and
|
||||
repository changes are deliberately not exposed. `/v1/health` lists the new
|
||||
areas in `capabilities` (`device_info`, `display`, `audio`, `input_gestures`,
|
||||
`ui_inspection`, `processes`, `services`, `logs`, `network_capture`,
|
||||
`app_details`, `system_control`, `file_tools`, `packages`, `environment_update`, `udid_override`) so a host can hide
|
||||
`app_details`, `system_control`, `file_tools`, `packages`, `environment_update`, `udid_override`, `setup_skip`) so a host can hide
|
||||
panels an older agent cannot serve. icli failures reach the caller with
|
||||
icli's own error `code` (`failed`, `unavailable`, `device_locked`, …) and
|
||||
message.
|
||||
|
||||
@@ -15,6 +15,7 @@ extension GuestAPI {
|
||||
executeFileTool,
|
||||
executeEnvironment,
|
||||
executeDeviceIdentity,
|
||||
executeSetupAssistant,
|
||||
]
|
||||
for area in areas {
|
||||
if let result = try area(method, params) {
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import Darwin
|
||||
import Foundation
|
||||
import IcliKit
|
||||
import IcliSystem
|
||||
|
||||
// MARK: - Setup Assistant
|
||||
|
||||
/// Setup Assistant (Setup.app) keeps its state in `com.apple.purplebuddy`
|
||||
/// for user mobile. SpringBoard reads it once, when it starts: it runs the
|
||||
/// full flow while `SetupDone` is not true, and the flow shown after a
|
||||
/// software update while `SetupVersion` is below SetupAssistant.framework's
|
||||
/// `BYBuddyIOSCurrentVersion`. Writing the keys while Setup is on screen does
|
||||
/// not dismiss it, and killing Setup only makes SpringBoard start it again,
|
||||
/// so a skip writes the keys and then restarts SpringBoard.
|
||||
/// `Research/Guest/setup_assistant_skip.md` has the experiments behind this.
|
||||
extension GuestAPI {
|
||||
static func executeSetupAssistant(_ method: String, _ params: [String: Any]) throws -> [String: Any]? {
|
||||
switch method {
|
||||
case "setup.status":
|
||||
return try setupAssistantStatus()
|
||||
case "setup.skip":
|
||||
try requireForce(params, "skip Setup Assistant")
|
||||
return try skipSetupAssistant()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private static let buddyDomain = "com.apple.purplebuddy"
|
||||
|
||||
/// `BYBuddyIOSCurrentVersion`, a 32-bit integer: 11 on iOS 26.4 and 27.0.
|
||||
/// Nil when the framework does not export it.
|
||||
static let setupAssistantCurrentVersion: Int? = {
|
||||
let path = "/System/Library/PrivateFrameworks/SetupAssistant.framework/SetupAssistant"
|
||||
guard let handle = dlopen(path, RTLD_LAZY),
|
||||
let symbol = dlsym(handle, "BYBuddyIOSCurrentVersion")
|
||||
else { return nil }
|
||||
return Int(symbol.assumingMemoryBound(to: Int32.self).pointee)
|
||||
}()
|
||||
|
||||
private static func buddyValue(_ key: String) -> Any? {
|
||||
CFPreferencesCopyValue(key as CFString, buddyDomain as CFString, "mobile" as CFString, kCFPreferencesAnyHost)
|
||||
}
|
||||
|
||||
private static var buddySetupVersion: Int? {
|
||||
(buddyValue("SetupVersion") as? NSNumber)?.intValue
|
||||
}
|
||||
|
||||
/// True when SpringBoard will run Setup the next time it starts. Two
|
||||
/// preference reads, cheap enough for `/v1/health`.
|
||||
static func setupAssistantPending() -> Bool {
|
||||
guard buddyValue("SetupDone") as? Bool == true else { return true }
|
||||
guard let current = setupAssistantCurrentVersion else { return false }
|
||||
return (buddySetupVersion ?? 0) < current
|
||||
}
|
||||
|
||||
private static func setupAssistantStatus() throws -> [String: Any] {
|
||||
let processes = try listProcesses(filter: "Setup")["processes"] as? [[String: Any]] ?? []
|
||||
let pid = processes.first { $0["executable"] as? String == "/Applications/Setup.app/Setup" }?["pid"]
|
||||
return [
|
||||
"pending": setupAssistantPending(),
|
||||
"running": pid != nil,
|
||||
"pid": pid ?? 0,
|
||||
"setup_done": buddyValue("SetupDone") as? Bool ?? false,
|
||||
"setup_version": buddySetupVersion.map { $0 as Any } ?? NSNull(),
|
||||
"current_version": setupAssistantCurrentVersion.map { $0 as Any } ?? NSNull(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Marks setup finished for this iOS version and restarts SpringBoard.
|
||||
/// These three keys are all Setup needs: with every other key absent,
|
||||
/// SpringBoard starts to the Home Screen and no later panes appear.
|
||||
private static func skipSetupAssistant() throws -> [String: Any] {
|
||||
guard let current = setupAssistantCurrentVersion else {
|
||||
throw GuestAPIError.operationFailed("SetupAssistant.framework does not export BYBuddyIOSCurrentVersion")
|
||||
}
|
||||
let version = max(buddySetupVersion ?? 0, current)
|
||||
_ = try writePreference(domain: buddyDomain, key: "SetupDone", value: .bool(true))
|
||||
_ = try writePreference(domain: buddyDomain, key: "SetupFinishedAllSteps", value: .bool(true))
|
||||
_ = try writePreference(domain: buddyDomain, key: "SetupVersion", value: .int(Int64(version)))
|
||||
let springBoard = try respring()
|
||||
var status = try setupAssistantStatus()
|
||||
status["respring"] = springBoard
|
||||
return status
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,7 @@ enum GuestAPI {
|
||||
"binary_hash": binaryHash,
|
||||
"ios": "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)",
|
||||
"ip": ip ?? "",
|
||||
"setup_pending": setupAssistantPending(),
|
||||
"capabilities": [
|
||||
"touch",
|
||||
"touch2",
|
||||
@@ -94,6 +95,7 @@ enum GuestAPI {
|
||||
"packages",
|
||||
"environment_update",
|
||||
"udid_override",
|
||||
"setup_skip",
|
||||
],
|
||||
]
|
||||
}
|
||||
|
||||
@@ -21558,6 +21558,108 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Skip Setup" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Skip Setup"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "設定をスキップ"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "설정 건너뛰기"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bỏ qua cài đặt"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "跳过设置"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Skip Setup Assistant?" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Skip Setup Assistant?"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "設定アシスタントをスキップしますか?"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "설정 지원을 건너뛰시겠습니까?"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bỏ qua Trợ lý cài đặt?"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "要跳过设置助理吗?"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Skip Setup Assistant…" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Skip Setup Assistant…"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "設定アシスタントをスキップ…"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "설정 지원 건너뛰기…"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Bỏ qua Trợ lý cài đặt…"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "跳过设置助理…"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Source: Off" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -23600,6 +23702,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"The guest marks setup as finished and restarts SpringBoard. Use this only on a guest that is already activated. Language, passcode, Apple Account and privacy settings keep their defaults." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "The guest marks setup as finished and restarts SpringBoard. Use this only on a guest that is already activated. Language, passcode, Apple Account and privacy settings keep their defaults."
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "ゲストは設定を完了済みにして SpringBoard を再起動します。アクティベート済みのゲストでのみ使用してください。言語、パスコード、Apple アカウント、プライバシーの設定はデフォルトのままです。"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "게스트가 설정을 완료된 것으로 표시하고 SpringBoard를 재시동합니다. 이미 활성화된 게스트에서만 사용하십시오. 언어, 암호, Apple 계정 및 개인정보 보호 설정은 기본값으로 유지됩니다."
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Máy khách sẽ đánh dấu đã hoàn tất cài đặt và khởi động lại SpringBoard. Chỉ dùng trên máy khách đã được kích hoạt. Ngôn ngữ, mật mã, Tài khoản Apple và cài đặt quyền riêng tư giữ giá trị mặc định."
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "客体会将设置标记为已完成并重启 SpringBoard。仅在已激活的客体上使用。语言、密码、Apple 账户和隐私设置保持默认。"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"The guest refused the restart request." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -24880,6 +25016,40 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"Unable to Skip Setup Assistant" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Unable to Skip Setup Assistant"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "設定アシスタントをスキップできません"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "설정 지원을 건너뛸 수 없음"
|
||||
}
|
||||
},
|
||||
"vi" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Không thể bỏ qua Trợ lý cài đặt"
|
||||
}
|
||||
},
|
||||
"zh-Hans" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "无法跳过设置助理"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Unable to add the keychain item. Try again." : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
|
||||
@@ -73,6 +73,22 @@ final class VPhoneGuestControl {
|
||||
orientationObservers.append(observer)
|
||||
}
|
||||
|
||||
/// True while the guest will show Setup Assistant, from `/v1/health`.
|
||||
/// False when disconnected.
|
||||
private(set) var isSetupAssistantPending = false {
|
||||
didSet {
|
||||
for observer in setupAssistantObservers {
|
||||
observer(isSetupAssistantPending)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ObservationIgnored private var setupAssistantObservers: [(Bool) -> Void] = []
|
||||
|
||||
func observeSetupAssistantPending(_ observer: @escaping (Bool) -> Void) {
|
||||
setupAssistantObservers.append(observer)
|
||||
}
|
||||
|
||||
/// Turns the guest to the first of `candidates` it accepts. Each is set
|
||||
/// before the guest is asked, so the window turns with the guest instead
|
||||
/// of after it, and a refused one moves straight on to the next. When
|
||||
@@ -162,6 +178,10 @@ final class VPhoneGuestControl {
|
||||
if ios != guestIOSVersion {
|
||||
guestIOSVersion = ios
|
||||
}
|
||||
let setupPending = info["setup_pending"] as? Bool ?? false
|
||||
if setupPending != isSetupAssistantPending {
|
||||
isSetupAssistantPending = setupPending
|
||||
}
|
||||
if !isConnected {
|
||||
isConnected = true
|
||||
print("[control] connected to vphoned HTTP API (iOS \(guestIOSVersion ?? "?"))")
|
||||
@@ -187,6 +207,7 @@ final class VPhoneGuestControl {
|
||||
guestIPAddress = nil
|
||||
guestIOSVersion = nil
|
||||
interfaceOrientation = nil
|
||||
isSetupAssistantPending = false
|
||||
if wasConnected {
|
||||
onDisconnect?()
|
||||
}
|
||||
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
import Foundation
|
||||
|
||||
// MARK: - Setup Assistant
|
||||
|
||||
/// Skipping Setup Assistant on a guest that is already activated. vphoned
|
||||
/// marks setup finished for the guest's iOS version and restarts
|
||||
/// SpringBoard, which then starts at the Lock Screen instead of Setup.
|
||||
/// `isSetupAssistantPending` follows on the next health probe.
|
||||
extension VPhoneGuestControl {
|
||||
func skipSetupAssistant() async throws {
|
||||
guard guestCapabilities.contains("setup_skip") else {
|
||||
throw ControlError.unsupportedCapability("setup_skip")
|
||||
}
|
||||
_ = try await call("setup.skip", params: ["force": true])
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ class VPhoneMenuController {
|
||||
var restartGuestItem: NSMenuItem?
|
||||
var setUDIDItem: NSMenuItem?
|
||||
var resetUDIDItem: NSMenuItem?
|
||||
var skipSetupAssistantItem: NSMenuItem?
|
||||
var panelMenuItems: [VPhoneGuestPanel: NSMenuItem] = [:]
|
||||
var rotateMenuItems: [NSMenuItem] = []
|
||||
var touchIDMonitor: VPhoneTouchIDMonitor? {
|
||||
|
||||
@@ -85,6 +85,7 @@ extension VPhoneMenuController {
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
addUDIDItems(to: menu)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
addSetupAssistantItem(to: menu)
|
||||
let restart = makeItem("Restart Guest…", action: #selector(restartGuest), symbol: "arrow.clockwise")
|
||||
restart.isEnabled = false
|
||||
restartGuestItem = restart
|
||||
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import AppKit
|
||||
|
||||
// MARK: - Setup Assistant Menu Item
|
||||
|
||||
/// Skips Setup Assistant on a guest that is already activated. Enabled only
|
||||
/// while vphoned reports that the guest will show it. See
|
||||
/// `VPhoneGuestControlSetupAssistant.swift`.
|
||||
extension VPhoneMenuController {
|
||||
func addSetupAssistantItem(to menu: NSMenu) {
|
||||
let item = makeItem("Skip Setup Assistant…", action: #selector(skipSetupAssistant), symbol: "forward.end")
|
||||
item.isEnabled = false
|
||||
skipSetupAssistantItem = item
|
||||
menu.addItem(item)
|
||||
control.observeSetupAssistantPending { [weak item] pending in
|
||||
item?.isEnabled = pending
|
||||
}
|
||||
}
|
||||
|
||||
@objc func skipSetupAssistant() {
|
||||
VPhoneAlert.present(
|
||||
title: "Skip Setup Assistant?",
|
||||
message: "The guest marks setup as finished and restarts SpringBoard. Use this only on a guest that is already activated. Language, passcode, Apple Account and privacy settings keep their defaults.",
|
||||
style: .warning,
|
||||
buttons: ["Skip Setup", "Cancel"],
|
||||
) { response in
|
||||
guard response == .alertFirstButtonReturn else { return }
|
||||
self.skipSetupAssistantItem?.isEnabled = false
|
||||
Task {
|
||||
do {
|
||||
try await self.control.skipSetupAssistant()
|
||||
} catch {
|
||||
print("[setup] skip failed: \(error)")
|
||||
self.skipSetupAssistantItem?.isEnabled = self.control.isSetupAssistantPending
|
||||
VPhoneAlert.present(
|
||||
title: "Unable to Skip Setup Assistant",
|
||||
message: "Check that the guest agent is connected, then try again.",
|
||||
style: .warning,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user