Prechádzať zdrojové kódy

feat(live-activity): 支持 iOS 实时活动 (Live Activity)、灵动岛录音状态显示与后台音频权限配置

bob.yuxinyang 1 mesiac pred
rodič
commit
5bf347bc7d

+ 163 - 0
CelestiaTrace.xcodeproj/project.pbxproj

@@ -7,6 +7,7 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		01C6B879019C60235B096D80 /* CelestiaTraceLiveActivity.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 626376A6BF69F473261DF616 /* CelestiaTraceLiveActivity.appex */; };
 		10EAADFFB07A1A8721B25D6B /* RemoteNetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 24923AF5D038F81EB3248A4C /* RemoteNetworkService.swift */; };
 		128B1C7150980D1FBE511B79 /* RecordingEnvironmentDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB09EA3EE9CFFADB34B5B43A /* RecordingEnvironmentDetector.swift */; };
 		15EAFA0DBF80C789F02C5119 /* ProfileView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 086899076999CD4909BB9748 /* ProfileView.swift */; };
@@ -20,18 +21,21 @@
 		3A7E453FDCA5DA9F96194B23 /* CelestiaTraceApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 628549E7FC16C58A15C9E1B7 /* CelestiaTraceApp.swift */; };
 		403219D9D8C972EC2AA8F84E /* EditProfileModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA13A08133A50094CBFFDDF9 /* EditProfileModalView.swift */; };
 		44732C9690578EEFC6840D05 /* DeviceListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8CB67D1B2BDECAA772F3935 /* DeviceListView.swift */; };
+		46F4F91D30D4C26A44A4D570 /* RecordingLiveActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1A48C59DD78DE74AF832D246 /* RecordingLiveActivityManager.swift */; };
 		490825791613AC52D06ECFA7 /* CelestiaSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83CE4996BF0CC501E3B7C567 /* CelestiaSession.swift */; };
 		4F4476444CB6838B2BE6EE87 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 71787388F9456A4784534BCC /* ContentView.swift */; };
 		503E109CBBC69E3015AE37A3 /* SilenceDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = AEED3C75367F66D6D6CE745D /* SilenceDetector.swift */; };
 		58EFA65370C61ED9A8EE6627 /* ActiveRecordingView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B7B9693BD9924C08A1F2ECD1 /* ActiveRecordingView.swift */; };
 		70A52B64861B541B4C9423AC /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5353BBE8E9552B4B486D8701 /* Foundation.framework */; };
 		75AE08DD135D81A3E19190EE /* DiscreetWarningBanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4F4B4B59E0916CE9ACFF362F /* DiscreetWarningBanner.swift */; };
+		7892677460CB60EACD115895 /* RecordingLiveActivityWidget.swift in Sources */ = {isa = PBXBuildFile; fileRef = DCFA418F675B567DAF8E8905 /* RecordingLiveActivityWidget.swift */; };
 		7CA015A965BD30FAB3A92507 /* AuthManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 073CAFDB8C45F4EAA33FA0EA /* AuthManager.swift */; };
 		8B5C51BB4474283BDF4ADACB /* LiveWaveformView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1189E47F735DDB40D1E741E6 /* LiveWaveformView.swift */; };
 		8FBEE86CDB9635710EF85F1B /* BLEManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F52FB139E193DBAC41F9D3F6 /* BLEManager.swift */; };
 		936A52ADE1ECF30D3650B8D8 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5353BBE8E9552B4B486D8701 /* Foundation.framework */; };
 		93F943133BCD792D991AA8E3 /* AudioChunk.swift in Sources */ = {isa = PBXBuildFile; fileRef = 320789BA1CB176C4DBC51E99 /* AudioChunk.swift */; };
 		96791550D01A4FD10EEB9BA3 /* RecordingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64180D833523B84195EE7E2A /* RecordingViewModel.swift */; };
+		96D67D2F80255A2EAEE6ECEC /* RecordingActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B11040B6E3800958078529 /* RecordingActivityAttributes.swift */; };
 		9BDA680497BF5DD72A7BA8F3 /* HomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6305E60488EEABAFE2E1D6D8 /* HomeView.swift */; };
 		9C5A0C9CDFCB8CFB10FF0B06 /* DeviceScanView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E7063E6702335B02B65775F4 /* DeviceScanView.swift */; };
 		9E4554980E7FF484BF400458 /* MultiTrackTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1DC9982D9E53E1A77CDB0470 /* MultiTrackTimeline.swift */; };
@@ -44,14 +48,17 @@
 		A9B8C7D6E5F4000000000007 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = A9B8C7D6E5F4000000000008 /* LaunchScreen.storyboard */; };
 		AF09E026C9AB15874D870E1F /* PulsingRecordButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 77D23B83EDDD5489004E42DC /* PulsingRecordButton.swift */; };
 		B262A6AF17A835DB666B2FAB /* SessionListView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBB5FC2AC0B9D31C7A04715 /* SessionListView.swift */; };
+		BCC851C8A3DF4F885F179FCE /* RecordingActivityAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08B11040B6E3800958078529 /* RecordingActivityAttributes.swift */; };
 		BE12EF8734ABF645B2B28BBB /* PlaybackViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E528009D607763F9E68EFBAD /* PlaybackViewModel.swift */; };
 		C2807F9F2794AFA8BF03F104 /* ChangePasswordModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 66BE0E22B2A472E2AFC9CB98 /* ChangePasswordModalView.swift */; };
+		C7FCC7859292F4917426A553 /* CelestiaTraceLiveActivityBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 72CA6784C282FC33FAD436B8 /* CelestiaTraceLiveActivityBundle.swift */; };
 		C8794B9720CD85F518A82473 /* BoundDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EF9EF0F961DB2B546B9C0F1 /* BoundDevice.swift */; };
 		CD45B55AB993D054E50A43BE /* RealAudioRecorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 977EB01CFA491A1573D7CF22 /* RealAudioRecorder.swift */; };
 		DDC0D9C4883B4861CBC74C39 /* ChronoFeedRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9275B988BEBBEA80370E1AC4 /* ChronoFeedRow.swift */; };
 		E78FC729614A4C93C97D564D /* ColorTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95AF20E8EF4319BBEBA608C1 /* ColorTheme.swift */; };
 		F385E2FCB06D39917F51846E /* AudioRecorderProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B407473A307EEAF2CB5EE94 /* AudioRecorderProtocol.swift */; };
 		F3CB51CC262E0F4929EB129B /* RemoteAuthService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4E13AE9CB7E8C9E3BACFC679 /* RemoteAuthService.swift */; };
+		F3E8621D553D266CAE80F6A0 /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5353BBE8E9552B4B486D8701 /* Foundation.framework */; };
 		F5035B942DE1FEC10515D9D6 /* ImagePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADF67536C0CF9AE7F405C74 /* ImagePicker.swift */; };
 		FD30DBB57E19055AB8F0A956 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4883D42730E6133FCC3C81C2 /* SettingsView.swift */; };
 /* End PBXBuildFile section */
@@ -64,13 +71,36 @@
 			remoteGlobalIDString = EC79DCF0EBB2620B0FC21573;
 			remoteInfo = CelestiaTrace;
 		};
+		DABA98985DEBD8FCC304C6F7 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = 979128538DFF0B361F857241 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = F5E2F19ABDFEEE6B93651416;
+			remoteInfo = CelestiaTraceLiveActivity;
+		};
 /* End PBXContainerItemProxy section */
 
+/* Begin PBXCopyFilesBuildPhase section */
+		A95144330D7365DA09C7087E /* Embed Foundation Extensions */ = {
+			isa = PBXCopyFilesBuildPhase;
+			buildActionMask = 2147483647;
+			dstPath = "";
+			dstSubfolderSpec = 13;
+			files = (
+				01C6B879019C60235B096D80 /* CelestiaTraceLiveActivity.appex in Embed Foundation Extensions */,
+			);
+			name = "Embed Foundation Extensions";
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXCopyFilesBuildPhase section */
+
 /* Begin PBXFileReference section */
 		073CAFDB8C45F4EAA33FA0EA /* AuthManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthManager.swift; sourceTree = "<group>"; };
 		086899076999CD4909BB9748 /* ProfileView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ProfileView.swift; sourceTree = "<group>"; };
+		08B11040B6E3800958078529 /* RecordingActivityAttributes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecordingActivityAttributes.swift; sourceTree = "<group>"; };
 		0EBB5FC2AC0B9D31C7A04715 /* SessionListView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = SessionListView.swift; sourceTree = "<group>"; };
 		1189E47F735DDB40D1E741E6 /* LiveWaveformView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LiveWaveformView.swift; sourceTree = "<group>"; };
+		1A48C59DD78DE74AF832D246 /* RecordingLiveActivityManager.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecordingLiveActivityManager.swift; sourceTree = "<group>"; };
 		1DC9982D9E53E1A77CDB0470 /* MultiTrackTimeline.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MultiTrackTimeline.swift; sourceTree = "<group>"; };
 		1EF9EF0F961DB2B546B9C0F1 /* BoundDevice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = BoundDevice.swift; sourceTree = "<group>"; };
 		24923AF5D038F81EB3248A4C /* RemoteNetworkService.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RemoteNetworkService.swift; sourceTree = "<group>"; };
@@ -84,15 +114,18 @@
 		4F4B4B59E0916CE9ACFF362F /* DiscreetWarningBanner.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DiscreetWarningBanner.swift; sourceTree = "<group>"; };
 		51A1BE84E0C74D9F7BE9DC3E /* UserAccount.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = UserAccount.swift; sourceTree = "<group>"; };
 		5353BBE8E9552B4B486D8701 /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS18.0.sdk/System/Library/Frameworks/Foundation.framework; sourceTree = DEVELOPER_DIR; };
+		626376A6BF69F473261DF616 /* CelestiaTraceLiveActivity.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = CelestiaTraceLiveActivity.appex; sourceTree = BUILT_PRODUCTS_DIR; };
 		628549E7FC16C58A15C9E1B7 /* CelestiaTraceApp.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CelestiaTraceApp.swift; sourceTree = "<group>"; };
 		6305E60488EEABAFE2E1D6D8 /* HomeView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = HomeView.swift; sourceTree = "<group>"; };
 		64180D833523B84195EE7E2A /* RecordingViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecordingViewModel.swift; sourceTree = "<group>"; };
 		66BE0E22B2A472E2AFC9CB98 /* ChangePasswordModalView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChangePasswordModalView.swift; sourceTree = "<group>"; };
 		71729F8F4A48D77C1898C27B /* CelestiaTraceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = CelestiaTraceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		71787388F9456A4784534BCC /* ContentView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
+		72CA6784C282FC33FAD436B8 /* CelestiaTraceLiveActivityBundle.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CelestiaTraceLiveActivityBundle.swift; sourceTree = "<group>"; };
 		77D23B83EDDD5489004E42DC /* PulsingRecordButton.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PulsingRecordButton.swift; sourceTree = "<group>"; };
 		7F2D8AEE2E58FE0F2EF531D0 /* NetworkServiceProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = NetworkServiceProtocol.swift; sourceTree = "<group>"; };
 		83CE4996BF0CC501E3B7C567 /* CelestiaSession.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CelestiaSession.swift; sourceTree = "<group>"; };
+		899B3C2E923AC5479D0AE32A /* Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		8CDFF4B7FD8488111C99DADF /* AuthServiceProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthServiceProtocol.swift; sourceTree = "<group>"; };
 		9275B988BEBBEA80370E1AC4 /* ChronoFeedRow.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ChronoFeedRow.swift; sourceTree = "<group>"; };
 		95AF20E8EF4319BBEBA608C1 /* ColorTheme.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ColorTheme.swift; sourceTree = "<group>"; };
@@ -110,6 +143,7 @@
 		DADF67536C0CF9AE7F405C74 /* ImagePicker.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ImagePicker.swift; sourceTree = "<group>"; };
 		DB09EA3EE9CFFADB34B5B43A /* RecordingEnvironmentDetector.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecordingEnvironmentDetector.swift; sourceTree = "<group>"; };
 		DB56F0DFCB1FE77D7636B43D /* CelestiaTrace.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = CelestiaTrace.app; sourceTree = BUILT_PRODUCTS_DIR; };
+		DCFA418F675B567DAF8E8905 /* RecordingLiveActivityWidget.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = RecordingLiveActivityWidget.swift; sourceTree = "<group>"; };
 		E10ED811D3048B961C4754A6 /* AuthModalView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = AuthModalView.swift; sourceTree = "<group>"; };
 		E528009D607763F9E68EFBAD /* PlaybackViewModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PlaybackViewModel.swift; sourceTree = "<group>"; };
 		E7063E6702335B02B65775F4 /* DeviceScanView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = DeviceScanView.swift; sourceTree = "<group>"; };
@@ -135,6 +169,14 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		8F2F64E3A21692B84286B36E /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				F3E8621D553D266CAE80F6A0 /* Foundation.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
@@ -153,6 +195,7 @@
 			children = (
 				DB56F0DFCB1FE77D7636B43D /* CelestiaTrace.app */,
 				71729F8F4A48D77C1898C27B /* CelestiaTraceTests.xctest */,
+				626376A6BF69F473261DF616 /* CelestiaTraceLiveActivity.appex */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -196,6 +239,15 @@
 			path = Profile;
 			sourceTree = "<group>";
 		};
+		55A6E794C53AADF4C0BD6748 /* LiveActivity */ = {
+			isa = PBXGroup;
+			children = (
+				08B11040B6E3800958078529 /* RecordingActivityAttributes.swift */,
+			);
+			name = LiveActivity;
+			path = LiveActivity;
+			sourceTree = "<group>";
+		};
 		565448EB878982B445E267AF /* Frameworks */ = {
 			isa = PBXGroup;
 			children = (
@@ -224,6 +276,17 @@
 			path = Auth;
 			sourceTree = "<group>";
 		};
+		76DF21F5990CBBC48E108438 /* CelestiaTraceLiveActivity */ = {
+			isa = PBXGroup;
+			children = (
+				72CA6784C282FC33FAD436B8 /* CelestiaTraceLiveActivityBundle.swift */,
+				DCFA418F675B567DAF8E8905 /* RecordingLiveActivityWidget.swift */,
+				899B3C2E923AC5479D0AE32A /* Info.plist */,
+			);
+			name = CelestiaTraceLiveActivity;
+			path = CelestiaTraceLiveActivity;
+			sourceTree = "<group>";
+		};
 		7B90B8D292E18F60A02CDFE2 /* Views */ = {
 			isa = PBXGroup;
 			children = (
@@ -301,6 +364,7 @@
 				DB59940538488B1E753621A8 /* Network */,
 				FD4D76A3358AAF1519C279F1 /* Audio */,
 				AF823CBC596A095A54F096C1 /* Bluetooth */,
+				1A48C59DD78DE74AF832D246 /* RecordingLiveActivityManager.swift */,
 			);
 			path = Services;
 			sourceTree = "<group>";
@@ -314,6 +378,7 @@
 				2626EC2897F60B86D833D875 /* Models */,
 				7B90B8D292E18F60A02CDFE2 /* Views */,
 				B33580BE3AF0DC9085B1D3A1 /* Services */,
+				55A6E794C53AADF4C0BD6748 /* LiveActivity */,
 			);
 			path = CelestiaTrace;
 			sourceTree = "<group>";
@@ -351,6 +416,7 @@
 				565448EB878982B445E267AF /* Frameworks */,
 				B5CB52A86415BBB0B9DCE932 /* CelestiaTrace */,
 				32200F0540C3761CA9D1495A /* CelestiaTraceTests */,
+				76DF21F5990CBBC48E108438 /* CelestiaTraceLiveActivity */,
 			);
 			sourceTree = "<group>";
 		};
@@ -422,16 +488,35 @@
 				9E60C6991F09AB5E80EF4283 /* Sources */,
 				6FF5C5F71B01A15E70EBC20C /* Frameworks */,
 				2B844E255303FD43D7D16A02 /* Resources */,
+				A95144330D7365DA09C7087E /* Embed Foundation Extensions */,
 			);
 			buildRules = (
 			);
 			dependencies = (
+				FAC05647B7B91065690E0EAB /* PBXTargetDependency */,
 			);
 			name = CelestiaTrace;
 			productName = CelestiaTrace;
 			productReference = DB56F0DFCB1FE77D7636B43D /* CelestiaTrace.app */;
 			productType = "com.apple.product-type.application";
 		};
+		F5E2F19ABDFEEE6B93651416 /* CelestiaTraceLiveActivity */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = 45022A0FA83D32670D30B200 /* Build configuration list for PBXNativeTarget "CelestiaTraceLiveActivity" */;
+			buildPhases = (
+				F73E5C57E12A8709FF074E12 /* Sources */,
+				8F2F64E3A21692B84286B36E /* Frameworks */,
+				469C396BB9D89A7898530847 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+			);
+			name = CelestiaTraceLiveActivity;
+			productName = CelestiaTraceLiveActivity;
+			productReference = 626376A6BF69F473261DF616 /* CelestiaTraceLiveActivity.appex */;
+			productType = "com.apple.product-type.app-extension";
+		};
 /* End PBXNativeTarget section */
 
 /* Begin PBXProject section */
@@ -462,6 +547,7 @@
 			targets = (
 				EC79DCF0EBB2620B0FC21573 /* CelestiaTrace */,
 				D5681575EDB1ABEE4093C509 /* CelestiaTraceTests */,
+				F5E2F19ABDFEEE6B93651416 /* CelestiaTraceLiveActivity */,
 			);
 		};
 /* End PBXProject section */
@@ -476,6 +562,13 @@
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
+		469C396BB9D89A7898530847 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		D875758EF038ED00E0A14D96 /* Resources */ = {
 			isa = PBXResourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -540,6 +633,18 @@
 				F385E2FCB06D39917F51846E /* AudioRecorderProtocol.swift in Sources */,
 				A1B2C3D4E5F6012345678901 /* AudioPathHelper.swift in Sources */,
 				8FBEE86CDB9635710EF85F1B /* BLEManager.swift in Sources */,
+				46F4F91D30D4C26A44A4D570 /* RecordingLiveActivityManager.swift in Sources */,
+				BCC851C8A3DF4F885F179FCE /* RecordingActivityAttributes.swift in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+		F73E5C57E12A8709FF074E12 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				C7FCC7859292F4917426A553 /* CelestiaTraceLiveActivityBundle.swift in Sources */,
+				7892677460CB60EACD115895 /* RecordingLiveActivityWidget.swift in Sources */,
+				96D67D2F80255A2EAEE6ECEC /* RecordingActivityAttributes.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -552,9 +657,37 @@
 			target = EC79DCF0EBB2620B0FC21573 /* CelestiaTrace */;
 			targetProxy = 55C3FF8383E750C9C9453EC5 /* PBXContainerItemProxy */;
 		};
+		FAC05647B7B91065690E0EAB /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			name = CelestiaTraceLiveActivity;
+			target = F5E2F19ABDFEEE6B93651416 /* CelestiaTraceLiveActivity */;
+			targetProxy = DABA98985DEBD8FCC304C6F7 /* PBXContainerItemProxy */;
+		};
 /* End PBXTargetDependency section */
 
 /* Begin XCBuildConfiguration section */
+		02AF74A06F9C06B4F4B287E7 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				CLANG_ENABLE_OBJC_WEAK = NO;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = 5ERVQEJ497;
+				GENERATE_INFOPLIST_FILE = NO;
+				INFOPLIST_FILE = CelestiaTraceLiveActivity/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../../Frameworks";
+				MARKETING_VERSION = 1.0.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.celestia.trace.live-activity";
+				SDKROOT = iphoneos;
+				SKIP_INSTALL = YES;
+				SWIFT_VERSION = 5.9;
+				TARGETED_DEVICE_FAMILY = 1;
+				VALIDATE_PRODUCT = YES;
+			};
+			name = Release;
+		};
 		76F4E0F0A78EC9D84B954B50 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -616,6 +749,27 @@
 			};
 			name = Debug;
 		};
+		84826E351EF861B4360FFC19 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				APPLICATION_EXTENSION_API_ONLY = YES;
+				CLANG_ENABLE_OBJC_WEAK = NO;
+				CODE_SIGN_STYLE = Automatic;
+				CURRENT_PROJECT_VERSION = 1;
+				DEVELOPMENT_TEAM = 5ERVQEJ497;
+				GENERATE_INFOPLIST_FILE = NO;
+				INFOPLIST_FILE = CelestiaTraceLiveActivity/Info.plist;
+				IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+				LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../../Frameworks";
+				MARKETING_VERSION = 1.0.0;
+				PRODUCT_BUNDLE_IDENTIFIER = "com.celestia.trace.live-activity";
+				SDKROOT = iphoneos;
+				SKIP_INSTALL = YES;
+				SWIFT_VERSION = 5.9;
+				TARGETED_DEVICE_FAMILY = 1;
+			};
+			name = Debug;
+		};
 		95AB591F506D02959044D749 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
@@ -775,6 +929,15 @@
 			defaultConfigurationIsVisible = 0;
 			defaultConfigurationName = Release;
 		};
+		45022A0FA83D32670D30B200 /* Build configuration list for PBXNativeTarget "CelestiaTraceLiveActivity" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				02AF74A06F9C06B4F4B287E7 /* Release */,
+				84826E351EF861B4360FFC19 /* Debug */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		902A4F79574BE80E9469463E /* Build configuration list for PBXNativeTarget "CelestiaTrace" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (

+ 91 - 0
CelestiaTrace.xcodeproj/xcshareddata/xcschemes/CelestiaTrace App.xcscheme

@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "2650"
+   version = "1.7">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "EC79DCF0EBB2620B0FC21573"
+               BuildableName = "CelestiaTrace.app"
+               BlueprintName = "CelestiaTrace"
+               ReferencedContainer = "container:CelestiaTrace.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+      <Testables>
+         <TestableReference
+            skipped = "NO"
+            parallelizable = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "D5681575EDB1ABEE4093C509"
+               BuildableName = "CelestiaTraceTests.xctest"
+               BlueprintName = "CelestiaTraceTests"
+               ReferencedContainer = "container:CelestiaTrace.xcodeproj">
+            </BuildableReference>
+         </TestableReference>
+      </Testables>
+   </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 = "EC79DCF0EBB2620B0FC21573"
+            BuildableName = "CelestiaTrace.app"
+            BlueprintName = "CelestiaTrace"
+            ReferencedContainer = "container:CelestiaTrace.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "EC79DCF0EBB2620B0FC21573"
+            BuildableName = "CelestiaTrace.app"
+            BlueprintName = "CelestiaTrace"
+            ReferencedContainer = "container:CelestiaTrace.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 125 - 0
CelestiaTrace.xcodeproj/xcshareddata/xcschemes/CelestiaTraceLiveActivity.xcscheme

@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "2660"
+   wasCreatedForAppExtension = "YES"
+   version = "2.0">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES"
+      buildArchitectures = "Automatic">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "F5E2F19ABDFEEE6B93651416"
+               BuildableName = "CelestiaTraceLiveActivity.appex"
+               BlueprintName = "CelestiaTraceLiveActivity"
+               ReferencedContainer = "container:CelestiaTrace.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "EC79DCF0EBB2620B0FC21573"
+               BuildableName = "CelestiaTrace.app"
+               BlueprintName = "CelestiaTrace"
+               ReferencedContainer = "container:CelestiaTrace.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      shouldAutocreateTestPlan = "YES">
+   </TestAction>
+   <LaunchAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = ""
+      selectedLauncherIdentifier = "Xcode.IDEFoundation.Launcher.PosixSpawn"
+      launchStyle = "0"
+      askForAppToLaunch = "Yes"
+      useCustomWorkingDirectory = "NO"
+      ignoresPersistentStateOnLaunch = "NO"
+      debugDocumentVersioning = "YES"
+      debugServiceExtension = "internal"
+      allowLocationSimulation = "YES"
+      launchAutomaticallySubstyle = "2"
+      queueDebuggingEnableBacktraceRecording = "Yes">
+      <RemoteRunnable
+         runnableDebuggingMode = "2"
+         BundleIdentifier = "com.apple.springboard">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "F5E2F19ABDFEEE6B93651416"
+            BuildableName = "CelestiaTraceLiveActivity.appex"
+            BlueprintName = "CelestiaTraceLiveActivity"
+            ReferencedContainer = "container:CelestiaTrace.xcodeproj">
+         </BuildableReference>
+      </RemoteRunnable>
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "EC79DCF0EBB2620B0FC21573"
+            BuildableName = "CelestiaTrace.app"
+            BlueprintName = "CelestiaTrace"
+            ReferencedContainer = "container:CelestiaTrace.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+      <EnvironmentVariables>
+         <EnvironmentVariable
+            key = "_XCWidgetKind"
+            value = ""
+            isEnabled = "YES">
+         </EnvironmentVariable>
+         <EnvironmentVariable
+            key = "_XCWidgetDefaultView"
+            value = "timeline"
+            isEnabled = "YES">
+         </EnvironmentVariable>
+         <EnvironmentVariable
+            key = "_XCWidgetFamily"
+            value = "systemMedium"
+            isEnabled = "YES">
+         </EnvironmentVariable>
+      </EnvironmentVariables>
+   </LaunchAction>
+   <ProfileAction
+      buildConfiguration = "Release"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      savedToolIdentifier = ""
+      useCustomWorkingDirectory = "NO"
+      debugDocumentVersioning = "YES"
+      askForAppToLaunch = "Yes"
+      launchAutomaticallySubstyle = "2">
+      <BuildableProductRunnable
+         runnableDebuggingMode = "0">
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "EC79DCF0EBB2620B0FC21573"
+            BuildableName = "CelestiaTrace.app"
+            BlueprintName = "CelestiaTrace"
+            ReferencedContainer = "container:CelestiaTrace.xcodeproj">
+         </BuildableReference>
+      </BuildableProductRunnable>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 14 - 0
CelestiaTrace/LiveActivity/RecordingActivityAttributes.swift

@@ -0,0 +1,14 @@
+import ActivityKit
+import Foundation
+
+struct RecordingActivityAttributes: ActivityAttributes {
+    struct ContentState: Codable, Hashable {
+        var timerStartDate: Date
+        var elapsedSeconds: TimeInterval
+        var isPaused: Bool
+    }
+
+    var sessionID: UUID
+    var title: String
+    var sourceName: String
+}

+ 14 - 0
CelestiaTrace/Resources/Info.plist

@@ -24,6 +24,8 @@
 	<string>$(CURRENT_PROJECT_VERSION)</string>
 	<key>LSRequiresIPhoneOS</key>
 	<true/>
+	<key>NSSupportsLiveActivities</key>
+	<true/>
 	<key>NSBluetoothAlwaysUsageDescription</key>
 	<string>需要蓝牙权限以搜索、绑定并使用微光(Spark)录音设备</string>
 	<key>NSBluetoothPeripheralUsageDescription</key>
@@ -36,8 +38,20 @@
 	<string>需要相册权限以选择照片记录</string>
 	<key>UIBackgroundModes</key>
 	<array>
+		<string>audio</string>
 		<string>bluetooth-central</string>
 	</array>
+	<key>CFBundleURLTypes</key>
+	<array>
+		<dict>
+			<key>CFBundleTypeRole</key>
+			<string>Editor</string>
+			<key>CFBundleURLSchemes</key>
+			<array>
+				<string>celestiatrace</string>
+			</array>
+		</dict>
+	</array>
 	<key>UILaunchStoryboardName</key>
 	<string>LaunchScreen</string>
 	<key>UISupportedInterfaceOrientations</key>

+ 15 - 8
CelestiaTrace/Services/Audio/RealAudioRecorder.swift

@@ -78,15 +78,15 @@ final class RealAudioRecorder: AudioRecorderProtocol {
             let recorder = try AVAudioRecorder(url: fileURL, settings: settings)
             recorder.isMeteringEnabled = true
             
-            if recorder.prepareToRecord() {
-                recorder.record()
+            if recorder.prepareToRecord(), recorder.record() {
                 self.audioRecorder = recorder
                 self.isRecording = true
                 self.isPaused = false
                 self.startTimer()
                 print("[RealAudioRecorder] Recording started: \(fileURL.lastPathComponent)")
             } else {
-                print("[RealAudioRecorder] Prepare to record failed")
+                try? session.setActive(false, options: .notifyOthersOnDeactivation)
+                print("[RealAudioRecorder] Failed to prepare or start recording")
             }
         } catch {
             print("[RealAudioRecorder] Setup failed: \(error.localizedDescription)")
@@ -104,7 +104,7 @@ final class RealAudioRecorder: AudioRecorderProtocol {
         currentAmplitude = 0
         
         // Deactivate audio session
-        try? AVAudioSession.sharedInstance().setActive(false)
+        try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
         print("[RealAudioRecorder] Recording stopped. File saved.")
     }
     
@@ -118,10 +118,17 @@ final class RealAudioRecorder: AudioRecorderProtocol {
     
     func resumeRecording() {
         guard isRecording else { return }
-        try? AVAudioSession.sharedInstance().setActive(true)
-        audioRecorder?.record()
-        isPaused = false
-        startTimer()
+        do {
+            try AVAudioSession.sharedInstance().setActive(true)
+            guard audioRecorder?.record() == true else {
+                print("[RealAudioRecorder] Failed to resume recording")
+                return
+            }
+            isPaused = false
+            startTimer()
+        } catch {
+            print("[RealAudioRecorder] Failed to reactivate audio session: \(error.localizedDescription)")
+        }
     }
     
     // MARK: - Private Helpers

+ 80 - 0
CelestiaTrace/Services/RecordingLiveActivityManager.swift

@@ -0,0 +1,80 @@
+import ActivityKit
+import Foundation
+
+@MainActor
+final class RecordingLiveActivityManager {
+    static let shared = RecordingLiveActivityManager()
+
+    private var activity: Activity<RecordingActivityAttributes>?
+
+    private init() {}
+
+    func start(
+        sessionID: UUID,
+        title: String,
+        sourceName: String,
+        elapsedSeconds: TimeInterval
+    ) {
+        guard ActivityAuthorizationInfo().areActivitiesEnabled else { return }
+
+        Task {
+            await endExistingActivities()
+
+            let attributes = RecordingActivityAttributes(
+                sessionID: sessionID,
+                title: title,
+                sourceName: sourceName
+            )
+            let state = makeState(elapsedSeconds: elapsedSeconds, isPaused: false)
+
+            do {
+                activity = try Activity.request(
+                    attributes: attributes,
+                    content: ActivityContent(state: state, staleDate: nil),
+                    pushType: nil
+                )
+            } catch {
+                print("[RecordingLiveActivity] Failed to start: \(error.localizedDescription)")
+            }
+        }
+    }
+
+    func update(elapsedSeconds: TimeInterval, isPaused: Bool) {
+        guard let activity else { return }
+        let state = makeState(elapsedSeconds: elapsedSeconds, isPaused: isPaused)
+
+        Task {
+            await activity.update(ActivityContent(state: state, staleDate: nil))
+        }
+    }
+
+    func end(elapsedSeconds: TimeInterval) {
+        guard let activity else { return }
+        self.activity = nil
+        let finalState = makeState(elapsedSeconds: elapsedSeconds, isPaused: true)
+
+        Task {
+            await activity.end(
+                ActivityContent(state: finalState, staleDate: nil),
+                dismissalPolicy: .immediate
+            )
+        }
+    }
+
+    private func endExistingActivities() async {
+        for existingActivity in Activity<RecordingActivityAttributes>.activities {
+            await existingActivity.end(nil, dismissalPolicy: .immediate)
+        }
+    }
+
+    private func makeState(
+        elapsedSeconds: TimeInterval,
+        isPaused: Bool
+    ) -> RecordingActivityAttributes.ContentState {
+        RecordingActivityAttributes.ContentState(
+            timerStartDate: Date().addingTimeInterval(-elapsedSeconds),
+            elapsedSeconds: elapsedSeconds,
+            isPaused: isPaused
+        )
+    }
+}

+ 0 - 1
CelestiaTrace/ViewModels/PlaybackViewModel.swift

@@ -275,7 +275,6 @@ final class PlaybackViewModel: NSObject, AVAudioPlayerDelegate {
             if currentTimeSec >= range.start && currentTimeSec < range.end {
                 let targetMs = range.end * 1000.0
                 seekTo(timeMs: targetMs)
-                print("[PlaybackViewModel] Auto-skipped silence from \(range.start)s to \(range.end)s")
                 break
             }
         }

+ 45 - 0
CelestiaTrace/Views/Recording/ActiveRecordingView.swift

@@ -24,6 +24,7 @@ struct ActiveRecordingView: View {
     @State private var stopErrorMessage: String?
     @State private var recordName: String
     @State private var showRecordNameEditor = false
+    @State private var hasStartedLiveActivity = false
     @FocusState private var isRecordNameFocused: Bool
 
     init(
@@ -107,12 +108,36 @@ struct ActiveRecordingView: View {
             UIApplication.shared.isIdleTimerDisabled = true
             recordingVM.startRecording(initialDuration: initialDuration)
         }
+        .onChange(of: recordingVM.isRecording) { _, isRecording in
+            guard isRecording, !hasStartedLiveActivity else { return }
+            hasStartedLiveActivity = true
+            RecordingLiveActivityManager.shared.start(
+                sessionID: session.id,
+                title: session.title,
+                sourceName: recordingVM.sourceDisplayName,
+                elapsedSeconds: recordingVM.elapsedTime
+            )
+        }
+        .onChange(of: recordingVM.isPaused) { _, isPaused in
+            guard hasStartedLiveActivity else { return }
+            RecordingLiveActivityManager.shared.update(
+                elapsedSeconds: recordingVM.elapsedTime,
+                isPaused: isPaused
+            )
+        }
+        .onOpenURL { url in
+            handleRecordingURL(url)
+        }
         .onDisappear {
             UIApplication.shared.isIdleTimerDisabled = false
             commitRecordName()
             if recordingVM.isRecording {
                 recordingVM.stopRecording()
             }
+            if hasStartedLiveActivity {
+                RecordingLiveActivityManager.shared.end(elapsedSeconds: recordingVM.elapsedTime)
+                hasStartedLiveActivity = false
+            }
         }
         .alert("无法结束设备录音", isPresented: Binding(
             get: { stopErrorMessage != nil },
@@ -567,6 +592,8 @@ struct ActiveRecordingView: View {
         let existingPath = session.localAudioPath
         let existingURL = AudioPathHelper.resolveURL(for: existingPath)
         let totalRecordedSeconds = recordingVM.elapsedTime
+        RecordingLiveActivityManager.shared.end(elapsedSeconds: totalRecordedSeconds)
+        hasStartedLiveActivity = false
         
         if let newRecordedURL = newRecordedURL {
             Task { @MainActor in
@@ -647,6 +674,24 @@ struct ActiveRecordingView: View {
         isRecordNameFocused = false
         showRecordNameEditor = false
     }
+
+    private func handleRecordingURL(_ url: URL) {
+        guard url.scheme == "celestiatrace",
+              url.host == "recording" else { return }
+
+        let components = url.pathComponents.filter { $0 != "/" }
+        guard let sessionID = components.first,
+              sessionID.caseInsensitiveCompare(session.id.uuidString) == .orderedSame else { return }
+
+        switch components.dropFirst().first {
+        case "photo":
+            capturePhoto()
+        case "note":
+            showNoteSheet = true
+        default:
+            break
+        }
+    }
 }
 
 // MARK: - Amplitude Level Meter View

+ 9 - 0
CelestiaTraceLiveActivity/CelestiaTraceLiveActivityBundle.swift

@@ -0,0 +1,9 @@
+import SwiftUI
+import WidgetKit
+
+@main
+struct CelestiaTraceLiveActivityBundle: WidgetBundle {
+    var body: some Widget {
+        RecordingLiveActivityWidget()
+    }
+}

+ 29 - 0
CelestiaTraceLiveActivity/Info.plist

@@ -0,0 +1,29 @@
+<?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>星痕实时记录</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>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>$(MARKETING_VERSION)</string>
+	<key>CFBundleVersion</key>
+	<string>$(CURRENT_PROJECT_VERSION)</string>
+	<key>NSExtension</key>
+	<dict>
+		<key>NSExtensionPointIdentifier</key>
+		<string>com.apple.widgetkit-extension</string>
+	</dict>
+</dict>
+</plist>

+ 157 - 0
CelestiaTraceLiveActivity/RecordingLiveActivityWidget.swift

@@ -0,0 +1,157 @@
+import ActivityKit
+import SwiftUI
+import WidgetKit
+
+struct RecordingLiveActivityWidget: Widget {
+    var body: some WidgetConfiguration {
+        ActivityConfiguration(for: RecordingActivityAttributes.self) { context in
+            lockScreenView(context)
+                .activityBackgroundTint(Color.black.opacity(0.92))
+                .activitySystemActionForegroundColor(.white)
+                .widgetURL(recordingURL(context.attributes))
+        } dynamicIsland: { context in
+            DynamicIsland {
+                DynamicIslandExpandedRegion(.leading) {
+                    Label("正在记录", systemImage: "record.circle.fill")
+                        .font(.caption.weight(.semibold))
+                        .foregroundStyle(.red)
+                }
+
+                DynamicIslandExpandedRegion(.trailing) {
+                    elapsedTime(context.state)
+                        .font(.caption.monospacedDigit())
+                        .foregroundStyle(.white)
+                }
+
+                DynamicIslandExpandedRegion(.bottom) {
+                    HStack {
+                        Text(context.attributes.title)
+                            .font(.caption)
+                            .lineLimit(1)
+
+                        Spacer()
+
+                        quickAction(
+                            title: "拍照",
+                            systemImage: "camera.fill",
+                            url: actionURL("photo", context.attributes)
+                        )
+                        quickAction(
+                            title: "文字",
+                            systemImage: "note.text",
+                            url: actionURL("note", context.attributes)
+                        )
+                    }
+                    .foregroundStyle(.white)
+                }
+            } compactLeading: {
+                Image(systemName: "record.circle.fill")
+                    .foregroundStyle(.red)
+            } compactTrailing: {
+                elapsedTime(context.state)
+                    .font(.caption2.monospacedDigit())
+                    .frame(maxWidth: 48)
+            } minimal: {
+                Image(systemName: "record.circle.fill")
+                    .foregroundStyle(.red)
+            }
+            .widgetURL(recordingURL(context.attributes))
+            .keylineTint(.red)
+        }
+    }
+
+    private func lockScreenView(
+        _ context: ActivityViewContext<RecordingActivityAttributes>
+    ) -> some View {
+        VStack(alignment: .leading, spacing: 12) {
+            HStack(spacing: 10) {
+                Image(systemName: context.state.isPaused ? "pause.circle.fill" : "record.circle.fill")
+                    .font(.title2)
+                    .foregroundStyle(context.state.isPaused ? .orange : .red)
+
+                VStack(alignment: .leading, spacing: 2) {
+                    Text(context.attributes.title)
+                        .font(.headline)
+                        .foregroundStyle(.white)
+                        .lineLimit(1)
+                    Text(context.state.isPaused ? "记录已暂停" : "正在记录 · \(context.attributes.sourceName)")
+                        .font(.caption)
+                        .foregroundStyle(.white.opacity(0.65))
+                        .lineLimit(1)
+                }
+
+                Spacer()
+
+                elapsedTime(context.state)
+                    .font(.title3.monospacedDigit().weight(.medium))
+                    .foregroundStyle(.white)
+            }
+
+            HStack(spacing: 10) {
+                quickAction(
+                    title: "拍照记录",
+                    systemImage: "camera.fill",
+                    url: actionURL("photo", context.attributes)
+                )
+                quickAction(
+                    title: "文字记录",
+                    systemImage: "note.text",
+                    url: actionURL("note", context.attributes)
+                )
+            }
+        }
+        .padding(16)
+    }
+
+    @ViewBuilder
+    private func elapsedTime(
+        _ state: RecordingActivityAttributes.ContentState
+    ) -> some View {
+        if state.isPaused {
+            Text(formattedDuration(state.elapsedSeconds))
+        } else {
+            Text(
+                timerInterval: state.timerStartDate...Date.distantFuture,
+                pauseTime: nil,
+                countsDown: false,
+                showsHours: true
+            )
+        }
+    }
+
+    private func quickAction(
+        title: String,
+        systemImage: String,
+        url: URL
+    ) -> some View {
+        Link(destination: url) {
+            Label(title, systemImage: systemImage)
+                .font(.caption.weight(.semibold))
+                .foregroundStyle(.white)
+                .frame(maxWidth: .infinity)
+                .padding(.vertical, 8)
+                .background(.white.opacity(0.12), in: RoundedRectangle(cornerRadius: 8))
+        }
+    }
+
+    private func recordingURL(_ attributes: RecordingActivityAttributes) -> URL {
+        URL(string: "celestiatrace://recording/\(attributes.sessionID.uuidString)")!
+    }
+
+    private func actionURL(
+        _ action: String,
+        _ attributes: RecordingActivityAttributes
+    ) -> URL {
+        URL(string: "celestiatrace://recording/\(attributes.sessionID.uuidString)/\(action)")!
+    }
+
+    private func formattedDuration(_ seconds: TimeInterval) -> String {
+        let totalSeconds = max(0, Int(seconds))
+        return String(
+            format: "%02d:%02d:%02d",
+            totalSeconds / 3600,
+            (totalSeconds % 3600) / 60,
+            totalSeconds % 60
+        )
+    }
+}

+ 46 - 0
generate_project.rb

@@ -52,6 +52,8 @@ app_target.build_configurations.each do |config|
   config.build_settings['INFOPLIST_KEY_NSPhotoLibraryUsageDescription'] = '需要相册权限以选择照片记录'
   config.build_settings['INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription'] = '需要蓝牙权限以扫描和绑定 BLE 录音设备'
   config.build_settings['INFOPLIST_KEY_NSBluetoothPeripheralUsageDescription'] = '需要蓝牙权限以与录音设备传输数据'
+  # Keep active microphone recordings running while the app is backgrounded or the device is locked.
+  config.build_settings['INFOPLIST_KEY_UIBackgroundModes'] = 'audio bluetooth-central'
   config.build_settings['MARKETING_VERSION'] = '1.0.0'
   config.build_settings['CURRENT_PROJECT_VERSION'] = '1'
   config.build_settings['ASSETCATALOG_COMPILER_APPICON_NAME'] = 'AppIcon'
@@ -60,6 +62,45 @@ app_target.build_configurations.each do |config|
   config.build_settings['DEVELOPMENT_ASSET_PATHS'] = '""'
 end
 
+# Create the Live Activity widget extension used on the Lock Screen and Dynamic Island.
+live_activity_target = project.new_target(
+  :app_extension,
+  'CelestiaTraceLiveActivity',
+  :ios,
+  '17.0'
+)
+live_activity_target.build_configurations.each do |config|
+  config.build_settings['SWIFT_VERSION'] = '5.9'
+  config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '17.0'
+  config.build_settings['TARGETED_DEVICE_FAMILY'] = '1'
+  config.build_settings['SDKROOT'] = 'iphoneos'
+  config.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.celestia.trace.live-activity'
+  config.build_settings['GENERATE_INFOPLIST_FILE'] = 'NO'
+  config.build_settings['INFOPLIST_FILE'] = 'CelestiaTraceLiveActivity/Info.plist'
+  config.build_settings['MARKETING_VERSION'] = '1.0.0'
+  config.build_settings['CURRENT_PROJECT_VERSION'] = '1'
+  config.build_settings['CODE_SIGN_STYLE'] = 'Automatic'
+  config.build_settings['APPLICATION_EXTENSION_API_ONLY'] = 'YES'
+  config.build_settings['SKIP_INSTALL'] = 'YES'
+  config.build_settings['LD_RUNPATH_SEARCH_PATHS'] = '$(inherited) @executable_path/../../Frameworks'
+end
+
+live_activity_group = main_group.new_group(
+  'CelestiaTraceLiveActivity',
+  'CelestiaTraceLiveActivity'
+)
+live_activity_sources = %w[
+  CelestiaTraceLiveActivityBundle.swift
+  RecordingLiveActivityWidget.swift
+].map { |filename| live_activity_group.new_file(filename) }
+live_activity_target.add_file_references(live_activity_sources)
+live_activity_group.new_file('Info.plist')
+
+app_target.add_dependency(live_activity_target)
+embed_extensions_phase = app_target.new_copy_files_build_phase('Embed Foundation Extensions')
+embed_extensions_phase.dst_subfolder_spec = '13'
+embed_extensions_phase.add_file_reference(live_activity_target.product_reference, true)
+
 # Create iOS Test target
 test_target = project.new_target(:unit_test_bundle, 'CelestiaTraceTests', :ios, '17.0')
 test_target.build_configurations.each do |config|
@@ -79,6 +120,11 @@ test_target.add_dependency(app_target)
 add_files_recursively('CelestiaTrace', app_group, app_target, project)
 add_files_recursively('Tests/CelestiaTraceTests', tests_group, test_target, project)
 
+shared_activity_ref = project.files.find do |file|
+  file.path == 'RecordingActivityAttributes.swift'
+end
+live_activity_target.add_file_references([shared_activity_ref]) if shared_activity_ref
+
 # Add the asset catalog (AppIcon and launch-screen color) to the app target.
 assets_ref = app_group.new_file('Resources/Assets.xcassets')
 app_target.resources_build_phase.add_file_reference(assets_ref)