feat: admit numbered iPhone companion
This commit is contained in:
parent
fb8607f248
commit
fe1aaade0e
20 changed files with 1563 additions and 7 deletions
|
|
@ -0,0 +1,49 @@
|
||||||
|
{
|
||||||
|
"schema": "hololake.ios-mobile-client.contract/v1",
|
||||||
|
"record_id": "HLP-IOS-CLIENT-001",
|
||||||
|
"state": "NUMBERED_SOURCE_SIMULATOR_AND_DEVELOPMENT_SIGNED_IPA_ACCEPTED",
|
||||||
|
"role": "REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM",
|
||||||
|
"root_node": "USER_LOCAL_COMPUTER_TERMINAL",
|
||||||
|
"numbered_transport": {
|
||||||
|
"desktop_module": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001",
|
||||||
|
"desktop_route_family": "HLP-NIPC-MOD-0030/HLP-NIPC-OP-0141..0146/HLP-NIPC-TGT-0030",
|
||||||
|
"wire_schema": "hololake.mobile-sync/v1",
|
||||||
|
"client_number": "HLP-IOS-CLIENT-001",
|
||||||
|
"unregistered_route_access": "FAIL_CLOSED"
|
||||||
|
},
|
||||||
|
"security": {
|
||||||
|
"pairing": "CHACHA20_POLY1305_ONE_TIME_SECRET",
|
||||||
|
"session": "CHACHA20_POLY1305_STRICT_COUNTER",
|
||||||
|
"key_storage": "KEYCHAIN_WHEN_UNLOCKED_THIS_DEVICE_ONLY",
|
||||||
|
"local_network_only": true,
|
||||||
|
"background_polling": false,
|
||||||
|
"remote_desktop_clone": false,
|
||||||
|
"desktop_offline_execution": false
|
||||||
|
},
|
||||||
|
"interface": {
|
||||||
|
"framework": "SWIFTUI",
|
||||||
|
"visual_family": "HOLOLAKE_TRADITIONAL_SURFACE_MOBILE_ADAPTATION",
|
||||||
|
"idle_state": "STATIC",
|
||||||
|
"manual_sync_only": true,
|
||||||
|
"pairing_entry": ["CUSTOM_URL_SCHEME", "PASTEBOARD"],
|
||||||
|
"projections": ["CHANNEL_INTEGRITY", "WEB_NOVEL_COUNTS_AND_WORKS", "EDUCATION_COUNTS", "BOUNDED_CAPTURE"]
|
||||||
|
},
|
||||||
|
"acceptance": {
|
||||||
|
"simulator_build": "PASS_SIGNED_LOCAL_SIMULATOR_IPHONE_17_PRO_IOS_26_5",
|
||||||
|
"simulator_visual_inspection": "PASS_PAIRING_AND_NUMBER_BOUNDARY_SURFACE_IDLE_STATIC",
|
||||||
|
"unit_tests": "PASS_4",
|
||||||
|
"development_signed_device_archive": "PASS_APPLE_DEVELOPMENT_TEAM_825A9L3G7Q",
|
||||||
|
"debugging_ipa_export": "PASS",
|
||||||
|
"version": "0.5.0",
|
||||||
|
"build": "1",
|
||||||
|
"bundle_identifier": "com.guanghulab.hololake",
|
||||||
|
"xcode": "26.6_17F113",
|
||||||
|
"archive_application_binary_sha256": "7021c9cde2f7bdf01cda6200669be528a58d27ef41adbd3d58d7135ff7245702",
|
||||||
|
"archive_application_cdhash": "11ae3a8ac0d761e4d4b4de229dbb19291de29e03",
|
||||||
|
"debugging_ipa_sha256": "a6b505e1a7dc10febd831a540396bb9530f78bb2429d258962f0250e5597b1f0",
|
||||||
|
"provisioning_profile_uuid": "fe5fe72c-4826-4e19-9672-d9868cc4491e",
|
||||||
|
"provisioning_profile_expires": "2027-07-19",
|
||||||
|
"desktop_delivery_path": "/Users/bingshuolingdianyuanhe/Desktop/HoloLake-iPhone-0.5.0-Development.ipa",
|
||||||
|
"real_iphone_pair_and_sync": "NOT_CLAIMED"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -65,12 +65,12 @@
|
||||||
},
|
},
|
||||||
"client_scope": {
|
"client_scope": {
|
||||||
"desktop_bridge": "IN_THIS_ADMISSION",
|
"desktop_bridge": "IN_THIS_ADMISSION",
|
||||||
"ios_application_source": "NOT_IN_DECLARED_DONOR_CANDIDATE",
|
"ios_application_source": "SEPARATELY_ADMITTED_AT_MOBILE_IOS_WITH_HLP_IOS_CLIENT_001",
|
||||||
"ios_installable_package": "PENDING_SEPARATE_ADMISSION",
|
"ios_installable_package": "DEVELOPMENT_SIGNED_DEBUGGING_IPA_EXPORTED",
|
||||||
"claim_real_iphone_end_to_end_accepted": false
|
"claim_real_iphone_end_to_end_accepted": false
|
||||||
},
|
},
|
||||||
"current_acceptance": {
|
"current_acceptance": {
|
||||||
"state": "PASS_DESKTOP_BRIDGE_ONLY",
|
"state": "PASS_DESKTOP_BRIDGE_AND_SEPARATELY_ADMITTED_IOS_CLIENT_ARTIFACT",
|
||||||
"module_package_sha256": "61a9d402c936d7477609bcfa6eb4ad6e83cb6c089ba51b3db09a2d6259185924",
|
"module_package_sha256": "61a9d402c936d7477609bcfa6eb4ad6e83cb6c089ba51b3db09a2d6259185924",
|
||||||
"module_package_signature": "PASS_EMBEDDED_PRODUCT_TRUST",
|
"module_package_signature": "PASS_EMBEDDED_PRODUCT_TRUST",
|
||||||
"signed_app_binary_sha256": "9be06d6c5dbd4f46f6f925142d633f12b42ee553873c2342f9804bcf39dde7b4",
|
"signed_app_binary_sha256": "9be06d6c5dbd4f46f6f925142d633f12b42ee553873c2342f9804bcf39dde7b4",
|
||||||
|
|
@ -84,7 +84,10 @@
|
||||||
"desktop_listener": "PASS_EXPLICIT_START_REACHABLE_STATUS_AND_EXPLICIT_STOP",
|
"desktop_listener": "PASS_EXPLICIT_START_REACHABLE_STATUS_AND_EXPLICIT_STOP",
|
||||||
"restart_restore": "PASS_ACTIVE_MODULE_RESTORED_LISTENER_OFFLINE",
|
"restart_restore": "PASS_ACTIVE_MODULE_RESTORED_LISTENER_OFFLINE",
|
||||||
"legacy_account_readback": "PASS_ONE_PAIRED_DEVICE_ONE_ISOLATED_CAPTURE_RETAINED",
|
"legacy_account_readback": "PASS_ONE_PAIRED_DEVICE_ONE_ISOLATED_CAPTURE_RETAINED",
|
||||||
"live_iphone_pairing_this_cycle": "NOT_CLAIMED",
|
"ios_client_contract": "contracts/ios-mobile-client-v1.json",
|
||||||
|
"ios_client_source_build_and_simulator": "PASS",
|
||||||
|
"ios_development_signed_ipa": "PASS_SHA256_a6b505e1a7dc10febd831a540396bb9530f78bb2429d258962f0250e5597b1f0",
|
||||||
|
"live_iphone_pairing_this_cycle": "NOT_CLAIMED_NO_PHYSICAL_DEVICE_USED",
|
||||||
"notarization": "FINAL_RELEASE_CANDIDATE_PENDING"
|
"notarization": "FINAL_RELEASE_CANDIDATE_PENDING"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"schema": "hololake.module-donor-admission-registry/v1",
|
"schema": "hololake.module-donor-admission-registry/v1",
|
||||||
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
|
"record_id": "HLP-MODULE-DONOR-ADMISSION-001",
|
||||||
"state": "SEVEN_CANDIDATES_ADMITTED_DONOR_SOURCES_REMAIN_READ_ONLY",
|
"state": "EIGHT_CANDIDATES_ADMITTED_DONOR_SOURCES_REMAIN_READ_ONLY",
|
||||||
"root_rule": {
|
"root_rule": {
|
||||||
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
||||||
"repair_old_application_in_place": false,
|
"repair_old_application_in_place": false,
|
||||||
|
|
@ -160,7 +160,7 @@
|
||||||
"runtime_module_number": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001",
|
"runtime_module_number": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001",
|
||||||
"numbered_ipc_module": "HLP-NIPC-MOD-0030",
|
"numbered_ipc_module": "HLP-NIPC-MOD-0030",
|
||||||
"acceptance_evidence": "contracts/mobile-sync-v1.json#current_acceptance",
|
"acceptance_evidence": "contracts/mobile-sync-v1.json#current_acceptance",
|
||||||
"boundary": "DESKTOP_ROOT_NODE_SAME_LAN_BRIDGE_ONLY; EXPLICIT_LISTENER; IOS_INSTALLABLE_PACKAGE_PENDING_SEPARATE_ADMISSION; NO_SECOND_PERSONA_SYSTEM"
|
"boundary": "DESKTOP_ROOT_NODE_SAME_LAN_BRIDGE_ONLY; EXPLICIT_LISTENER; IOS CLIENT REQUIRES SEPARATE NUMBERED ADMISSION; NO_SECOND_PERSONA_SYSTEM"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"admission_order": 7,
|
"admission_order": 7,
|
||||||
|
|
@ -180,6 +180,23 @@
|
||||||
"numbered_ipc_module": "HLP-NIPC-MOD-0031",
|
"numbered_ipc_module": "HLP-NIPC-MOD-0031",
|
||||||
"acceptance_evidence": "contracts/dynamic-language-world-visual-system.json#current_acceptance",
|
"acceptance_evidence": "contracts/dynamic-language-world-visual-system.json#current_acceptance",
|
||||||
"boundary": "QODER_LOCKED_VISUAL_SOURCE_PLUS_REALITY_TIME_AND_PUBLIC_WEATHER_PROJECTION; ALL_FUNCTION_ROUTES_REMAIN_NUMBERED; IDLE_STATIC; NO_FAKE_WEATHER_BROADCAST_OR_METRICS"
|
"boundary": "QODER_LOCKED_VISUAL_SOURCE_PLUS_REALITY_TIME_AND_PUBLIC_WEATHER_PROJECTION; ALL_FUNCTION_ROUTES_REMAIN_NUMBERED; IDLE_STATIC; NO_FAKE_WEATHER_BROADCAST_OR_METRICS"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"admission_order": 8,
|
||||||
|
"candidate_number": "HLP-DONOR-CAND-0008",
|
||||||
|
"name": "ios_numbered_remote_body_entry",
|
||||||
|
"state": "ADMITTED_SOURCE_SIMULATOR_AND_DEVELOPMENT_SIGNED_IPA_ACCEPTED",
|
||||||
|
"paths": [
|
||||||
|
"contracts/ios-mobile-client-v1.json",
|
||||||
|
"mobile/ios/project.yml",
|
||||||
|
"mobile/ios/Sources",
|
||||||
|
"mobile/ios/Tests"
|
||||||
|
],
|
||||||
|
"client_number": "HLP-IOS-CLIENT-001",
|
||||||
|
"desktop_runtime_module_number": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001",
|
||||||
|
"numbered_ipc_module": "HLP-NIPC-MOD-0030",
|
||||||
|
"acceptance_evidence": "contracts/ios-mobile-client-v1.json#acceptance",
|
||||||
|
"boundary": "IPHONE IS A THIN REMOTE BODY ENTRY; DESKTOP REMAINS ROOT NODE; SAME LAN EXPLICIT PAIR AND MANUAL SYNC ONLY; NO SECOND PERSONA SYSTEM; REAL IPHONE END TO END NOT CLAIMED"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"admission_gate": [
|
"admission_gate": [
|
||||||
|
|
|
||||||
4
product-source/hololake-native-desktop/mobile/ios/.gitignore
vendored
Normal file
4
product-source/hololake-native-desktop/mobile/ios/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
build/
|
||||||
|
*.xcuserstate
|
||||||
|
xcuserdata/
|
||||||
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>method</key>
|
||||||
|
<string>debugging</string>
|
||||||
|
<key>signingStyle</key>
|
||||||
|
<string>automatic</string>
|
||||||
|
<key>teamID</key>
|
||||||
|
<string>825A9L3G7Q</string>
|
||||||
|
<key>stripSwiftSymbols</key>
|
||||||
|
<true/>
|
||||||
|
<key>uploadSymbols</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>HoloLake</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(MARKETING_VERSION)</string>
|
||||||
|
<key>CFBundleURLTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Editor</string>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>com.guanghulab.hololake.pair</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>hololake</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||||
|
<key>LSApplicationCategoryType</key>
|
||||||
|
<string>public.app-category.productivity</string>
|
||||||
|
<key>NSAppTransportSecurity</key>
|
||||||
|
<dict>
|
||||||
|
<key>NSAllowsLocalNetworking</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
<key>NSLocalNetworkUsageDescription</key>
|
||||||
|
<string>HoloLake 仅在你明确配对后,通过同一局域网连接你的电脑端频道。</string>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchScreen</key>
|
||||||
|
<dict/>
|
||||||
|
<key>UIRequiresFullScreen</key>
|
||||||
|
<true/>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>$(PRODUCT_NAME)</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>BNDL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
@ -0,0 +1,425 @@
|
||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 77;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXBuildFile section */
|
||||||
|
147588FBBE65B668142D5DBE /* HoloLakeMobileTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BBF45AEC03B7B5DE76DE7DA /* HoloLakeMobileTests.swift */; };
|
||||||
|
149652D6C6336752E39CEF9D /* HoloLakeKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75A889326BDEACBDB756B15A /* HoloLakeKeychain.swift */; };
|
||||||
|
4C8E5CF9FBFC541E3724C695 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5DF66BC64D4DCD3BF0583C4 /* ContentView.swift */; };
|
||||||
|
4FD7821997D8E90E6594FC23 /* HoloLakeMobileApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DCE2A5796B15DAC37985182 /* HoloLakeMobileApp.swift */; };
|
||||||
|
9BC7CC6D8BC355D731943912 /* HoloLakeDesign.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39A48AB789379757C66C6884 /* HoloLakeDesign.swift */; };
|
||||||
|
C0E93C7456C0A3BE8BA595FC /* HoloLakeSyncClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = B91D61B90D609EF17397D193 /* HoloLakeSyncClient.swift */; };
|
||||||
|
FFEEE7FC9ED4ECCD91A96AF1 /* HoloLakeModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 001F5647617271E7611365B1 /* HoloLakeModels.swift */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
CD999C930B8B85FE1C3D884D /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 7BB68CAE65448CB9319F9FE1 /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 306917F839BF1A325E2C4CF4;
|
||||||
|
remoteInfo = HoloLakeMobile;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
001F5647617271E7611365B1 /* HoloLakeModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeModels.swift; sourceTree = "<group>"; };
|
||||||
|
39A48AB789379757C66C6884 /* HoloLakeDesign.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeDesign.swift; sourceTree = "<group>"; };
|
||||||
|
6DCE2A5796B15DAC37985182 /* HoloLakeMobileApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeMobileApp.swift; sourceTree = "<group>"; };
|
||||||
|
75A889326BDEACBDB756B15A /* HoloLakeKeychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeKeychain.swift; sourceTree = "<group>"; };
|
||||||
|
7BBF45AEC03B7B5DE76DE7DA /* HoloLakeMobileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeMobileTests.swift; sourceTree = "<group>"; };
|
||||||
|
AAB8B15F447567F18DF8DA22 /* HoloLakeMobileTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = HoloLakeMobileTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
B91D61B90D609EF17397D193 /* HoloLakeSyncClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HoloLakeSyncClient.swift; sourceTree = "<group>"; };
|
||||||
|
D5DF66BC64D4DCD3BF0583C4 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||||
|
F674C2EA817D94D8089E46A6 /* HoloLakeMobile.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = HoloLakeMobile.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
14AB215583F1F3DFF867AD25 /* Sources */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D5DF66BC64D4DCD3BF0583C4 /* ContentView.swift */,
|
||||||
|
39A48AB789379757C66C6884 /* HoloLakeDesign.swift */,
|
||||||
|
75A889326BDEACBDB756B15A /* HoloLakeKeychain.swift */,
|
||||||
|
6DCE2A5796B15DAC37985182 /* HoloLakeMobileApp.swift */,
|
||||||
|
001F5647617271E7611365B1 /* HoloLakeModels.swift */,
|
||||||
|
B91D61B90D609EF17397D193 /* HoloLakeSyncClient.swift */,
|
||||||
|
);
|
||||||
|
path = Sources;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
57FD470E035A4BE1AC23C789 = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
14AB215583F1F3DFF867AD25 /* Sources */,
|
||||||
|
65C6F102E3F59F8DEC7CF977 /* Tests */,
|
||||||
|
C1AA7BA4A57964238429B9FC /* Products */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
65C6F102E3F59F8DEC7CF977 /* Tests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
7BBF45AEC03B7B5DE76DE7DA /* HoloLakeMobileTests.swift */,
|
||||||
|
);
|
||||||
|
path = Tests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
C1AA7BA4A57964238429B9FC /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
F674C2EA817D94D8089E46A6 /* HoloLakeMobile.app */,
|
||||||
|
AAB8B15F447567F18DF8DA22 /* HoloLakeMobileTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
306917F839BF1A325E2C4CF4 /* HoloLakeMobile */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 821BFCD605046D5BD179550C /* Build configuration list for PBXNativeTarget "HoloLakeMobile" */;
|
||||||
|
buildPhases = (
|
||||||
|
EF1823B21F5FD988A9F1C68B /* Sources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = HoloLakeMobile;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = HoloLakeMobile;
|
||||||
|
productReference = F674C2EA817D94D8089E46A6 /* HoloLakeMobile.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
9F8DF7CBFC2D6315E6807301 /* HoloLakeMobileTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 94843BB695F4BFC5D7BD814C /* Build configuration list for PBXNativeTarget "HoloLakeMobileTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
C9F5955F3342913B35B01034 /* Sources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
43C074DF3E4135062FD9F2D5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = HoloLakeMobileTests;
|
||||||
|
packageProductDependencies = (
|
||||||
|
);
|
||||||
|
productName = HoloLakeMobileTests;
|
||||||
|
productReference = AAB8B15F447567F18DF8DA22 /* HoloLakeMobileTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
7BB68CAE65448CB9319F9FE1 /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1430;
|
||||||
|
TargetAttributes = {
|
||||||
|
306917F839BF1A325E2C4CF4 = {
|
||||||
|
DevelopmentTeam = 825A9L3G7Q;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
|
9F8DF7CBFC2D6315E6807301 = {
|
||||||
|
DevelopmentTeam = 825A9L3G7Q;
|
||||||
|
ProvisioningStyle = Automatic;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 9D0C9B96A22462DAABC1E8F8 /* Build configuration list for PBXProject "HoloLakeMobile" */;
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
Base,
|
||||||
|
en,
|
||||||
|
);
|
||||||
|
mainGroup = 57FD470E035A4BE1AC23C789;
|
||||||
|
minimizedProjectReferenceProxies = 1;
|
||||||
|
preferredProjectObjectVersion = 77;
|
||||||
|
productRefGroup = C1AA7BA4A57964238429B9FC /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
306917F839BF1A325E2C4CF4 /* HoloLakeMobile */,
|
||||||
|
9F8DF7CBFC2D6315E6807301 /* HoloLakeMobileTests */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
C9F5955F3342913B35B01034 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
147588FBBE65B668142D5DBE /* HoloLakeMobileTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
EF1823B21F5FD988A9F1C68B /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
4C8E5CF9FBFC541E3724C695 /* ContentView.swift in Sources */,
|
||||||
|
9BC7CC6D8BC355D731943912 /* HoloLakeDesign.swift in Sources */,
|
||||||
|
149652D6C6336752E39CEF9D /* HoloLakeKeychain.swift in Sources */,
|
||||||
|
4FD7821997D8E90E6594FC23 /* HoloLakeMobileApp.swift in Sources */,
|
||||||
|
FFEEE7FC9ED4ECCD91A96AF1 /* HoloLakeModels.swift in Sources */,
|
||||||
|
C0E93C7456C0A3BE8BA595FC /* HoloLakeSyncClient.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
43C074DF3E4135062FD9F2D5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 306917F839BF1A325E2C4CF4 /* HoloLakeMobile */;
|
||||||
|
targetProxy = CD999C930B8B85FE1C3D884D /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
41EDE5121626CC1CCE382706 /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
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;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
DEVELOPMENT_TEAM = 825A9L3G7Q;
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
MARKETING_VERSION = 0.5.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
4F5C6B6543A0A3082363F109 /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
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;
|
||||||
|
CODE_SIGN_STYLE = Automatic;
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
CURRENT_PROJECT_VERSION = 1;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
DEVELOPMENT_TEAM = 825A9L3G7Q;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"DEBUG=1",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
|
||||||
|
MARKETING_VERSION = 0.5.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||||
|
MTL_FAST_MATH = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTS_MACCATALYST = NO;
|
||||||
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
|
SWIFT_VERSION = 5.0;
|
||||||
|
TARGETED_DEVICE_FAMILY = 1;
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
512CB348BCA8C1688C5D794E /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
INFOPLIST_FILE = "Generated/HoloLakeMobileTests-Info.plist";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.guanghulab.hololake.tests;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HoloLake.app/HoloLake";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
A44510E6B199DFF6BD5BC1EF /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
|
INFOPLIST_FILE = "Generated/HoloLakeMobileTests-Info.plist";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
"@loader_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.guanghulab.hololake.tests;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/HoloLake.app/HoloLake";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
B82D688E4449B0C6DC27D25E /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
INFOPLIST_FILE = "Generated/HoloLakeMobile-Info.plist";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.guanghulab.hololake;
|
||||||
|
PRODUCT_MODULE_NAME = HoloLakeMobile;
|
||||||
|
PRODUCT_NAME = HoloLake;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
BECD79B877979A2CD86E7D9C /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
|
CODE_SIGN_IDENTITY = "iPhone Developer";
|
||||||
|
INFOPLIST_FILE = "Generated/HoloLakeMobile-Info.plist";
|
||||||
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
|
"$(inherited)",
|
||||||
|
"@executable_path/Frameworks",
|
||||||
|
);
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER = com.guanghulab.hololake;
|
||||||
|
PRODUCT_MODULE_NAME = HoloLakeMobile;
|
||||||
|
PRODUCT_NAME = HoloLake;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
821BFCD605046D5BD179550C /* Build configuration list for PBXNativeTarget "HoloLakeMobile" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
BECD79B877979A2CD86E7D9C /* Debug */,
|
||||||
|
B82D688E4449B0C6DC27D25E /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
|
94843BB695F4BFC5D7BD814C /* Build configuration list for PBXNativeTarget "HoloLakeMobileTests" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
A44510E6B199DFF6BD5BC1EF /* Debug */,
|
||||||
|
512CB348BCA8C1688C5D794E /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
|
9D0C9B96A22462DAABC1E8F8 /* Build configuration list for PBXProject "HoloLakeMobile" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
4F5C6B6543A0A3082363F109 /* Debug */,
|
||||||
|
41EDE5121626CC1CCE382706 /* Release */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Debug;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 7BB68CAE65448CB9319F9FE1 /* Project object */;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1430"
|
||||||
|
version = "1.7">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES"
|
||||||
|
runPostActionsOnFailure = "NO">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "306917F839BF1A325E2C4CF4"
|
||||||
|
BuildableName = "HoloLakeMobile.app"
|
||||||
|
BlueprintName = "HoloLakeMobile"
|
||||||
|
ReferencedContainer = "container:HoloLakeMobile.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
onlyGenerateCoverageForSpecifiedTargets = "NO">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "306917F839BF1A325E2C4CF4"
|
||||||
|
BuildableName = "HoloLakeMobile.app"
|
||||||
|
BlueprintName = "HoloLakeMobile"
|
||||||
|
ReferencedContainer = "container:HoloLakeMobile.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "NO">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "9F8DF7CBFC2D6315E6807301"
|
||||||
|
BuildableName = "HoloLakeMobileTests.xctest"
|
||||||
|
BlueprintName = "HoloLakeMobileTests"
|
||||||
|
ReferencedContainer = "container:HoloLakeMobile.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "306917F839BF1A325E2C4CF4"
|
||||||
|
BuildableName = "HoloLakeMobile.app"
|
||||||
|
BlueprintName = "HoloLakeMobile"
|
||||||
|
ReferencedContainer = "container:HoloLakeMobile.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "306917F839BF1A325E2C4CF4"
|
||||||
|
BuildableName = "HoloLakeMobile.app"
|
||||||
|
BlueprintName = "HoloLakeMobile"
|
||||||
|
ReferencedContainer = "container:HoloLakeMobile.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
</CommandLineArguments>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
import SwiftUI
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
struct ContentView: View {
|
||||||
|
@EnvironmentObject private var client: HoloLakeSyncClient
|
||||||
|
@State private var pairingText = ""
|
||||||
|
@State private var captureTitle = ""
|
||||||
|
@State private var captureBody = ""
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
ZStack {
|
||||||
|
LinearGradient(
|
||||||
|
colors: [HoloLakeDesign.deep, HoloLakeDesign.lake, HoloLakeDesign.violet.opacity(0.78)],
|
||||||
|
startPoint: .topLeading,
|
||||||
|
endPoint: .bottomTrailing
|
||||||
|
)
|
||||||
|
.ignoresSafeArea()
|
||||||
|
|
||||||
|
ScrollView {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
header
|
||||||
|
if client.session == nil { pairingCard } else { dashboard }
|
||||||
|
boundaryCard
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 18)
|
||||||
|
.padding(.vertical, 16)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toolbar(.hidden, for: .navigationBar)
|
||||||
|
.alert("HoloLake", isPresented: Binding(
|
||||||
|
get: { client.errorText != nil },
|
||||||
|
set: { if !$0 { client.errorText = nil } }
|
||||||
|
)) {
|
||||||
|
Button("知道了", role: .cancel) { client.errorText = nil }
|
||||||
|
} message: {
|
||||||
|
Text(client.errorText ?? "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.preferredColorScheme(.dark)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var header: some View {
|
||||||
|
HStack(alignment: .center) {
|
||||||
|
VStack(alignment: .leading, spacing: 5) {
|
||||||
|
Text("HoloLake").font(.title.weight(.semibold)).tracking(2)
|
||||||
|
Text("同一频道 · iPhone 轻入口")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(HoloLakeDesign.muted)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Circle()
|
||||||
|
.fill(client.session == nil ? HoloLakeDesign.gold : HoloLakeDesign.mint)
|
||||||
|
.frame(width: 11, height: 11)
|
||||||
|
.shadow(color: client.session == nil ? HoloLakeDesign.gold : HoloLakeDesign.mint, radius: 8)
|
||||||
|
}
|
||||||
|
.foregroundStyle(HoloLakeDesign.pearl)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var pairingCard: some View {
|
||||||
|
LakeCard {
|
||||||
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
Text("连接电脑端").font(.title3.weight(.semibold))
|
||||||
|
Text("先在电脑端打开“多端同步”,再扫描配对码或粘贴配对链接。配对密钥只保存在这台 iPhone 的钥匙串。")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(HoloLakeDesign.muted)
|
||||||
|
|
||||||
|
TextField("hololake://pair?…", text: $pairingText, axis: .vertical)
|
||||||
|
.textInputAutocapitalization(.never)
|
||||||
|
.autocorrectionDisabled()
|
||||||
|
.padding(14)
|
||||||
|
.background(.black.opacity(0.22), in: RoundedRectangle(cornerRadius: 16))
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Button("从剪贴板读取") {
|
||||||
|
pairingText = UIPasteboard.general.string ?? ""
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
|
||||||
|
Button("校验并配对") {
|
||||||
|
Task { await client.pair(from: pairingText) }
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(HoloLakeDesign.gold)
|
||||||
|
.foregroundStyle(HoloLakeDesign.deep)
|
||||||
|
.disabled(pairingText.isEmpty || client.isWorking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.foregroundStyle(HoloLakeDesign.pearl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var dashboard: some View {
|
||||||
|
VStack(spacing: 16) {
|
||||||
|
LakeCard {
|
||||||
|
VStack(alignment: .leading, spacing: 14) {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
|
Text(client.session?.desktopName ?? "电脑端")
|
||||||
|
.font(.title3.weight(.semibold))
|
||||||
|
Text(client.stateText)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(HoloLakeDesign.muted)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
Task { await client.sync() }
|
||||||
|
} label: {
|
||||||
|
if client.isWorking {
|
||||||
|
ProgressView()
|
||||||
|
} else {
|
||||||
|
Label("同步", systemImage: "arrow.triangle.2.circlepath")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.borderedProminent)
|
||||||
|
.tint(HoloLakeDesign.mint)
|
||||||
|
.foregroundStyle(HoloLakeDesign.deep)
|
||||||
|
.disabled(client.isWorking)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let snapshot = client.snapshot {
|
||||||
|
HStack(spacing: 10) {
|
||||||
|
MetricTile(label: "作品", value: "\(snapshot.webNovel.workCount)")
|
||||||
|
MetricTile(label: "章节", value: "\(snapshot.webNovel.chapterCount)")
|
||||||
|
MetricTile(label: "教育表", value: "\(snapshot.education.activeTableCount)")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text("保持静止;只有你点击同步时才会连接电脑。")
|
||||||
|
.font(.subheadline)
|
||||||
|
.foregroundStyle(HoloLakeDesign.muted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LakeCard {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("随手记回频道").font(.headline)
|
||||||
|
TextField("标题(可选)", text: $captureTitle)
|
||||||
|
.padding(12)
|
||||||
|
.background(.black.opacity(0.2), in: RoundedRectangle(cornerRadius: 14))
|
||||||
|
TextField("正文", text: $captureBody, axis: .vertical)
|
||||||
|
.lineLimit(4...10)
|
||||||
|
.padding(12)
|
||||||
|
.background(.black.opacity(0.2), in: RoundedRectangle(cornerRadius: 14))
|
||||||
|
Button("写入电脑端收件箱") {
|
||||||
|
let title = captureTitle
|
||||||
|
let body = captureBody
|
||||||
|
Task {
|
||||||
|
await client.sync(captureTitle: title, captureBody: body)
|
||||||
|
if client.errorText == nil {
|
||||||
|
captureTitle = ""
|
||||||
|
captureBody = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
.disabled(captureBody.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || client.isWorking)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let works = client.snapshot?.webNovel.works, !works.isEmpty {
|
||||||
|
LakeCard {
|
||||||
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
|
Text("最近作品").font(.headline)
|
||||||
|
ForEach(works.prefix(5)) { work in
|
||||||
|
HStack {
|
||||||
|
Text(work.title).lineLimit(1)
|
||||||
|
Spacer()
|
||||||
|
Text(work.status)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(HoloLakeDesign.muted)
|
||||||
|
}
|
||||||
|
if work.id != works.prefix(5).last?.id {
|
||||||
|
Divider().overlay(.white.opacity(0.12))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button("解除本机配对", role: .destructive) {
|
||||||
|
client.disconnect()
|
||||||
|
}
|
||||||
|
.font(.footnote)
|
||||||
|
}
|
||||||
|
.foregroundStyle(HoloLakeDesign.pearl)
|
||||||
|
}
|
||||||
|
|
||||||
|
private var boundaryCard: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text("HLP-IOS-CLIENT-001")
|
||||||
|
.font(.caption2.monospaced())
|
||||||
|
.foregroundStyle(HoloLakeDesign.gold)
|
||||||
|
Text("手机是同一频道的远程身体入口,不复制电脑桌面,不在电脑离线时执行人格或行业任务。")
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundStyle(HoloLakeDesign.muted)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(.horizontal, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
enum HoloLakeDesign {
|
||||||
|
static let deep = Color(red: 0.025, green: 0.045, blue: 0.11)
|
||||||
|
static let lake = Color(red: 0.08, green: 0.14, blue: 0.32)
|
||||||
|
static let violet = Color(red: 0.34, green: 0.19, blue: 0.52)
|
||||||
|
static let pearl = Color(red: 0.95, green: 0.96, blue: 1)
|
||||||
|
static let muted = Color(red: 0.62, green: 0.66, blue: 0.78)
|
||||||
|
static let gold = Color(red: 0.96, green: 0.87, blue: 0.62)
|
||||||
|
static let mint = Color(red: 0.43, green: 0.93, blue: 0.78)
|
||||||
|
}
|
||||||
|
|
||||||
|
struct LakeCard<Content: View>: View {
|
||||||
|
@ViewBuilder let content: Content
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
content
|
||||||
|
.padding(18)
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.background(.white.opacity(0.075), in: RoundedRectangle(cornerRadius: 24))
|
||||||
|
.overlay {
|
||||||
|
RoundedRectangle(cornerRadius: 24)
|
||||||
|
.stroke(.white.opacity(0.14), lineWidth: 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MetricTile: View {
|
||||||
|
let label: String
|
||||||
|
let value: String
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text(label).font(.caption).foregroundStyle(HoloLakeDesign.muted)
|
||||||
|
Text(value).font(.title2.weight(.semibold)).foregroundStyle(HoloLakeDesign.pearl)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .leading)
|
||||||
|
.padding(14)
|
||||||
|
.background(.white.opacity(0.055), in: RoundedRectangle(cornerRadius: 18))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
|
enum HoloLakeKeychain {
|
||||||
|
private static let service = "com.guanghulab.hololake.mobile-sync"
|
||||||
|
private static let account = "HLP-IOS-CLIENT-001"
|
||||||
|
|
||||||
|
static func load() throws -> StoredSession? {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: account,
|
||||||
|
kSecReturnData as String: true,
|
||||||
|
kSecMatchLimit as String: kSecMatchLimitOne
|
||||||
|
]
|
||||||
|
var result: CFTypeRef?
|
||||||
|
let status = SecItemCopyMatching(query as CFDictionary, &result)
|
||||||
|
if status == errSecItemNotFound { return nil }
|
||||||
|
guard status == errSecSuccess, let data = result as? Data else {
|
||||||
|
throw HoloLakeMobileError.keychain(status)
|
||||||
|
}
|
||||||
|
return try JSONDecoder().decode(StoredSession.self, from: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func save(_ session: StoredSession) throws {
|
||||||
|
let data = try JSONEncoder().encode(session)
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: account
|
||||||
|
]
|
||||||
|
let attributes: [String: Any] = [
|
||||||
|
kSecValueData as String: data,
|
||||||
|
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
|
||||||
|
]
|
||||||
|
let update = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
|
||||||
|
if update == errSecItemNotFound {
|
||||||
|
var insertion = query
|
||||||
|
insertion.merge(attributes) { _, new in new }
|
||||||
|
let status = SecItemAdd(insertion as CFDictionary, nil)
|
||||||
|
guard status == errSecSuccess else { throw HoloLakeMobileError.keychain(status) }
|
||||||
|
} else if update != errSecSuccess {
|
||||||
|
throw HoloLakeMobileError.keychain(update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func remove() throws {
|
||||||
|
let query: [String: Any] = [
|
||||||
|
kSecClass as String: kSecClassGenericPassword,
|
||||||
|
kSecAttrService as String: service,
|
||||||
|
kSecAttrAccount as String: account
|
||||||
|
]
|
||||||
|
let status = SecItemDelete(query as CFDictionary)
|
||||||
|
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||||
|
throw HoloLakeMobileError.keychain(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct HoloLakeMobileApp: App {
|
||||||
|
@StateObject private var client = HoloLakeSyncClient()
|
||||||
|
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
ContentView()
|
||||||
|
.environmentObject(client)
|
||||||
|
.onOpenURL { url in
|
||||||
|
Task { await client.pair(with: url) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,177 @@
|
||||||
|
import Foundation
|
||||||
|
import Security
|
||||||
|
|
||||||
|
struct PairingDescriptor: Equatable {
|
||||||
|
let host: String
|
||||||
|
let port: Int
|
||||||
|
let pairingID: String
|
||||||
|
let secret: Data
|
||||||
|
|
||||||
|
init(url: URL) throws {
|
||||||
|
guard url.scheme == "hololake", url.host == "pair",
|
||||||
|
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||||
|
throw HoloLakeMobileError.invalidPairingLink
|
||||||
|
}
|
||||||
|
var values: [String: String] = [:]
|
||||||
|
for item in components.queryItems ?? [] {
|
||||||
|
guard let value = item.value, values[item.name] == nil else {
|
||||||
|
throw HoloLakeMobileError.invalidPairingLink
|
||||||
|
}
|
||||||
|
values[item.name] = value
|
||||||
|
}
|
||||||
|
guard let host = values["host"], !host.isEmpty,
|
||||||
|
let portText = values["port"], let port = Int(portText), (1...65535).contains(port),
|
||||||
|
let pairingID = values["id"], UUID(uuidString: pairingID) != nil,
|
||||||
|
let secretText = values["secret"],
|
||||||
|
let secret = Data(base64URL: secretText), secret.count == 32 else {
|
||||||
|
throw HoloLakeMobileError.invalidPairingLink
|
||||||
|
}
|
||||||
|
self.host = host
|
||||||
|
self.port = port
|
||||||
|
self.pairingID = pairingID
|
||||||
|
self.secret = secret
|
||||||
|
}
|
||||||
|
|
||||||
|
var baseURL: URL {
|
||||||
|
URL(string: "http://\(host):\(port)")!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StoredSession: Codable, Equatable {
|
||||||
|
let host: String
|
||||||
|
let port: Int
|
||||||
|
let deviceID: String
|
||||||
|
let sessionKey: Data
|
||||||
|
let desktopName: String
|
||||||
|
var counter: UInt64
|
||||||
|
var cursor: UInt64
|
||||||
|
|
||||||
|
var baseURL: URL {
|
||||||
|
URL(string: "http://\(host):\(port)")!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PairRequest: Codable {
|
||||||
|
let deviceName: String
|
||||||
|
let platform: String
|
||||||
|
let requestID: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct PairResponse: Codable {
|
||||||
|
let schema: String
|
||||||
|
let state: String
|
||||||
|
let deviceID: String
|
||||||
|
let sessionKey: String
|
||||||
|
let desktopName: String
|
||||||
|
let rootNodeRole: String
|
||||||
|
let requestID: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SyncRequest: Codable {
|
||||||
|
let counter: UInt64
|
||||||
|
let afterCursor: UInt64?
|
||||||
|
let capture: MobileCaptureInput?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MobileCaptureInput: Codable {
|
||||||
|
let title: String
|
||||||
|
let body: String
|
||||||
|
let requestID: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MobileSyncSnapshot: Codable {
|
||||||
|
let schema: String
|
||||||
|
let state: String
|
||||||
|
let cursor: UInt64
|
||||||
|
let generatedAtUnixMs: UInt64
|
||||||
|
let desktopName: String
|
||||||
|
let rootNodeOnline: Bool
|
||||||
|
let personalChannel: MobileChannelProjection
|
||||||
|
let webNovel: MobileWebNovelProjection
|
||||||
|
let education: MobileEducationProjection
|
||||||
|
let recentCaptures: [MobileCaptureProjection]
|
||||||
|
let boundary: MobileBoundaryProjection
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MobileChannelProjection: Codable {
|
||||||
|
let home: String
|
||||||
|
let growthEventCount: UInt64
|
||||||
|
let integrity: String
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MobileWebNovelProjection: Codable {
|
||||||
|
let workCount: UInt64
|
||||||
|
let volumeCount: UInt64
|
||||||
|
let chapterCount: UInt64
|
||||||
|
let works: [MobileWebNovelWork]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MobileWebNovelWork: Codable, Identifiable {
|
||||||
|
let workID: String
|
||||||
|
let title: String
|
||||||
|
let status: String
|
||||||
|
let updatedAtUnixMs: UInt64
|
||||||
|
var id: String { workID }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MobileEducationProjection: Codable {
|
||||||
|
let activeTableCount: UInt64
|
||||||
|
let archivedTableCount: UInt64
|
||||||
|
let unassignedTableCount: UInt64
|
||||||
|
let sensitiveValuesIncluded: Bool
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MobileCaptureProjection: Codable, Identifiable {
|
||||||
|
let cursor: UInt64
|
||||||
|
let captureID: String
|
||||||
|
let title: String
|
||||||
|
let body: String
|
||||||
|
let sourceDeviceID: String
|
||||||
|
let createdAtUnixMs: UInt64
|
||||||
|
var id: String { captureID }
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MobileBoundaryProjection: Codable {
|
||||||
|
let mobileRole: String
|
||||||
|
let remoteDesktopClone: Bool
|
||||||
|
let desktopOfflineExecution: Bool
|
||||||
|
let sensitiveEducationValues: String
|
||||||
|
let modelAPI: String
|
||||||
|
}
|
||||||
|
|
||||||
|
enum HoloLakeMobileError: LocalizedError {
|
||||||
|
case invalidPairingLink
|
||||||
|
case invalidResponse
|
||||||
|
case rejected(String)
|
||||||
|
case missingNonce
|
||||||
|
case missingSession
|
||||||
|
case keychain(OSStatus)
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .invalidPairingLink: return "配对链接无效或已损坏。"
|
||||||
|
case .invalidResponse: return "电脑端返回了无法校验的响应。"
|
||||||
|
case .rejected(let code): return "电脑端拒绝了本次请求:\(code)"
|
||||||
|
case .missingNonce: return "加密响应缺少一次性随机数。"
|
||||||
|
case .missingSession: return "请先与电脑端 HoloLake 配对。"
|
||||||
|
case .keychain(let status): return "本机钥匙串写入失败(\(status))。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
init?(base64URL value: String) {
|
||||||
|
var text = value.replacingOccurrences(of: "-", with: "+")
|
||||||
|
.replacingOccurrences(of: "_", with: "/")
|
||||||
|
text += String(repeating: "=", count: (4 - text.count % 4) % 4)
|
||||||
|
guard let data = Data(base64Encoded: text) else { return nil }
|
||||||
|
self = data
|
||||||
|
}
|
||||||
|
|
||||||
|
var base64URL: String {
|
||||||
|
base64EncodedString()
|
||||||
|
.replacingOccurrences(of: "+", with: "-")
|
||||||
|
.replacingOccurrences(of: "/", with: "_")
|
||||||
|
.replacingOccurrences(of: "=", with: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,190 @@
|
||||||
|
import CryptoKit
|
||||||
|
import Foundation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class HoloLakeSyncClient: ObservableObject {
|
||||||
|
static let clientNumber = "HLP-IOS-CLIENT-001"
|
||||||
|
static let pairAAD = Data("hololake.mobile.pair/v1".utf8)
|
||||||
|
|
||||||
|
@Published private(set) var session: StoredSession?
|
||||||
|
@Published private(set) var snapshot: MobileSyncSnapshot?
|
||||||
|
@Published private(set) var stateText = "尚未配对"
|
||||||
|
@Published private(set) var isWorking = false
|
||||||
|
@Published var errorText: String?
|
||||||
|
|
||||||
|
init() {
|
||||||
|
do {
|
||||||
|
session = try HoloLakeKeychain.load()
|
||||||
|
stateText = session == nil ? "尚未配对" : "已配对,等待手动同步"
|
||||||
|
} catch {
|
||||||
|
errorText = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pair(with url: URL) async {
|
||||||
|
await perform("正在校验配对…") {
|
||||||
|
let descriptor = try PairingDescriptor(url: url)
|
||||||
|
let requestID = UUID().uuidString.lowercased()
|
||||||
|
let payload = PairRequest(
|
||||||
|
deviceName: UIDevice.current.name,
|
||||||
|
platform: "IOS",
|
||||||
|
requestID: requestID
|
||||||
|
)
|
||||||
|
let responseData = try await Self.exchange(
|
||||||
|
url: descriptor.baseURL.appending(path: "v1/pair"),
|
||||||
|
key: descriptor.secret,
|
||||||
|
aad: Self.pairAAD,
|
||||||
|
headers: ["X-HoloLake-Pairing-ID": descriptor.pairingID],
|
||||||
|
payload: payload
|
||||||
|
)
|
||||||
|
let response = try JSONDecoder().decode(PairResponse.self, from: responseData)
|
||||||
|
guard response.schema == "hololake.mobile-sync/v1",
|
||||||
|
response.state == "PAIRED",
|
||||||
|
response.rootNodeRole == "USER_LOCAL_COMPUTER_TERMINAL",
|
||||||
|
response.requestID == requestID,
|
||||||
|
let sessionKey = Data(base64URL: response.sessionKey), sessionKey.count == 32 else {
|
||||||
|
throw HoloLakeMobileError.invalidResponse
|
||||||
|
}
|
||||||
|
let next = StoredSession(
|
||||||
|
host: descriptor.host,
|
||||||
|
port: descriptor.port,
|
||||||
|
deviceID: response.deviceID,
|
||||||
|
sessionKey: sessionKey,
|
||||||
|
desktopName: response.desktopName,
|
||||||
|
counter: 0,
|
||||||
|
cursor: 0
|
||||||
|
)
|
||||||
|
try HoloLakeKeychain.save(next)
|
||||||
|
session = next
|
||||||
|
stateText = "已与 \(response.desktopName) 配对"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func pair(from text: String) async {
|
||||||
|
guard let url = URL(string: text.trimmingCharacters(in: .whitespacesAndNewlines)) else {
|
||||||
|
errorText = HoloLakeMobileError.invalidPairingLink.localizedDescription
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await pair(with: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
func sync(captureTitle: String? = nil, captureBody: String? = nil) async {
|
||||||
|
await perform("正在与电脑同步…") {
|
||||||
|
guard var current = session else { throw HoloLakeMobileError.missingSession }
|
||||||
|
current.counter += 1
|
||||||
|
// Consume and persist the counter before transport. If the root node accepts a
|
||||||
|
// request but its response is lost, the next manual retry still moves forward.
|
||||||
|
try HoloLakeKeychain.save(current)
|
||||||
|
session = current
|
||||||
|
let capture: MobileCaptureInput?
|
||||||
|
if let body = captureBody?.trimmingCharacters(in: .whitespacesAndNewlines), !body.isEmpty {
|
||||||
|
capture = MobileCaptureInput(
|
||||||
|
title: (captureTitle ?? "").trimmingCharacters(in: .whitespacesAndNewlines),
|
||||||
|
body: body,
|
||||||
|
requestID: UUID().uuidString.lowercased()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
capture = nil
|
||||||
|
}
|
||||||
|
let aad = Data("hololake.mobile.sync/v1:\(current.deviceID)".utf8)
|
||||||
|
let responseData = try await Self.exchange(
|
||||||
|
url: current.baseURL.appending(path: "v1/sync"),
|
||||||
|
key: current.sessionKey,
|
||||||
|
aad: aad,
|
||||||
|
headers: ["X-HoloLake-Device-ID": current.deviceID],
|
||||||
|
payload: SyncRequest(counter: current.counter, afterCursor: nil, capture: capture)
|
||||||
|
)
|
||||||
|
let nextSnapshot = try JSONDecoder().decode(MobileSyncSnapshot.self, from: responseData)
|
||||||
|
guard nextSnapshot.schema == "hololake.mobile-sync/v1",
|
||||||
|
nextSnapshot.state == "SYNCED_WITH_ROOT_NODE",
|
||||||
|
nextSnapshot.rootNodeOnline,
|
||||||
|
nextSnapshot.boundary.mobileRole == "REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM",
|
||||||
|
!nextSnapshot.boundary.remoteDesktopClone,
|
||||||
|
!nextSnapshot.boundary.desktopOfflineExecution else {
|
||||||
|
throw HoloLakeMobileError.invalidResponse
|
||||||
|
}
|
||||||
|
current.cursor = nextSnapshot.cursor
|
||||||
|
try HoloLakeKeychain.save(current)
|
||||||
|
session = current
|
||||||
|
snapshot = nextSnapshot
|
||||||
|
stateText = "已同步 · \(nextSnapshot.desktopName)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func disconnect() {
|
||||||
|
do {
|
||||||
|
try HoloLakeKeychain.remove()
|
||||||
|
session = nil
|
||||||
|
snapshot = nil
|
||||||
|
stateText = "尚未配对"
|
||||||
|
} catch {
|
||||||
|
errorText = error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func perform(_ activeText: String, operation: () async throws -> Void) async {
|
||||||
|
guard !isWorking else { return }
|
||||||
|
isWorking = true
|
||||||
|
errorText = nil
|
||||||
|
stateText = activeText
|
||||||
|
do {
|
||||||
|
try await operation()
|
||||||
|
} catch {
|
||||||
|
errorText = error.localizedDescription
|
||||||
|
stateText = session == nil ? "尚未配对" : "同步未完成"
|
||||||
|
}
|
||||||
|
isWorking = false
|
||||||
|
}
|
||||||
|
|
||||||
|
static func exchange<Payload: Encodable>(
|
||||||
|
url: URL,
|
||||||
|
key: Data,
|
||||||
|
aad: Data,
|
||||||
|
headers: [String: String],
|
||||||
|
payload: Payload,
|
||||||
|
session: URLSession = .shared
|
||||||
|
) async throws -> Data {
|
||||||
|
let clear = try JSONEncoder().encode(payload)
|
||||||
|
let nonceData = randomNonce()
|
||||||
|
let nonce = try ChaChaPoly.Nonce(data: nonceData)
|
||||||
|
let sealed = try ChaChaPoly.seal(
|
||||||
|
clear,
|
||||||
|
using: SymmetricKey(data: key),
|
||||||
|
nonce: nonce,
|
||||||
|
authenticating: aad
|
||||||
|
)
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.timeoutInterval = 8
|
||||||
|
request.cachePolicy = .reloadIgnoringLocalCacheData
|
||||||
|
request.httpBody = sealed.ciphertext + sealed.tag
|
||||||
|
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")
|
||||||
|
request.setValue(nonceData.base64URL, forHTTPHeaderField: "X-HoloLake-Nonce")
|
||||||
|
headers.forEach { request.setValue($1, forHTTPHeaderField: $0) }
|
||||||
|
let (body, response) = try await session.data(for: request)
|
||||||
|
guard let http = response as? HTTPURLResponse else {
|
||||||
|
throw HoloLakeMobileError.invalidResponse
|
||||||
|
}
|
||||||
|
guard http.statusCode == 200 else {
|
||||||
|
let code = (try? JSONSerialization.jsonObject(with: body) as? [String: Any])?["code"] as? String
|
||||||
|
throw HoloLakeMobileError.rejected(code ?? "HTTP_\(http.statusCode)")
|
||||||
|
}
|
||||||
|
guard let nonceHeader = http.value(forHTTPHeaderField: "X-HoloLake-Nonce"),
|
||||||
|
let responseNonceData = Data(base64URL: nonceHeader), responseNonceData.count == 12 else {
|
||||||
|
throw HoloLakeMobileError.missingNonce
|
||||||
|
}
|
||||||
|
let responseNonce = try ChaChaPoly.Nonce(data: responseNonceData)
|
||||||
|
guard body.count >= 16 else { throw HoloLakeMobileError.invalidResponse }
|
||||||
|
let responseBox = try ChaChaPoly.SealedBox(
|
||||||
|
nonce: responseNonce,
|
||||||
|
ciphertext: body.dropLast(16),
|
||||||
|
tag: body.suffix(16)
|
||||||
|
)
|
||||||
|
return try ChaChaPoly.open(responseBox, using: SymmetricKey(data: key), authenticating: aad)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func randomNonce() -> Data {
|
||||||
|
Data((0..<12).map { _ in UInt8.random(in: .min ... .max) })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
import CryptoKit
|
||||||
|
import XCTest
|
||||||
|
@testable import HoloLakeMobile
|
||||||
|
|
||||||
|
final class HoloLakeMobileTests: XCTestCase {
|
||||||
|
func testPairingCoordinateParsesOnlyCompleteLink() throws {
|
||||||
|
let secret = Data(repeating: 7, count: 32).base64URL
|
||||||
|
let id = UUID().uuidString.lowercased()
|
||||||
|
let url = try XCTUnwrap(URL(string: "hololake://pair?host=192.168.1.8&port=37421&id=\(id)&secret=\(secret)"))
|
||||||
|
let value = try PairingDescriptor(url: url)
|
||||||
|
XCTAssertEqual(value.host, "192.168.1.8")
|
||||||
|
XCTAssertEqual(value.port, 37421)
|
||||||
|
XCTAssertEqual(value.pairingID, id)
|
||||||
|
XCTAssertEqual(value.secret, Data(repeating: 7, count: 32))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPairingCoordinateRejectsMissingSecret() throws {
|
||||||
|
let url = try XCTUnwrap(URL(string: "hololake://pair?host=127.0.0.1&port=37421&id=\(UUID().uuidString)"))
|
||||||
|
XCTAssertThrowsError(try PairingDescriptor(url: url))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testPairingCoordinateRejectsDuplicateKeys() throws {
|
||||||
|
let secret = Data(repeating: 7, count: 32).base64URL
|
||||||
|
let id = UUID().uuidString.lowercased()
|
||||||
|
let url = try XCTUnwrap(URL(string: "hololake://pair?host=192.168.1.8&host=127.0.0.1&port=37421&id=\(id)&secret=\(secret)"))
|
||||||
|
XCTAssertThrowsError(try PairingDescriptor(url: url))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testCryptoKitPayloadMatchesServerWireShape() throws {
|
||||||
|
let key = SymmetricKey(data: Data(repeating: 3, count: 32))
|
||||||
|
let nonce = try ChaChaPoly.Nonce(data: Data(repeating: 9, count: 12))
|
||||||
|
let aad = Data("hololake.mobile.pair/v1".utf8)
|
||||||
|
let clear = Data("hello".utf8)
|
||||||
|
let sealed = try ChaChaPoly.seal(clear, using: key, nonce: nonce, authenticating: aad)
|
||||||
|
let wire = sealed.ciphertext + sealed.tag
|
||||||
|
XCTAssertEqual(wire.count, clear.count + 16)
|
||||||
|
let restored = try ChaChaPoly.open(
|
||||||
|
ChaChaPoly.SealedBox(nonce: nonce, ciphertext: wire.dropLast(16), tag: wire.suffix(16)),
|
||||||
|
using: key,
|
||||||
|
authenticating: aad
|
||||||
|
)
|
||||||
|
XCTAssertEqual(restored, clear)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,63 @@
|
||||||
|
name: HoloLakeMobile
|
||||||
|
options:
|
||||||
|
bundleIdPrefix: com.guanghulab
|
||||||
|
deploymentTarget:
|
||||||
|
iOS: "17.0"
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
DEVELOPMENT_TEAM: 825A9L3G7Q
|
||||||
|
CODE_SIGN_STYLE: Automatic
|
||||||
|
SWIFT_VERSION: "5.0"
|
||||||
|
CURRENT_PROJECT_VERSION: 1
|
||||||
|
MARKETING_VERSION: 0.5.0
|
||||||
|
TARGETED_DEVICE_FAMILY: "1"
|
||||||
|
SUPPORTS_MACCATALYST: false
|
||||||
|
targets:
|
||||||
|
HoloLakeMobile:
|
||||||
|
type: application
|
||||||
|
platform: iOS
|
||||||
|
sources:
|
||||||
|
- path: Sources
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: com.guanghulab.hololake
|
||||||
|
PRODUCT_NAME: HoloLake
|
||||||
|
PRODUCT_MODULE_NAME: HoloLakeMobile
|
||||||
|
info:
|
||||||
|
path: Generated/HoloLakeMobile-Info.plist
|
||||||
|
properties:
|
||||||
|
CFBundleDisplayName: HoloLake
|
||||||
|
CFBundleShortVersionString: "$(MARKETING_VERSION)"
|
||||||
|
CFBundleVersion: "$(CURRENT_PROJECT_VERSION)"
|
||||||
|
LSApplicationCategoryType: public.app-category.productivity
|
||||||
|
NSLocalNetworkUsageDescription: HoloLake 仅在你明确配对后,通过同一局域网连接你的电脑端频道。
|
||||||
|
UIApplicationSupportsIndirectInputEvents: true
|
||||||
|
UIRequiresFullScreen: true
|
||||||
|
UILaunchScreen: {}
|
||||||
|
UISupportedInterfaceOrientations:
|
||||||
|
- UIInterfaceOrientationPortrait
|
||||||
|
CFBundleURLTypes:
|
||||||
|
- CFBundleTypeRole: Editor
|
||||||
|
CFBundleURLName: com.guanghulab.hololake.pair
|
||||||
|
CFBundleURLSchemes:
|
||||||
|
- hololake
|
||||||
|
NSAppTransportSecurity:
|
||||||
|
NSAllowsLocalNetworking: true
|
||||||
|
scheme:
|
||||||
|
testTargets:
|
||||||
|
- HoloLakeMobileTests
|
||||||
|
HoloLakeMobileTests:
|
||||||
|
type: bundle.unit-test
|
||||||
|
platform: iOS
|
||||||
|
sources:
|
||||||
|
- path: Tests
|
||||||
|
dependencies:
|
||||||
|
- target: HoloLakeMobile
|
||||||
|
settings:
|
||||||
|
base:
|
||||||
|
PRODUCT_BUNDLE_IDENTIFIER: com.guanghulab.hololake.tests
|
||||||
|
TEST_HOST: "$(BUILT_PRODUCTS_DIR)/HoloLake.app/HoloLake"
|
||||||
|
BUNDLE_LOADER: "$(TEST_HOST)"
|
||||||
|
info:
|
||||||
|
path: Generated/HoloLakeMobileTests-Info.plist
|
||||||
|
properties: {}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import { readFileSync } from 'node:fs'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
const read = (path) => readFileSync(new URL(`../${path}`, import.meta.url), 'utf8')
|
||||||
|
const contract = JSON.parse(read('contracts/ios-mobile-client-v1.json'))
|
||||||
|
const project = read('mobile/ios/project.yml')
|
||||||
|
const client = read('mobile/ios/Sources/HoloLakeSyncClient.swift')
|
||||||
|
const keychain = read('mobile/ios/Sources/HoloLakeKeychain.swift')
|
||||||
|
const models = read('mobile/ios/Sources/HoloLakeModels.swift')
|
||||||
|
const interfaceSource = read('mobile/ios/Sources/ContentView.swift')
|
||||||
|
|
||||||
|
test('iPhone client is the numbered remote body entry of the desktop root node', () => {
|
||||||
|
assert.equal(contract.record_id, 'HLP-IOS-CLIENT-001')
|
||||||
|
assert.equal(contract.role, 'REMOTE_BODY_ENTRY_OF_THE_SAME_PERSONA_SYSTEM')
|
||||||
|
assert.equal(contract.root_node, 'USER_LOCAL_COMPUTER_TERMINAL')
|
||||||
|
assert.equal(contract.numbered_transport.desktop_module, 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001')
|
||||||
|
assert.match(project, /PRODUCT_BUNDLE_IDENTIFIER: com\.guanghulab\.hololake/)
|
||||||
|
assert.match(client, /static let clientNumber = "HLP-IOS-CLIENT-001"/)
|
||||||
|
assert.match(interfaceSource, /HLP-IOS-CLIENT-001/)
|
||||||
|
assert.equal(contract.state, 'NUMBERED_SOURCE_SIMULATOR_AND_DEVELOPMENT_SIGNED_IPA_ACCEPTED')
|
||||||
|
assert.equal(contract.acceptance.unit_tests, 'PASS_4')
|
||||||
|
assert.equal(contract.acceptance.real_iphone_pair_and_sync, 'NOT_CLAIMED')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('iPhone secrets stay device local and the transport matches the desktop protocol', () => {
|
||||||
|
assert.equal(contract.security.key_storage, 'KEYCHAIN_WHEN_UNLOCKED_THIS_DEVICE_ONLY')
|
||||||
|
assert.equal(contract.security.background_polling, false)
|
||||||
|
assert.equal(contract.security.remote_desktop_clone, false)
|
||||||
|
assert.match(keychain, /kSecAttrAccessibleWhenUnlockedThisDeviceOnly/)
|
||||||
|
assert.match(client, /ChaChaPoly\.seal/)
|
||||||
|
assert.match(client, /ChaChaPoly\.open/)
|
||||||
|
assert.match(client, /hololake\.mobile\.pair\/v1/)
|
||||||
|
assert.match(client, /hololake\.mobile\.sync\/v1:/)
|
||||||
|
assert.match(client, /response\.rootNodeRole == "USER_LOCAL_COMPUTER_TERMINAL"/)
|
||||||
|
assert.match(client, /nextSnapshot\.rootNodeOnline/)
|
||||||
|
assert.ok(client.indexOf('try HoloLakeKeychain.save(current)') < client.indexOf('url: current.baseURL.appending(path: "v1/sync")'))
|
||||||
|
assert.match(models, /hololake/)
|
||||||
|
assert.doesNotMatch(client, /Timer|scheduledTimer|setInterval|backgroundTask/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('mobile interface stays manual and exposes only bounded projections', () => {
|
||||||
|
assert.equal(contract.interface.idle_state, 'STATIC')
|
||||||
|
assert.equal(contract.interface.manual_sync_only, true)
|
||||||
|
assert.match(interfaceSource, /只有你点击同步时才会连接电脑/)
|
||||||
|
assert.match(interfaceSource, /写入电脑端收件箱/)
|
||||||
|
assert.doesNotMatch(interfaceSource, /人格执行|模型调用|远程桌面/)
|
||||||
|
})
|
||||||
|
|
@ -6,13 +6,21 @@ const catalog = JSON.parse(readFileSync(new URL('../contracts/module-donor-admis
|
||||||
const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8'))
|
const numbered = JSON.parse(readFileSync(new URL('../contracts/numbered-ipc-registry.json', import.meta.url), 'utf8'))
|
||||||
|
|
||||||
test('chaotic donors are read-only candidates and never a bulk merge source', () => {
|
test('chaotic donors are read-only candidates and never a bulk merge source', () => {
|
||||||
assert.equal(catalog.state, 'SEVEN_CANDIDATES_ADMITTED_DONOR_SOURCES_REMAIN_READ_ONLY')
|
assert.equal(catalog.state, 'EIGHT_CANDIDATES_ADMITTED_DONOR_SOURCES_REMAIN_READ_ONLY')
|
||||||
assert.equal(catalog.root_rule.repair_old_application_in_place, false)
|
assert.equal(catalog.root_rule.repair_old_application_in_place, false)
|
||||||
assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false)
|
assert.equal(catalog.root_rule.bulk_merge_or_wholesale_copy_allowed, false)
|
||||||
assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true)
|
assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true)
|
||||||
assert.ok(catalog.donors.every((donor) => donor.state.startsWith('READ_ONLY')))
|
assert.ok(catalog.donors.every((donor) => donor.state.startsWith('READ_ONLY')))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('iPhone source enters as its own numbered client admission', () => {
|
||||||
|
const ios = catalog.candidates.find((candidate) => candidate.client_number === 'HLP-IOS-CLIENT-001')
|
||||||
|
assert.equal(ios.candidate_number, 'HLP-DONOR-CAND-0008')
|
||||||
|
assert.equal(ios.desktop_runtime_module_number, 'HLP-MOD-OFFICIAL-MOBILE-SYNC-0001')
|
||||||
|
assert.equal(ios.numbered_ipc_module, 'HLP-NIPC-MOD-0030')
|
||||||
|
assert.match(ios.boundary, /NO SECOND PERSONA SYSTEM/)
|
||||||
|
})
|
||||||
|
|
||||||
test('candidate coordinates are unique but are not permanent runtime module numbers', () => {
|
test('candidate coordinates are unique but are not permanent runtime module numbers', () => {
|
||||||
const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number)
|
const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number)
|
||||||
const admitted = catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED'))
|
const admitted = catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED'))
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue