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": {
|
||||
"desktop_bridge": "IN_THIS_ADMISSION",
|
||||
"ios_application_source": "NOT_IN_DECLARED_DONOR_CANDIDATE",
|
||||
"ios_installable_package": "PENDING_SEPARATE_ADMISSION",
|
||||
"ios_application_source": "SEPARATELY_ADMITTED_AT_MOBILE_IOS_WITH_HLP_IOS_CLIENT_001",
|
||||
"ios_installable_package": "DEVELOPMENT_SIGNED_DEBUGGING_IPA_EXPORTED",
|
||||
"claim_real_iphone_end_to_end_accepted": false
|
||||
},
|
||||
"current_acceptance": {
|
||||
"state": "PASS_DESKTOP_BRIDGE_ONLY",
|
||||
"state": "PASS_DESKTOP_BRIDGE_AND_SEPARATELY_ADMITTED_IOS_CLIENT_ARTIFACT",
|
||||
"module_package_sha256": "61a9d402c936d7477609bcfa6eb4ad6e83cb6c089ba51b3db09a2d6259185924",
|
||||
"module_package_signature": "PASS_EMBEDDED_PRODUCT_TRUST",
|
||||
"signed_app_binary_sha256": "9be06d6c5dbd4f46f6f925142d633f12b42ee553873c2342f9804bcf39dde7b4",
|
||||
|
|
@ -84,7 +84,10 @@
|
|||
"desktop_listener": "PASS_EXPLICIT_START_REACHABLE_STATUS_AND_EXPLICIT_STOP",
|
||||
"restart_restore": "PASS_ACTIVE_MODULE_RESTORED_LISTENER_OFFLINE",
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"schema": "hololake.module-donor-admission-registry/v1",
|
||||
"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": {
|
||||
"official_base": "HOLOLAKE_0.5.0_NUMBERED_IPC_ROOT",
|
||||
"repair_old_application_in_place": false,
|
||||
|
|
@ -160,7 +160,7 @@
|
|||
"runtime_module_number": "HLP-MOD-OFFICIAL-MOBILE-SYNC-0001",
|
||||
"numbered_ipc_module": "HLP-NIPC-MOD-0030",
|
||||
"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,
|
||||
|
|
@ -180,6 +180,23 @@
|
|||
"numbered_ipc_module": "HLP-NIPC-MOD-0031",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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": [
|
||||
|
|
|
|||
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'))
|
||||
|
||||
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.bulk_merge_or_wholesale_copy_allowed, false)
|
||||
assert.equal(catalog.root_rule.one_candidate_per_admission_cycle, true)
|
||||
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', () => {
|
||||
const coordinates = catalog.candidates.map((candidate) => candidate.candidate_number)
|
||||
const admitted = catalog.candidates.filter((candidate) => candidate.state.startsWith('ADMITTED'))
|
||||
|
|
|
|||
Loading…
Reference in a new issue