cleanup: remove unused and unnecessary code

This commit is contained in:
ajaysehwal
2026-06-04 10:52:43 -04:00
committed by Evan Kaloudis
parent b142cbe98c
commit af60a4be72
12 changed files with 8 additions and 330 deletions
@@ -1,20 +0,0 @@
{
"colors" : [
{
"color" : {
"color-space" : "srgb",
"components" : {
"alpha" : "1.000",
"blue" : "0.000",
"green" : "0.700",
"red" : "1.000"
}
},
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
-6
View File
@@ -1,19 +1,13 @@
import Foundation
import ActivityKit
// Shared between NWCWidget extension and the main zeus app target.
// Both targets compile this file; ActivityKit matches activities by type name.
@available(iOS 16.1, *)
struct NWCLiveActivityAttributes: ActivityAttributes {
struct ContentState: Codable, Hashable {
/// Currently playing track name, or nil when audio is stopped.
var currentTrackName: String?
/// Whether the user has muted the audio (session stays alive).
var isMuted: Bool
}
/// Fixed: when the NWC session started (drives the elapsed timer).
var startedAt: Date
}
-26
View File
@@ -1,26 +0,0 @@
import Foundation
// Compiled into BOTH the NWCWidget extension AND the main zeus app target.
//
// In the widget extension process the NoOp implementation is used.
// In the main app process NWCActivityManager.init() immediately swaps
// NWCBridge to the real NWCWidgetBridgeImpl, so Live Activity button taps
// (which run inside the host-app process via LiveActivityIntent) are routed
// back to the running audio session.
protocol NWCWidgetBridgeProtocol {
func nextTrack()
func prevTrack()
func toggleMute()
func stopNWC()
}
/// Global. NoOp by default; replaced by NWCWidgetBridgeImpl in the main app.
var NWCBridge: NWCWidgetBridgeProtocol = NWCWidgetBridgeNoOp()
final class NWCWidgetBridgeNoOp: NWCWidgetBridgeProtocol {
func nextTrack() {}
func prevTrack() {}
func toggleMute() {}
func stopNWC() {}
}
+1 -4
View File
@@ -1,9 +1,6 @@
import AppIntents
import WidgetKit
// Darwin notification names must match NWCAudioKeepAlive.m exactly.
// CFNotificationCenter is the correct IPC mechanism for widget extension host app
// because LiveActivityIntent.perform() runs in the extension process, not the main app.
// Must match NWCAudioKeepAlive.m
private let kNWCNextTrack = "com.zeusln.zeus.nwc.nextTrack"
private let kNWCPrevTrack = "com.zeusln.zeus.nwc.prevTrack"
private let kNWCToggleMute = "com.zeusln.zeus.nwc.toggleMute"
+4 -22
View File
@@ -3,10 +3,6 @@ import AppIntents
import SwiftUI
import WidgetKit
// Zeus icon (widget asset catalog: zeusLogo @1x/@2x/@3x)
// Uses the same layout as Primal's dynamicIslandLogo explicit frame + fit.
// Asset is built from zeus_icon.jpg (opaque), not the 1024 App Store icon.
private struct ZeusIcon: View {
var size: CGFloat = 20
@@ -20,8 +16,6 @@ private struct ZeusIcon: View {
}
}
// Circle button style
private struct CircleButtonStyle: ButtonStyle {
var size: CGFloat = 38
var tint: Color = Color.white.opacity(0.14)
@@ -35,15 +29,11 @@ private struct CircleButtonStyle: ButtonStyle {
}
}
// Expanded / lock-screen content
private struct NWCExpandedView: View {
let context: ActivityViewContext<NWCLiveActivityAttributes>
var body: some View {
VStack(alignment: .leading, spacing: 9) {
// Row 1 header
HStack(spacing: 10) {
ZeusIcon(size: 28)
@@ -59,16 +49,13 @@ private struct NWCExpandedView: View {
.monospacedDigit()
}
// Row 2 track name
let muted = context.state.isMuted
if let track = context.state.currentTrackName {
Text(muted ? "Muted · \(track)" : "Now playing: \(track)")
Text(context.state.isMuted ? "Muted · \(track)" : "Now playing: \(track)")
.font(.system(size: 12))
.foregroundStyle(.white.opacity(0.5))
.lineLimit(1)
}
// Row 3 controls (all buttons same size)
HStack(spacing: 0) {
Button(intent: NWCPrevTrackIntent()) {
Image(systemName: "backward.fill")
@@ -80,13 +67,12 @@ private struct NWCExpandedView: View {
Spacer()
Button(intent: NWCToggleMuteIntent()) {
Image(systemName: muted ? "speaker.slash.fill" : "speaker.wave.2.fill")
Image(systemName: context.state.isMuted ? "speaker.slash.fill" : "speaker.wave.2.fill")
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(muted ? Color.orange : .white.opacity(0.85))
.contentTransition(.symbolEffect(.replace))
.foregroundStyle(context.state.isMuted ? Color.orange : .white.opacity(0.85))
}
.buttonStyle(CircleButtonStyle(
tint: muted ? Color.orange.opacity(0.25) : Color.white.opacity(0.14)
tint: context.state.isMuted ? Color.orange.opacity(0.25) : Color.white.opacity(0.14)
))
Spacer()
@@ -118,12 +104,9 @@ private struct NWCExpandedView: View {
}
}
// Widget
struct NWCWidgetLiveActivity: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: NWCLiveActivityAttributes.self) { context in
// Lock-screen / notification-banner
NWCExpandedView(context: context)
.padding(.horizontal, 20)
.padding(.vertical, 14)
@@ -144,7 +127,6 @@ struct NWCWidgetLiveActivity: Widget {
: "speaker.wave.2.fill")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(context.state.isMuted ? .orange : .white)
.contentTransition(.symbolEffect(.replace))
}
.buttonStyle(.plain)
.padding(.trailing, 2)
-8
View File
@@ -227,9 +227,7 @@
NWCAUDIO0000000000000012 /* White Noise.m4a in Resources */ = {isa = PBXBuildFile; fileRef = NWCAUDIO0000000000000016 /* White Noise.m4a */; };
NWCAUDIO0000000000000013 /* Gentle Rain.m4a in Resources */ = {isa = PBXBuildFile; fileRef = NWCAUDIO0000000000000017 /* Gentle Rain.m4a */; };
NWCLA000000000000000001 /* NWCWidgetAttributes.swift in Sources */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000007 /* NWCWidgetAttributes.swift */; };
NWCLA000000000000000002 /* NWCWidgetBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000008 /* NWCWidgetBridge.swift */; };
NWCLA000000000000000003 /* NWCActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000009 /* NWCActivityManager.swift */; };
NWCLA000000000000000004 /* NWCWidgetBridgeImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000010 /* NWCWidgetBridgeImpl.swift */; };
NWCLA000000000000000005 /* NWCWidget.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = NWCLA000000000000000006 /* NWCWidget.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
@@ -721,9 +719,7 @@
NWCAUDIO0000000000000017 /* Gentle Rain.m4a */ = {isa = PBXFileReference; lastKnownFileType = "audio.x-m4a"; name = "Gentle Rain.m4a"; path = "zeus/Gentle Rain.m4a"; sourceTree = "<group>"; };
NWCLA000000000000000006 /* NWCWidget.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = NWCWidget.appex; sourceTree = BUILT_PRODUCTS_DIR; };
NWCLA000000000000000007 /* NWCWidgetAttributes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NWCWidgetAttributes.swift; path = NWCWidget/NWCWidgetAttributes.swift; sourceTree = "<group>"; };
NWCLA000000000000000008 /* NWCWidgetBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NWCWidgetBridge.swift; path = NWCWidget/NWCWidgetBridge.swift; sourceTree = "<group>"; };
NWCLA000000000000000009 /* NWCActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NWCActivityManager.swift; path = zeus/NWCActivityManager.swift; sourceTree = "<group>"; };
NWCLA000000000000000010 /* NWCWidgetBridgeImpl.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = NWCWidgetBridgeImpl.swift; path = zeus/NWCWidgetBridgeImpl.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
@@ -916,9 +912,7 @@
NWC1A2B3C4D5E6F700000003 /* NWCAudioKeepAlive.h */,
NWC1A2B3C4D5E6F700000002 /* NWCAudioKeepAlive.m */,
NWCLA000000000000000009 /* NWCActivityManager.swift */,
NWCLA000000000000000010 /* NWCWidgetBridgeImpl.swift */,
NWCLA000000000000000007 /* NWCWidgetAttributes.swift */,
NWCLA000000000000000008 /* NWCWidgetBridge.swift */,
NWCAUDIO0000000000000015 /* Fireplace.m4a */,
NWCAUDIO0000000000000016 /* White Noise.m4a */,
NWCAUDIO0000000000000017 /* Gentle Rain.m4a */,
@@ -2070,9 +2064,7 @@
13B07FBC1A68108700A75B9A /* AppDelegate.mm in Sources */,
NWC1A2B3C4D5E6F700000001 /* NWCAudioKeepAlive.m in Sources */,
NWCLA000000000000000001 /* NWCWidgetAttributes.swift in Sources */,
NWCLA000000000000000002 /* NWCWidgetBridge.swift in Sources */,
NWCLA000000000000000003 /* NWCActivityManager.swift in Sources */,
NWCLA000000000000000004 /* NWCWidgetBridgeImpl.swift in Sources */,
745388442A54C2B6000927CE /* lightning.pb.swift in Sources */,
74B637822A5A350B00750202 /* LncModule.mm in Sources */,
74B637842A5A350B00750202 /* Callback.mm in Sources */,
-2
View File
@@ -50,8 +50,6 @@ static void ClearKeychainIfNecessary() {
[RNNotifications startMonitorNotifications];
ClearKeychainIfNecessary();
// Initialize NWCActivityManager early so the widget bridge is set up before
// any Live Activity intent fires (intents run in the host-app process).
if (@available(iOS 16.1, *)) {
Class managerClass = NSClassFromString(@"NWCActivityManager");
if (managerClass && [managerClass respondsToSelector:@selector(shared)]) {
-21
View File
@@ -1,28 +1,18 @@
import Foundation
import ActivityKit
/// Manages the NWC Live Activity (Dynamic Island + lock-screen banner).
@available(iOS 16.1, *)
@objc final class NWCActivityManager: NSObject {
@objc static let shared = NWCActivityManager()
@objc var onNextTrack: (() -> Void)?
@objc var onPrevTrack: (() -> Void)?
@objc var onToggleMute: (() -> Void)?
@objc var onStop: (() -> Void)?
private var activity: Activity<NWCLiveActivityAttributes>?
override private init() {
super.init()
NWCBridge = NWCWidgetBridgeImpl(manager: self)
// Synchronous cleanup so a force-quit island is gone on next cold start.
endAllActivitiesBlocking()
}
// ObjC entry points
@objc func startActivity(trackName: String, isMuted: Bool) {
DispatchQueue.main.async { [weak self] in
self?.startActivityOnMain(trackName: trackName, isMuted: isMuted)
@@ -35,21 +25,16 @@ import ActivityKit
}
}
/// Normal stop (async). Ends every NWC activity, not only the cached reference.
@objc func stopActivity() {
DispatchQueue.main.async { [weak self] in
self?.stopActivityOnMain()
}
}
/// Ends all NWC Live Activities and blocks until ActivityKit completes.
/// Required on force-quit: async `stopActivity` often never runs before iOS kills the process.
@objc func endAllActivitiesImmediately() {
endAllActivitiesBlocking()
}
// Main-queue implementation
private var staleDate: Date { Date().addingTimeInterval(300) }
private func startActivityOnMain(trackName: String, isMuted: Bool) {
@@ -60,7 +45,6 @@ import ActivityKit
}
if let existing = activity, Self.isLive(existing) {
NSLog("[NWCActivity] activity already live, updating instead")
updateActivityOnMain(trackName: trackName, isMuted: isMuted)
return
}
@@ -114,8 +98,6 @@ import ActivityKit
}
}
// Blocking end (force-quit + cold-start cleanup)
private func endAllActivitiesBlocking() {
let sem = DispatchSemaphore(value: 0)
Task.detached(priority: .userInitiated) {
@@ -141,12 +123,9 @@ import ActivityKit
} else {
await act.end(using: finalState, dismissalPolicy: .immediate)
}
NSLog("[NWCActivity] ended activity id=%@", act.id)
}
}
// Helpers
private static func isLive(_ act: Activity<NWCLiveActivityAttributes>) -> Bool {
if act.activityState == .ended { return false }
if #available(iOS 16.2, *) {
+3 -145
View File
@@ -4,7 +4,6 @@
#import <UIKit/UIKit.h>
#import <sys/utsname.h>
// Event name constants
static NSString *const kEventInterrupted = @"NWCAudioInterrupted";
static NSString *const kEventInterruptionEnded = @"NWCAudioInterruptionEnded";
static NSString *const kEventRouteChanged = @"NWCAudioRouteChanged";
@@ -12,18 +11,14 @@ static NSString *const kEventStatusUpdate = @"NWCAudioStatusUpdate";
static NSString *const kEventSuspended = @"NWCAudioSuspended";
static NSString *const kEventTrackChanged = @"NWCAudioTrackChanged";
// How often (seconds) to emit a heartbeat status event while active
static const NSTimeInterval kStatusIntervalSeconds = 30.0;
// Darwin notification names shared with NWCWidgetIntent.swift (extension IPC).
// LiveActivityIntent.perform() runs in the widget extension process; posting a
// Darwin notification is the only way to reach the background-audio main app.
// Must match NWCWidgetIntent.swift
static NSString *const kDarwinNextTrack = @"com.zeusln.zeus.nwc.nextTrack";
static NSString *const kDarwinPrevTrack = @"com.zeusln.zeus.nwc.prevTrack";
static NSString *const kDarwinToggleMute = @"com.zeusln.zeus.nwc.toggleMute";
static NSString *const kDarwinStop = @"com.zeusln.zeus.nwc.stop";
// Available ambient audio tracks bundled with the app
static NSArray<NSString *> *kAvailableTracks(void) {
return @[@"Fireplace", @"White Noise", @"Gentle Rain"];
}
@@ -32,27 +27,21 @@ static NSArray<NSString *> *kAvailableTracks(void) {
@property (nonatomic, strong) AVAudioPlayer *audioPlayer;
// Track management
@property (nonatomic, copy) NSArray<NSString *> *trackNames;
@property (nonatomic, assign) NSInteger currentTrackIndex;
@property (nonatomic, assign) BOOL isMuted;
// Monitoring
@property (nonatomic, strong) NSDate *sessionStartTime;
@property (nonatomic, strong) NSDate *backgroundEnteredTime;
@property (nonatomic, strong) NSTimer *statusTimer;
@property (nonatomic, assign) BOOL isActive;
@property (nonatomic, assign) BOOL hasListeners;
// Stats
@property (nonatomic, assign) NSUInteger disconnectCount;
@property (nonatomic, strong) NSString *lastDisconnectReason;
@property (nonatomic, strong) NSString *iosVersion;
@property (nonatomic, strong) NSString *deviceModel;
// Live Activity early-start support.
// Set YES (while app is in foreground) so that the UIApplicationWillResignActiveNotification
// handler can call Activity.request() before the app state becomes .background.
@property (nonatomic, assign) BOOL nwcArmed;
- (void)switchToTrackAtIndex:(NSInteger)index;
@@ -63,8 +52,7 @@ static NSArray<NSString *> *kAvailableTracks(void) {
@end
// Darwin notification C callback must live after the private @interface above
// so the compiler can see properties and methods used inside the block.
// After private @interface (nwcDarwinCallback uses instance members).
static void nwcDarwinCallback(CFNotificationCenterRef center, void *observer,
CFNotificationName name, const void *object, CFDictionaryRef userInfo) {
NWCAudioKeepAlive *self = (__bridge NWCAudioKeepAlive *)observer;
@@ -105,7 +93,6 @@ static void nwcDarwinCallback(CFNotificationCenterRef center, void *observer,
RCT_EXPORT_MODULE();
// RCTEventEmitter
+ (BOOL)requiresMainQueueSetup {
return NO;
@@ -130,7 +117,6 @@ RCT_EXPORT_MODULE();
self.hasListeners = NO;
}
// Lifecycle
- (instancetype)init {
if (self = [super init]) {
@@ -159,12 +145,7 @@ RCT_EXPORT_MODULE();
[self unregisterDarwinObservers];
}
// Public API
/**
* Starts the AVAudioSession (.playback + mixWithOthers) and begins looping
* the currently selected ambient track. Returns a status dict on resolve.
*/
RCT_EXPORT_METHOD(startAudioKeepAlive:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
if (self.isActive) {
@@ -220,8 +201,6 @@ RCT_EXPORT_METHOD(startAudioKeepAlive:(RCTPromiseResolveBlock)resolve
}];
});
// 5. Wire up widget button callbacks and start the Live Activity
[self registerWidgetCallbacks];
if (@available(iOS 16.1, *)) {
NSString *trackName = self.trackNames[self.currentTrackIndex];
[[NWCActivityManager shared] startActivityWithTrackName:trackName
@@ -233,26 +212,17 @@ RCT_EXPORT_METHOD(startAudioKeepAlive:(RCTPromiseResolveBlock)resolve
resolve([self currentStatusDict]);
}
/**
* Stops playback and deactivates the AVAudioSession.
*/
RCT_EXPORT_METHOD(stopAudioKeepAlive:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
[self stopInternal:@"manual_stop"];
resolve([self currentStatusDict]);
}
/**
* Returns the current status without modifying state.
*/
RCT_EXPORT_METHOD(getStatus:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
resolve([self currentStatusDict]);
}
/**
* Returns the list of available ambient audio tracks.
*/
RCT_EXPORT_METHOD(getAvailableTracks:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSMutableArray *result = [NSMutableArray array];
@@ -266,9 +236,6 @@ RCT_EXPORT_METHOD(getAvailableTracks:(RCTPromiseResolveBlock)resolve
resolve(result);
}
/**
* Selects a track by index and immediately switches to it if active.
*/
RCT_EXPORT_METHOD(setTrack:(NSInteger)index
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
@@ -287,18 +254,12 @@ RCT_EXPORT_METHOD(setTrack:(NSInteger)index
resolve([self currentStatusDict]);
}
/**
* Advances to the next track (wraps around).
*/
RCT_EXPORT_METHOD(nextTrack:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSInteger next = (self.currentTrackIndex + 1) % (NSInteger)self.trackNames.count;
[self setTrack:next resolver:resolve rejecter:reject];
}
/**
* Goes back to the previous track (wraps around).
*/
RCT_EXPORT_METHOD(previousTrack:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
NSInteger prev = (self.currentTrackIndex - 1 + (NSInteger)self.trackNames.count)
@@ -306,9 +267,6 @@ RCT_EXPORT_METHOD(previousTrack:(RCTPromiseResolveBlock)resolve
[self setTrack:prev resolver:resolve rejecter:reject];
}
/**
* Mutes or unmutes the audio track. The session stays alive; only volume is 0.
*/
RCT_EXPORT_METHOD(setMuted:(BOOL)muted
resolver:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
@@ -316,31 +274,21 @@ RCT_EXPORT_METHOD(setMuted:(BOOL)muted
resolve([self currentStatusDict]);
}
/**
* Called from JS while the app is still in the FOREGROUND (when the NWC service
* becomes active and the AppState monitor is registered). Sets a flag so that
* handleWillResignActive: can call Activity.request() immediately the only
* window where ActivityKit accepts new activities (UIApplication.applicationState
* must be .active, which it is during UIApplicationWillResignActiveNotification).
*/
RCT_EXPORT_METHOD(armNWCAudio:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
self.nwcArmed = YES;
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
// Remove stale observers before re-registering to avoid duplicates.
[nc removeObserver:self name:UIApplicationWillResignActiveNotification object:nil];
[nc removeObserver:self name:UIApplicationWillTerminateNotification object:nil];
[nc addObserver:self
selector:@selector(handleWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:nil];
// Stop the Live Activity if the process is force-quit by the user.
[nc addObserver:self
selector:@selector(handleWillTerminate:)
name:UIApplicationWillTerminateNotification
object:nil];
// Always unregister first to prevent duplicate callbacks if armNWCAudio
// is called more than once during a session.
[self unregisterDarwinObservers];
[self registerDarwinObservers];
@@ -349,9 +297,6 @@ RCT_EXPORT_METHOD(armNWCAudio:(RCTPromiseResolveBlock)resolve
resolve(@(YES));
}
/**
* Called from JS when the NWC service stops or is torn down.
*/
RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
@@ -368,10 +313,7 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
resolve(@(YES));
}
// Internal track / mute helpers
/// Switches to the given track index, starts it if the session is active,
/// and updates the Live Activity state.
- (void)switchToTrackAtIndex:(NSInteger)index {
self.currentTrackIndex = index;
@@ -387,7 +329,6 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
}
}
/// Applies a mute/unmute change and propagates to the Live Activity.
- (void)applyMuted:(BOOL)muted {
self.isMuted = muted;
self.audioPlayer.volume = muted ? 0.0f : 1.0f;
@@ -400,62 +341,7 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
}
}
// Widget callback registration
/// Registers callback blocks on NWCActivityManager so widget button taps
/// (next/prev/mute/stop) route back into this instance.
- (void)registerWidgetCallbacks {
if (@available(iOS 16.1, *)) {
__weak typeof(self) weakSelf = self;
[NWCActivityManager shared].onNextTrack = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
NSInteger next = (strongSelf.currentTrackIndex + 1)
% (NSInteger)strongSelf.trackNames.count;
[strongSelf switchToTrackAtIndex:next];
[strongSelf safeEmit:kEventTrackChanged body:@{
@"trackIndex": @(next),
@"trackName": strongSelf.trackNames[next],
@"source": @"widget"
}];
};
[NWCActivityManager shared].onPrevTrack = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
NSInteger count = (NSInteger)strongSelf.trackNames.count;
NSInteger prev = (strongSelf.currentTrackIndex - 1 + count) % count;
[strongSelf switchToTrackAtIndex:prev];
[strongSelf safeEmit:kEventTrackChanged body:@{
@"trackIndex": @(prev),
@"trackName": strongSelf.trackNames[prev],
@"source": @"widget"
}];
};
[NWCActivityManager shared].onToggleMute = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf applyMuted:!strongSelf.isMuted];
};
[NWCActivityManager shared].onStop = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf stopInternal:@"widget_stop"];
// Emit so JS can react (close NWC connection, etc.)
[strongSelf safeEmit:kEventSuspended body:@{
@"reason": @"widget_stop",
@"uptimeSeconds": @([strongSelf uptimeSeconds])
}];
};
}
}
// Audio player
/// Resolves a bundled ambient track by name (tries common audio extensions).
- (NSString *)pathForBundledTrackNamed:(NSString *)trackName {
static NSArray<NSString *> *extensions;
static dispatch_once_t once;
@@ -527,7 +413,6 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
self.audioPlayer = nil;
}
// AVAudioPlayerDelegate
- (void)audioPlayerDecodeErrorDidOccur:(AVAudioPlayer *)player error:(NSError *)error {
NSLog(@"[NWCAudio] Decode error: %@", error.localizedDescription);
@@ -547,7 +432,6 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
}
}
// Internal stop
- (void)stopInternal:(NSString *)reason {
if (!self.isActive) return;
@@ -563,9 +447,7 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
[self teardownAudioPlayer];
[self unregisterNotifications];
// unregisterNotifications uses removeObserver:self which wipes ALL observers
// registered by armNWCAudio. Re-register the two lifetime observers so the
// next background/terminate events are still handled correctly.
// removeObserver:self clears arm observers; re-register if still armed.
if (self.nwcArmed) {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
[nc addObserver:self
@@ -596,7 +478,6 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
NSLog(@"[NWCAudio] Stopped after %.0f s reason: %@", uptime, reason);
}
// AVAudioSession notifications
- (void)registerNotifications {
NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
@@ -628,12 +509,9 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
}
- (void)unregisterNotifications {
// Only removes NSNotificationCenter observers.
// Darwin observers (widget IPC) are managed separately by arm/disarm.
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// Darwin IPC helpers (widget extension main app)
- (void)registerDarwinObservers {
CFNotificationCenterRef darwin = CFNotificationCenterGetDarwinNotifyCenter();
@@ -660,18 +538,7 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
NSLog(@"[NWCAudio] Darwin observers removed");
}
// Notification handlers
/**
* Fires while UIApplication.applicationState is still .active (the only state
* in which ActivityKit accepts Activity.request()). If the NWC service is armed
* and audio is not yet playing, we start the Live Activity right here so it is
* already visible the instant the app moves to background.
*
* When startAudioKeepAlive is later called (from the 'background' AppState
* event on the JS side), NWCActivityManager detects the existing activity and
* updates its state instead of requesting a new one.
*/
- (void)handleWillResignActive:(NSNotification *)notification {
if (!self.nwcArmed) return;
if (@available(iOS 16.1, *)) {
@@ -682,11 +549,6 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
}
}
/**
* Called when the process is about to be terminated (user force-quits via the
* app-switcher, or the OS terminates a suspended background app).
* Ends the Live Activity immediately so the Dynamic Island disappears.
*/
- (void)handleWillTerminate:(NSNotification *)notification {
NSLog(@"[NWCAudio] App terminating ending Live Activity (blocking)");
if (@available(iOS 16.1, *)) {
@@ -789,7 +651,6 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
[self emitStatusUpdate:NO reason:@"returned_to_foreground"];
}
// Audio player resume
- (void)resumeAudioPlayer {
if (self.audioPlayer.isPlaying) return;
@@ -816,7 +677,6 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
}
}
// Event helpers
- (void)emitStatusUpdate:(BOOL)isSuspected reason:(NSString *)reason {
if (!self.hasListeners) return;
@@ -833,7 +693,6 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
[self sendEventWithName:eventName body:body];
}
// Status dict
- (NSDictionary *)currentStatusDict {
NSTimeInterval uptime = [self uptimeSeconds];
@@ -858,7 +717,6 @@ RCT_EXPORT_METHOD(disarmNWCAudio:(RCTPromiseResolveBlock)resolve
};
}
// Utilities
- (NSTimeInterval)uptimeSeconds {
return self.sessionStartTime
-38
View File
@@ -1,38 +0,0 @@
import Foundation
/// Real bridge implementation compiled into the main zeus app target only.
/// Routes Live Activity button taps back to NWCActivityManager's callback blocks
/// (which NWCAudioKeepAlive.m registers at session start).
@available(iOS 16.1, *)
final class NWCWidgetBridgeImpl: NWCWidgetBridgeProtocol {
private weak var manager: NWCActivityManager?
init(manager: NWCActivityManager) {
self.manager = manager
}
func nextTrack() {
DispatchQueue.main.async { [weak self] in
self?.manager?.onNextTrack?()
}
}
func prevTrack() {
DispatchQueue.main.async { [weak self] in
self?.manager?.onPrevTrack?()
}
}
func toggleMute() {
DispatchQueue.main.async { [weak self] in
self?.manager?.onToggleMute?()
}
}
func stopNWC() {
DispatchQueue.main.async { [weak self] in
self?.manager?.onStop?()
}
}
}