From ee4c88795add006d485ecb245ad675b5ed9244e5 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Wed, 26 Jul 2023 14:15:17 +0800 Subject: [PATCH 1/8] Feature: Wireless Connection --- Lookin.xcodeproj/project.pbxproj | 2 +- LookinClient/Connection/LKConnectionManager.h | 26 +- LookinClient/Connection/LKConnectionManager.m | 330 ++++++++++++------ .../Static/LKStaticWindowController.m | 102 +++--- Podfile | 4 +- Podfile.lock | 16 +- 6 files changed, 317 insertions(+), 163 deletions(-) diff --git a/Lookin.xcodeproj/project.pbxproj b/Lookin.xcodeproj/project.pbxproj index 60a5296b..7eba9b23 100644 --- a/Lookin.xcodeproj/project.pbxproj +++ b/Lookin.xcodeproj/project.pbxproj @@ -920,10 +920,10 @@ children = ( AAA8C08B21CC17670017A345 /* LKAppsManager.m */, AAA8C08821CC17670017A345 /* LKAppsManager.h */, + AAA8C08921CC17670017A345 /* LKConnectionManager.h */, AAA8C08C21CC17670017A345 /* LKConnectionManager.m */, AA7F157622C0F2B9004436AD /* LKConnectionRequest.h */, AA7F157722C0F2B9004436AD /* LKConnectionRequest.m */, - AAA8C08921CC17670017A345 /* LKConnectionManager.h */, AAA8C08D21CC17670017A345 /* LKInspectableApp.h */, AAA8C08A21CC17670017A345 /* LKInspectableApp.m */, ); diff --git a/LookinClient/Connection/LKConnectionManager.h b/LookinClient/Connection/LKConnectionManager.h index b44adcff..f7453d00 100644 --- a/LookinClient/Connection/LKConnectionManager.h +++ b/LookinClient/Connection/LKConnectionManager.h @@ -7,8 +7,18 @@ // #import +#import "ECOChannelManager.h" +#import "Lookin_PTChannel.h" -@class Lookin_PTChannel; +@class LKConnectionRequest; + +@protocol LookinChannelProtocol + +@property (readonly) BOOL isConnected; + +@property(nonatomic, strong) NSMutableSet *activeRequests; + +@end /** iOS 是 Server 端,macOS 是 Client 端 @@ -46,3 +56,17 @@ @property(nonatomic, strong, readonly) RACSubject *didReceivePush; @end + +@interface Lookin_PTChannel (LKConnection) + +/// 已经发送但尚未收到全部回复的请求 +@property(nonatomic, strong) NSMutableSet *activeRequests; + +@end + +@interface ECOChannelDeviceInfo (LKConnection) + +/// 已经发送但尚未收到全部回复的请求 +@property(nonatomic, strong) NSMutableSet *activeRequests; + +@end diff --git a/LookinClient/Connection/LKConnectionManager.m b/LookinClient/Connection/LKConnectionManager.m index c42db6df..5037b621 100644 --- a/LookinClient/Connection/LKConnectionManager.m +++ b/LookinClient/Connection/LKConnectionManager.m @@ -14,6 +14,7 @@ #import "LookinAppInfo.h" #import "LKConnectionRequest.h" #import "LKServerVersionRequestor.h" +#import "ECOChannelManager.h" static NSIndexSet * PushFrameTypeList() { static NSIndexSet *list; @@ -25,21 +26,26 @@ return list; } -@interface Lookin_PTChannel (LKConnection) +@implementation Lookin_PTChannel (LKConnection) + +- (void)setActiveRequests:(NSMutableSet *)activeRequests { + [self lookin_bindObject:activeRequests forKey:@"activeRequest"]; +} -/// 已经发送但尚未收到全部回复的请求 -@property(nonatomic, strong) NSMutableSet *activeRequests; +- (NSMutableSet *)activeRequests { + return [self lookin_getBindObjectForKey:@"activeRequest"]; +} @end -@implementation Lookin_PTChannel (LKConnection) +@implementation ECOChannelDeviceInfo (LKConnection) - (void)setActiveRequests:(NSMutableSet *)activeRequests { - [self lookin_bindObject:activeRequests forKey:@"activeRequest"]; + [self lookin_bindObject:activeRequests forKey:@"activeRequest"]; } - (NSMutableSet *)activeRequests { - return [self lookin_getBindObjectForKey:@"activeRequest"]; + return [self lookin_getBindObjectForKey:@"activeRequest"]; } @end @@ -82,6 +88,8 @@ @interface LKConnectionManager () @property(nonatomic, copy) NSArray *allSimulatorPorts; @property(nonatomic, strong) NSMutableArray *allUSBPorts; +@property(nonatomic, strong) NSMutableArray *allWirelessDevices; +@property(nonatomic, strong) ECOChannelManager *wirelessChannel; @end @@ -104,7 +112,7 @@ - (instancetype)init { if (self = [super init]) { _channelWillEnd = [RACSubject subject]; _didReceivePush = [RACSubject subject]; - + self.allSimulatorPorts = ({ NSMutableArray *ports = [NSMutableArray array]; for (int number = LookinSimulatorIPv4PortNumberStart; number <= LookinSimulatorIPv4PortNumberEnd; number++) { @@ -115,9 +123,11 @@ - (instancetype)init { ports; }); self.allUSBPorts = [NSMutableArray array]; - + self.allWirelessDevices = [NSMutableArray array]; + + [self _startListeningForWirelessDevices]; [self _startListeningForUSBDevices]; - + [[LKServerVersionRequestor shared] preload]; } return self; @@ -127,9 +137,10 @@ - (instancetype)init { - (RACSignal *)tryToConnectAllPorts { return [[RACSignal zip:@[[self _tryToConnectAllSimulatorPorts], - [self _tryToConnectAllUSBDevices]]] map:^id _Nullable(RACTuple * _Nullable value) { - RACTupleUnpack(NSArray *simulatorChannels, NSArray *usbChannels) = value; - NSArray *connectedChannels = [simulatorChannels arrayByAddingObjectsFromArray:usbChannels]; + [self _tryToConnectAllUSBDevices], + [self _tryToConnectToWirelessDevice]]] map:^id _Nullable(RACTuple * _Nullable value) { + RACTupleUnpack(NSArray *simulatorChannels, NSArray *usbChannels, NSArray *wirelessDevices) = value; + NSArray *connectedChannels = [[simulatorChannels arrayByAddingObjectsFromArray:usbChannels] arrayByAddingObjectsFromArray:wirelessDevices]; return connectedChannels; }]; } @@ -159,7 +170,7 @@ - (RACSignal *)_connectToSimulatorPort:(LKSimulatorConnectionPort *)port { [subscriber sendCompleted]; return nil; } - + Lookin_PTChannel *localChannel = [Lookin_PTChannel channelWithDelegate:self]; [localChannel connectToPort:port.portNumber IPv4Address:INADDR_LOOPBACK callback:^(NSError *error, Lookin_PTAddress *address) { if (error) { @@ -207,7 +218,7 @@ - (RACSignal *)_connectToUSBPort:(LKUSBConnectionPort *)port { [subscriber sendCompleted]; return nil; } - + Lookin_PTChannel *channel = [Lookin_PTChannel channelWithDelegate:self]; [channel connectToPort:port.portNumber overUSBHub:Lookin_PTUSBHub.sharedHub deviceID:port.deviceID callback:^(NSError *error) { if (error) { @@ -229,6 +240,19 @@ - (RACSignal *)_connectToUSBPort:(LKUSBConnectionPort *)port { }]; } +- (RACSignal *)_tryToConnectToWirelessDevice { + if (self.allWirelessDevices.count) { + NSArray *devices = [self.allWirelessDevices lookin_filter:^BOOL(ECOChannelDeviceInfo *obj) { + return obj.isConnected; + }]; + if (devices.count != self.allWirelessDevices.count) { + self.allWirelessDevices = [NSMutableArray arrayWithArray:devices]; + } + return [RACSignal return:devices]; + } + return [RACSignal return:@[]]; +} + #pragma mark - Request - (void)pushWithType:(unsigned int)pushType data:(NSObject *)data channel:(Lookin_PTChannel *)channel { @@ -254,7 +278,7 @@ - (RACSignal *)requestWithType:(unsigned int)requestType data:(NSObject *)reques } else { timeoutInterval = 2; } - + [self _requestWithType:LookinRequestTypePing channel:channel data:nil timeoutInterval:timeoutInterval succ:^(LookinConnectionResponseAttachment *pingResponse) { // ping 成功了 // NSLog(@"LookinClient, level1 - ping succ, will send request:%@, port:%@", @(type), @(channel.portNumber)); @@ -273,11 +297,11 @@ - (RACSignal *)requestWithType:(unsigned int)requestType data:(NSObject *)reques [subscriber sendCompleted]; }]; } - + } fail:^(NSError *error) { // ping 失败了 [subscriber sendError:error]; - + } completion:nil]; return nil; }]; @@ -290,26 +314,26 @@ - (NSError *)_checkServerVersionWithResponse:(LookinConnectionResponseAttachment NSError *versionErr = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_ServerVersionTooLow userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"Fail to inspect this iOS app due to a version problem.", nil), NSLocalizedRecoverySuggestionErrorKey:NSLocalizedString(@"Please update LookinServer.framework linked with target iOS App to a newer version. Visit the website below to get detailed instructions:\nhttps://lookin.work/faq/server-version-too-low/", nil)}]; return versionErr; } - + if (serverVersion > LOOKIN_SUPPORTED_SERVER_MAX) { // server 版本过高,需要升级 client NSError *versionErr = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_ServerVersionTooHigh userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"Lookin app version is too low.", nil), NSLocalizedRecoverySuggestionErrorKey:NSLocalizedString(@"Target iOS app is linked with a higher version LookinServer.framework. Please click \"Lookin\"-\"Check for Updates\" near the top-left corner or visit https://lookin.work to update your Lookin app.", nil)}]; return versionErr; - + } - + if (serverVersion < LOOKIN_SUPPORTED_SERVER_MIN) { // server 版本过低,需要升级 server NSError *versionErr = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_ServerVersionTooLow userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"Fail to inspect this iOS app due to a version problem.", nil), NSLocalizedRecoverySuggestionErrorKey:NSLocalizedString(@"Please update LookinServer.framework linked with target iOS App to a newer version. Visit the website below to get detailed instructions:\nhttps://lookin.work/faq/server-version-too-low/", nil)}]; return versionErr; } - + return nil; } #pragma mark - Private -- (void)_requestWithType:(unsigned int)requestType channel:(Lookin_PTChannel *)channel data:(NSObject *)data timeoutInterval:(NSTimeInterval)timeoutInterval succ:(void (^)(id data))succBlock fail:(void (^)(NSError *error))failBlock completion:(void (^)(void))completionBlock { +- (void)_requestWithType:(unsigned int)requestType channel:(id)channel data:(NSObject *)data timeoutInterval:(NSTimeInterval)timeoutInterval succ:(void (^)(id data))succBlock fail:(void (^)(NSError *error))failBlock completion:(void (^)(void))completionBlock { if (!channel) { NSAssert(NO, @""); if (failBlock) { @@ -323,6 +347,16 @@ - (void)_requestWithType:(unsigned int)requestType channel:(Lookin_PTChannel *)c } return; } + + ECOChannelDeviceInfo *device; + Lookin_PTChannel *ptChannel; + if ([channel isKindOfClass:ECOChannelDeviceInfo.class]) { + device = (ECOChannelDeviceInfo *)channel; + } + if ([channel isKindOfClass:Lookin_PTChannel.class]) { + ptChannel = (Lookin_PTChannel *)channel; + } + if (channel.activeRequests.count && requestType != LookinRequestTypePing) { // 检查是否有相同 type 的旧请求尚在进行中,如果有则移除之前的旧请求(旧请求会被报告 error) NSSet *requestsToBeDiscarded = [channel.activeRequests lookin_filter:^BOOL(LKConnectionRequest *obj) { @@ -335,11 +369,11 @@ - (void)_requestWithType:(unsigned int)requestType channel:(Lookin_PTChannel *)c } [obj endTimeoutCount]; [channel.activeRequests removeObject:obj]; - + NSLog(@"LookinClient - will discard request, type:%@, tag:%@", @(obj.type), @(obj.tag)); }]; } - + LKConnectionRequest *request = [[LKConnectionRequest alloc] init]; request.type = requestType; request.tag = (uint32_t)[[NSDate date] timeIntervalSince1970]; @@ -354,30 +388,42 @@ - (void)_requestWithType:(unsigned int)requestType channel:(Lookin_PTChannel *)c selfRequest.failBlock(error); [channel.activeRequests removeObject:selfRequest]; }; - + LookinConnectionAttachment *attachment = [LookinConnectionAttachment new]; attachment.data = data; NSError *archiveError = nil; - dispatch_data_t payload = [[NSKeyedArchiver archivedDataWithRootObject:attachment requiringSecureCoding:YES error:&archiveError] createReferencingDispatchData]; + NSData *sendData = [NSKeyedArchiver archivedDataWithRootObject:attachment requiringSecureCoding:YES error:&archiveError]; + dispatch_data_t payload = [sendData createReferencingDispatchData]; if (archiveError) { NSAssert(NO, @""); } - [channel sendFrameOfType:requestType tag:request.tag withPayload:payload callback:^(NSError *error) { -// NSLog(@"LookinClient - sendRequest, type:%@", @(requestType)); - if (error) { - if (failBlock) { - NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_PeerTalk userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"The operation failed due to an inner error.", nil)}]; - failBlock(error); - } - } else { - // 成功发出了该 request - if (!channel.activeRequests) { - channel.activeRequests = [NSMutableSet set]; - } - [channel.activeRequests addObject:request]; - [request resetTimeoutCount]; - } - }]; + + if (device) { + [self.wirelessChannel sendPacket:sendData extraInfo:@{@"tag": @(request.tag), @"type": @(request.type)} toDevice:device]; + if (!device.activeRequests) { + device.activeRequests = [NSMutableSet set]; + } + [device.activeRequests addObject:request]; + [request resetTimeoutCount]; + } + if (ptChannel) { + [ptChannel sendFrameOfType:requestType tag:request.tag withPayload:payload callback:^(NSError *error) { + // NSLog(@"LookinClient - sendRequest, type:%@", @(requestType)); + if (error) { + if (failBlock) { + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_PeerTalk userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"The operation failed due to an inner error.", nil)}]; + failBlock(error); + } + } else { + // 成功发出了该 request + if (!channel.activeRequests) { + channel.activeRequests = [NSMutableSet set]; + } + [channel.activeRequests addObject:request]; + [request resetTimeoutCount]; + } + }]; + } } - (void)cancelRequestWithType:(unsigned int)requestType channel:(Lookin_PTChannel *)channel { @@ -397,10 +443,10 @@ - (void)cancelRequestWithType:(unsigned int)requestType channel:(Lookin_PTChanne - (void)_startListeningForUSBDevices { NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; - + [nc addObserverForName:Lookin_PTUSBDeviceDidAttachNotification object:Lookin_PTUSBHub.sharedHub queue:nil usingBlock:^(NSNotification *note) { NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"]; - + /// 仅一台真机 device 上的所有 app 共享同一批端口(在 Lookin 里是 47175 ~ 47179 这 5 个),不同真机互不影响。比如依次启动“真机 A 的 app1”、“真机 A 的 app2”、“真机 B 的 app3”,则它们依次会占用 47175、47176、47175(注意不是 47177)这几个端口 for (int number = LookinUSBDeviceIPv4PortNumberStart; number <= LookinUSBDeviceIPv4PortNumberEnd; number++) { LKUSBConnectionPort *port = [LKUSBConnectionPort new]; @@ -410,7 +456,7 @@ - (void)_startListeningForUSBDevices { } NSLog(@"Lookin - USB 设备插入,DeviceID: %@", deviceID); }]; - + [nc addObserverForName:Lookin_PTUSBDeviceDidDetachNotification object:Lookin_PTUSBHub.sharedHub queue:nil usingBlock:^(NSNotification *note) { NSNumber *deviceID = [note.userInfo objectForKey:@"DeviceID"]; [self.allUSBPorts.copy enumerateObjectsUsingBlock:^(LKUSBConnectionPort * _Nonnull port, NSUInteger idx, BOOL * _Nonnull stop) { @@ -422,13 +468,71 @@ - (void)_startListeningForUSBDevices { }]; } +- (void)_startListeningForWirelessDevices { + if (!self.wirelessChannel) { + self.wirelessChannel = ECOChannelManager.new; + } + @weakify(self); + // 接收到数据回调 + self.wirelessChannel.receivedBlock = ^(ECOChannelDeviceInfo *device, NSData *data, NSDictionary *extraInfo) { + NSLog(@"🚀 Lookin receivedBlock device:%@", device); + NSNumber *tag = extraInfo[@"tag"]; + NSNumber *type = extraInfo[@"type"]; + LKConnectionRequest *activeRequest = [device.activeRequests lookin_firstFiltered:^BOOL(LKConnectionRequest *obj) { + return [@(obj.type) isEqualToNumber:type] && [@(obj.tag) isEqualToNumber:tag]; + }]; + if (!activeRequest) { + // 也许在 shouldAcceptFrameOfType 和 didReceiveFrame 两个时机之间,该 request 因为超时而被销毁了?有点玄学但确实偶尔会走到这里。 + return; + } + [self_weak_ _didReceiveDataWithChannel:device data:data activeRequest:activeRequest]; + }; + // 设备连接变更 + self.wirelessChannel.deviceBlock = ^(ECOChannelDeviceInfo *device, BOOL isConnected) { + NSLog(@"🚀 Lookin deviceBlock device:%@", device); + if (isConnected && ![self_weak_.allWirelessDevices containsObject:device]) { + NSString *uniId = [NSString stringWithFormat:@"%@_%@",device.uuid, device.appInfo.appId]; + [self_weak_.wirelessChannel sendAuthorizationMessageToDevice:device + state:ECOAuthorizeResponseType_AllowAlways + showAuthAlert:![self_weak_.wirelessChannel.whitelistDevices containsObject:uniId]]; + } else if (!isConnected) { + [self_weak_.allWirelessDevices removeObject:device]; + [self_weak_.channelWillEnd sendNext:device]; + } + }; + // 授权状态变更回调 + self.wirelessChannel.authStateChangedBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { + NSLog(@"🚀 Lookin authStateChangedBlock device:%@", device); + if (authState) { + if (![self_weak_.allWirelessDevices containsObject:device]) { + // Ping测试 + [self_weak_ _requestWithType:LookinRequestTypePing channel:device data:nil timeoutInterval:2 succ:^(LookinConnectionResponseAttachment *pingResponse) { + // ping 成功了 + // NSLog(@"LookinClient, level1 - ping succ, will send request:%@, port:%@", @(type), @(channel.portNumber)); + + [self_weak_.allWirelessDevices addObject:device]; + } fail:^(NSError *error) { + // ping 失败了 + } completion:nil]; + } + } else if ([self_weak_.allWirelessDevices containsObject:device]) { + [self_weak_.allWirelessDevices removeObject:device]; + [self_weak_.channelWillEnd sendNext:device]; + } + }; + // 请求授权状态认证回调 + self.wirelessChannel.requestAuthBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { + NSLog(@"🚀 Lookin requestAuthBlock device:%@ authState:%ld", device, authState); + }; +} + #pragma mark - - (BOOL)ioFrameChannel:(Lookin_PTChannel*)channel shouldAcceptFrameOfType:(uint32_t)type tag:(uint32_t)tag payloadSize:(uint32_t)payloadSize { if ([PushFrameTypeList() containsIndex:type]) { return YES; } - + LKConnectionRequest *activeRequest = [channel.activeRequests lookin_firstFiltered:^BOOL(LKConnectionRequest *obj) { return (obj.type == type && obj.tag == tag); }]; @@ -448,12 +552,12 @@ - (void)ioFrameChannel:(Lookin_PTChannel*)channel didReceiveFrameOfType:(uint32_ if (unarchiveError) { // NSAssert(NO, @""); } - + RACTuple *tuple = [RACTuple tupleWithObjects:channel, @(type), unarchivedData, nil]; [self.didReceivePush sendNext:tuple]; return; } - + // NSLog(@"LookinClient - did receive, port:%@, type:%@, tag:%@", @(channel.portNumber), @(type), @(tag)); LKConnectionRequest *activeRequest = [channel.activeRequests lookin_firstFiltered:^BOOL(LKConnectionRequest *obj) { return (obj.type == type && obj.tag == tag); @@ -464,70 +568,74 @@ - (void)ioFrameChannel:(Lookin_PTChannel*)channel didReceiveFrameOfType:(uint32_ } NSData *data = [NSData dataWithContentsOfDispatchData:payload.dispatchData]; - NSError *unarchiveError = nil; - LookinConnectionResponseAttachment *attachment = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSObject class] fromData:data error:&unarchiveError]; - if (unarchiveError) { - NSLog(@"Error:%@", unarchiveError); + + [self _didReceiveDataWithChannel:channel data:data activeRequest:activeRequest]; +} + +- (void)_didReceiveDataWithChannel:(id)channel data:(NSData *)data activeRequest:(LKConnectionRequest *)activeRequest { + NSError *unarchiveError = nil; + LookinConnectionResponseAttachment *attachment = [NSKeyedUnarchiver unarchivedObjectOfClass:[NSObject class] fromData:data error:&unarchiveError]; + if (unarchiveError) { // NSAssert(NO, @""); - } - - if (attachment.appIsInBackground) { - // app 处于后台模式 - - [activeRequest endTimeoutCount]; - [channel.activeRequests removeObject:activeRequest]; - - if (activeRequest.failBlock) { - NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_PingFailForBackgroundState userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"The operation failed because target iOS app has entered to the background state.", nil)}]; - activeRequest.failBlock(error); - } - - NSLog(@"Lookin - iOS app 报告自己处于后台,request fail"); - - return; - } - - if (activeRequest.succBlock) { - activeRequest.succBlock(attachment); - } - - static NSUInteger dataSize = 0; - static CFTimeInterval startTime = 0; - if (activeRequest.receivedDataCount == 0) { - dataSize = 0; - startTime = CACurrentMediaTime(); - } - dataSize += data.length; - - BOOL hasReceivedAllResponses = NO; - if (attachment.dataTotalCount > 0) { - activeRequest.receivedDataCount += attachment.currentDataCount; - if (activeRequest.receivedDataCount >= attachment.dataTotalCount) { - hasReceivedAllResponses = YES; - } - } else { - hasReceivedAllResponses = YES; - } - - if (hasReceivedAllResponses) { - [activeRequest endTimeoutCount]; - [channel.activeRequests removeObject:activeRequest]; - if (activeRequest.completionBlock) { - activeRequest.completionBlock(); - } - - CFTimeInterval timeDuration = CACurrentMediaTime() - startTime; - CGFloat totalSize = dataSize / 1024.0 / 1024.0; - if (totalSize > 0.5) { - NSMutableString *logString = [[NSMutableString alloc] initWithString:@"Lookin - "]; - [logString appendFormat:@"已收到全部请求 %@ / %@,总耗时:%.2f, 数据总大小:%.2fM", @(activeRequest.receivedDataCount), @(attachment.dataTotalCount), timeDuration, totalSize]; - NSLog(@"%@", logString); - } - } else { - /// 对于多 response 的请求,每收到一次 response 则重置 timeout 倒计时 - [activeRequest resetTimeoutCount]; + } + + if (attachment.appIsInBackground) { + // app 处于后台模式 + + [activeRequest endTimeoutCount]; + [channel.activeRequests removeObject:activeRequest]; + + if (activeRequest.failBlock) { + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_PingFailForBackgroundState userInfo:@{NSLocalizedDescriptionKey:NSLocalizedString(@"The operation failed because target iOS app has entered to the background state.", nil)}]; + activeRequest.failBlock(error); + } + + NSLog(@"Lookin - iOS app 报告自己处于后台,request fail"); + + return; + } + + if (activeRequest.succBlock) { + activeRequest.succBlock(attachment); + } + + static NSUInteger dataSize = 0; + static CFTimeInterval startTime = 0; + if (activeRequest.receivedDataCount == 0) { + dataSize = 0; + startTime = CACurrentMediaTime(); + } + dataSize += data.length; + + BOOL hasReceivedAllResponses = NO; + if (attachment.dataTotalCount > 0) { + activeRequest.receivedDataCount += attachment.currentDataCount; + if (activeRequest.receivedDataCount >= attachment.dataTotalCount) { + hasReceivedAllResponses = YES; + } + } else { + hasReceivedAllResponses = YES; + } + + if (hasReceivedAllResponses) { + [activeRequest endTimeoutCount]; + [channel.activeRequests removeObject:activeRequest]; + if (activeRequest.completionBlock) { + activeRequest.completionBlock(); + } + + CFTimeInterval timeDuration = CACurrentMediaTime() - startTime; + CGFloat totalSize = dataSize / 1024.0 / 1024.0; + if (totalSize > 0.5) { + NSMutableString *logString = [[NSMutableString alloc] initWithString:@"Lookin - "]; + [logString appendFormat:@"已收到全部请求 %@ / %@,总耗时:%.2f, 数据总大小:%.2fM", @(activeRequest.receivedDataCount), @(attachment.dataTotalCount), timeDuration, totalSize]; + NSLog(@"%@", logString); + } + } else { + /// 对于多 response 的请求,每收到一次 response 则重置 timeout 倒计时 + [activeRequest resetTimeoutCount]; // NSLog(@"Lookin - 收到请求 %@ / %@", @(activeRequest.receivedDataCount), @(attachment.dataTotalCount)); - } + } } - (void)ioFrameChannel:(Lookin_PTChannel*)channel didEndWithError:(NSError*)error { @@ -543,7 +651,7 @@ - (void)ioFrameChannel:(Lookin_PTChannel*)channel didEndWithError:(NSError*)erro } }]; [self.channelWillEnd sendNext:channel]; - + [channel close]; } diff --git a/LookinClient/Static/LKStaticWindowController.m b/LookinClient/Static/LKStaticWindowController.m index de24842a..154e222c 100644 --- a/LookinClient/Static/LKStaticWindowController.m +++ b/LookinClient/Static/LKStaticWindowController.m @@ -58,25 +58,25 @@ - (instancetype)init { window.minSize = NSMakeSize(HierarchyMinWidth + DashboardViewWidth + 200, 500); [window center]; [window setFrameUsingName:LKWindowSizeName_Static]; - + if (self = [self initWithWindow:window]) { [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(_handleInspectingAppDidEnd:) name:LKInspectingAppDidEndNotificationName object:nil]; - + _viewController = [[LKStaticViewController alloc] init]; window.contentView = self.viewController.view; self.contentViewController = self.viewController; - + NSToolbar *toolbar = [[NSToolbar alloc] init]; toolbar.displayMode = NSToolbarDisplayModeIconAndLabel; toolbar.sizeMode = NSToolbarSizeModeRegular; toolbar.delegate = self; window.toolbar = toolbar; - + NSToolbarItem *reloadItem = self.toolbarItemsMap[LKToolBarIdentifier_Reload]; @weakify(self); LKStaticAsyncUpdateManager *updateManager = [LKStaticAsyncUpdateManager sharedInstance]; updateManager.delegate = self; - + [[[RACSignal combineLatest:@[RACObserve(self, isFetchingHierarchy), RACObserve(self, isFetchingDetails)]] distinctUntilChanged] subscribeNext:^(RACTuple * _Nullable x) { @strongify(self); @@ -92,7 +92,7 @@ - (instancetype)init { [[RACObserve(self, isFetchingHierarchy) distinctUntilChanged] subscribeNext:^(NSNumber *x) { reloadItem.enabled = ![x boolValue]; }]; - + [RACObserve([LKStaticHierarchyDataSource sharedInstance], selectedItem) subscribeNext:^(id _Nullable x) { @strongify(self); NSButton *measureButton = (NSButton *)self.toolbarItemsMap[LKToolBarIdentifier_Measure].view; @@ -105,7 +105,7 @@ - (instancetype)init { - (void)popupAllInspectableAppsWithSource:(MenuPopoverAppsListControllerEventSource)source { NSView *appItemView = [self.toolbarItemsMap objectForKey:LKToolBarIdentifier_App].view; - + @weakify(self); [[[[LKAppsManager sharedInstance] fetchAppInfosWithImage:YES localInfos:nil] deliverOnMainThread] subscribeNext:^(NSArray *apps) { @strongify(self); @@ -115,37 +115,37 @@ - (void)popupAllInspectableAppsWithSource:(MenuPopoverAppsListControllerEventSou vc.didSelectApp = ^(LKInspectableApp *app) { @strongify(popover); [popover close]; - + if (app.serverVersionError) { if (app.serverVersionError.code == LookinErrCode_ServerVersionTooLow) { [LKHelper openLookinWebsiteWithPath:@"faq/server-version-too-low/"]; } else { [LKHelper openLookinWebsiteWithPath:@"faq/server-version-too-high/"]; } - + } else { [self.viewController.progressView animateToProgress:InitialIndicatorProgressWhenFetchHierarchy]; - + BOOL isTheSameApp = [[LKAppsManager sharedInstance].inspectingApp.appInfo isEqualToAppInfo:app.appInfo]; - + [[app fetchHierarchyData] subscribeNext:^(LookinHierarchyInfo *info) { [self.viewController.progressView finishWithCompletion:nil]; [LKAppsManager sharedInstance].inspectingApp = app; [[LKStaticHierarchyDataSource sharedInstance] reloadWithHierarchyInfo:info keepState:isTheSameApp]; - + } error:^(NSError * _Nullable error) { AlertError(error, self.window); [self.viewController.progressView resetToZero]; }]; } }; - + popover.behavior = NSPopoverBehaviorTransient; popover.animates = NO; popover.contentSize = vc.bestSize; popover.contentViewController = vc; [popover showRelativeToRect:NSMakeRect(0, 0, appItemView.bounds.size.width, appItemView.bounds.size.height) ofView:appItemView preferredEdge:NSRectEdgeMaxY]; - + } error:^(NSError * _Nullable error) { NSAssert(NO, @"该方法不应该 sendError"); }]; @@ -174,7 +174,7 @@ - (nullable NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:( } item = [[LKWindowToolbarHelper sharedInstance] makeToolBarItemWithIdentifier:itemIdentifier preferenceManager:[LKPreferenceManager mainManager]]; self.toolbarItemsMap[itemIdentifier] = item; - + if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_Reload]) { item.target = self; item.action = @selector(_handleReload); @@ -191,7 +191,7 @@ - (nullable NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:( } else if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_Console]) { item.target = self; item.action = @selector(_handleConsole); - + [[[RACObserve(self.viewController, showConsole) distinctUntilChanged] skip:1] subscribeNext:^(NSNumber *x) { ((NSButton *)item.view).state = x.boolValue ? NSControlStateValueOn : NSControlStateValueOff; }]; @@ -236,42 +236,42 @@ - (void)_handleReload { [[LKStaticAsyncUpdateManager sharedInstance] endUpdating]; return; } - + LKInspectableApp *app = [LKAppsManager sharedInstance].inspectingApp; if (!app) { [self popupAllInspectableAppsWithSource:MenuPopoverAppsListControllerEventSourceReloadButton]; return; } - + if (self.isFetchingHierarchy) { return; } - + self.isFetchingHierarchy = YES; - + [self.viewController.progressView animateToProgress:InitialIndicatorProgressWhenFetchHierarchy]; - + [LKPerformanceReporter.sharedInstance willStartReload]; @weakify(self); [[app fetchHierarchyData] subscribeNext:^(LookinHierarchyInfo *info) { [self.viewController.progressView finishWithCompletion:nil]; [[LKStaticHierarchyDataSource sharedInstance] reloadWithHierarchyInfo:info keepState:YES]; self.isFetchingHierarchy = NO; - + [LKPerformanceReporter.sharedInstance didFetchHierarchy]; - + } error:^(NSError * _Nullable error) { // error @strongify(self); [self.viewController.progressView resetToZero]; self.isFetchingHierarchy = NO; - + [[NSAlert alertWithError:error] beginSheetModalForWindow:self.window completionHandler:nil]; }]; } - (void)_handleApp { - // 停止可能存在的刷新倒计时 + // 停止可能存在的刷新倒计时 [self popupAllInspectableAppsWithSource:MenuPopoverAppsListControllerEventSourceAppButton]; } @@ -295,7 +295,7 @@ - (void)handleFastMode { - (void)_handleMessage:(NSButton *)button { NSMenu *menu = [NSMenu new]; - + NSArray *msgs = [[LKMessageManager sharedInstance] queryMessages]; for (NSString *msg in msgs) { if ([msg isEqualToString:LKMessage_Jobs]) { @@ -310,7 +310,7 @@ - (void)_handleMessage:(NSButton *)button { [menu addItem:[NSMenuItem separatorItem]]; continue; } - + if ([msg isEqualToString:LKMessage_NewServerVersion]) { [menu addItem:({ NSMenuItem *menuItem = [NSMenuItem new]; @@ -337,7 +337,7 @@ - (void)_handleMessage:(NSButton *)button { [menu addItem:[NSMenuItem separatorItem]]; continue; } - + if ([msg isEqualToString:LKMessage_SwiftSubspec]) { [menu addItem:({ NSMenuItem *menuItem = [NSMenuItem new]; @@ -365,9 +365,9 @@ - (void)_handleMessage:(NSButton *)button { menuItem; })]; } - + [NSMenu popUpContextMenu:menu withEvent:[[NSApplication sharedApplication] currentEvent] forView:button]; - + [MSACAnalytics trackEvent:@"Open Notification" withProperties:@{ @"Jobs": [NSString stringWithFormat:@"%@", @([msgs containsObject:LKMessage_Jobs])], @"SwiftSubspec": [NSString stringWithFormat:@"%@", @([msgs containsObject:LKMessage_SwiftSubspec])], @@ -380,9 +380,27 @@ - (void)_handleFreeRotation { [[LKPreferenceManager mainManager].freeRotation setBOOLValue:!boolValue ignoreSubscriber:nil]; } +#pragma mark - Others + +- (void)_showUSBLowSpeedTipsIfNeeded { + if (TutorialMng.hasAlreadyShowedTipsThisLaunch || TutorialMng.USBLowSpeed) { + return; + } + if (!InspectingApp || InspectingApp.appInfo.isWireless || InspectingApp.appInfo.deviceType == LookinAppInfoDeviceSimulator || [LKStaticHierarchyDataSource sharedInstance].flatItems.count < 170) { + return; + } + + TutorialMng.hasAlreadyShowedTipsThisLaunch = YES; + dispatch_async(dispatch_get_main_queue(), ^{ + [[LKTutorialManager sharedInstance] showPopoverOfView:self.toolbarItemsMap[LKToolBarIdentifier_Reload].view text:NSLocalizedString(@"Inspecting via USB is slower than inspecting a Xcode simulator.", nil) learned:^{ + [LKTutorialManager sharedInstance].USBLowSpeed = YES; + }]; + }); +} + #pragma mark - -- (void)appMenuManagerDidSelectReload { +- (void)appMenuManagerDidSelectReload { if (self.isFetchingHierarchy) { return; } @@ -433,7 +451,7 @@ - (void)appMenuManagerDidSelectIncreaseInterspace { [manager.zInterspace setDoubleValue:newValue ignoreSubscriber:nil]; } -- (void)appMenuManagerDidSelectExpansionIndex:(NSUInteger)index { +- (void)appMenuManagerDidSelectExpansionIndex:(NSUInteger)index { [[LKStaticHierarchyDataSource sharedInstance] adjustExpansionByIndex:index referenceDict:nil selectedItem:nil]; if (!TutorialMng.hasAlreadyShowedTipsThisLaunch && !TutorialMng.quickSelection && index <= 1) { @@ -444,10 +462,10 @@ - (void)appMenuManagerDidSelectExpansionIndex:(NSUInteger)index { - (void)appMenuManagerDidSelectExport { LKExportManager *exportManager = [LKExportManager sharedInstance]; LookinHierarchyInfo *hierarchyInfo = [LKStaticHierarchyDataSource sharedInstance].rawHierarchyInfo; - + __block NSString *fileName; __block NSData *exportedData = nil; - + LKExportAccessoryView *accessoryView = [LKExportAccessoryView new]; $(accessoryView).sizeToFit; [RACObserve([LKPreferenceManager mainManager], preferredExportCompression) subscribeNext:^(NSNumber *num) { @@ -455,7 +473,7 @@ - (void)appMenuManagerDidSelectExport { exportedData = [exportManager dataFromHierarchyInfo:hierarchyInfo imageCompression:compression fileName:&fileName]; accessoryView.dataSize = exportedData.length; }]; - + NSSavePanel *panel = [NSSavePanel savePanel]; panel.accessoryView = accessoryView; [panel setNameFieldStringValue:fileName]; @@ -477,7 +495,7 @@ - (void)appMenuManagerDidSelectExport { } } }]; - + [MSACAnalytics trackEvent:@"Export Document"]; } @@ -487,7 +505,7 @@ - (void)appMenuManagerDidSelectOpenInNewWindow { file.serverVersion = newHierarchyInfo.serverVersion; file.hierarchyInfo = newHierarchyInfo; [[LKNavigationManager sharedInstance] showReaderWithHierarchyFile:file title:nil]; - + [MSACAnalytics trackEvent:@"Open New Window"]; } @@ -514,28 +532,28 @@ - (void)handleTurnOnSwift { - (void)detailUpdateTasksTotalCount:(NSUInteger)totalCount finishedCount:(NSUInteger)finishedCount { NSToolbarItem *reloadItem = self.toolbarItemsMap[LKToolBarIdentifier_Reload]; NSButton *reloadButton = (NSButton *)reloadItem.view; - + BOOL isFetching = (totalCount > finishedCount); - + if (isFetching) { if (self.isFetchingDetails) { // 继续维持 fetch 状态 } else { // 进入 fetch 状态 self.isFetchingDetails = YES; - + NSImage *image = NSImageMake(@"icon_stop"); image.template = YES; reloadButton.image = image; } reloadItem.label = [NSString stringWithFormat:@"%@ / %@", @(finishedCount), @(totalCount)]; - + } else { if (self.isFetchingDetails) { // 退出 fetch 状态 self.isFetchingDetails = NO; reloadItem.label = NSLocalizedString(@"Reload", nil); - + NSImage *image = NSImageMake(@"icon_reload"); image.template = YES; reloadButton.image = image; diff --git a/Podfile b/Podfile index 792f5a3c..7953e06c 100644 --- a/Podfile +++ b/Podfile @@ -2,12 +2,12 @@ use_frameworks! #inhibit_all_warnings! -target 'LookinClient' do +target 'LookinClient' do platform :osx, '11.0' pod 'AppCenter' pod 'ReactiveObjC', '3.1.0' pod 'Sparkle', '~> 1.0' - pod 'LookinShared', :git=>'https://github.com/nova286/LookinServer.git', :commit => '6a21883a8eb18997d6c6c9ec8ae3ad25739aca7e' + pod 'LookinShared/Wireless', :git=>'https://github.com/nova286/LookinServer.git', :commit => '6ba8231d589c7404064ca8d571c49746a9f67366' #pod 'LookinShared', :path=>'../LookinServer/' end diff --git a/Podfile.lock b/Podfile.lock index f3f0da76..17d6385c 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -7,38 +7,42 @@ PODS: - AppCenter/Core (4.4.2) - AppCenter/Crashes (4.4.2): - AppCenter/Core - - LookinShared (1.2.8) + - CocoaAsyncSocket (7.6.5) + - LookinShared/Wireless (1.2.8): + - CocoaAsyncSocket - ReactiveObjC (3.1.0) - Sparkle (1.27.1) DEPENDENCIES: - AppCenter - - LookinShared (from `https://github.com/nova286/LookinServer.git`, commit `6a21883a8eb18997d6c6c9ec8ae3ad25739aca7e`) + - LookinShared/Wireless (from `https://github.com/nova286/LookinServer.git`, commit `6ba8231d589c7404064ca8d571c49746a9f67366`) - ReactiveObjC (= 3.1.0) - Sparkle (~> 1.0) SPEC REPOS: trunk: - AppCenter + - CocoaAsyncSocket - ReactiveObjC - Sparkle EXTERNAL SOURCES: LookinShared: - :commit: 6a21883a8eb18997d6c6c9ec8ae3ad25739aca7e + :commit: 6ba8231d589c7404064ca8d571c49746a9f67366 :git: https://github.com/nova286/LookinServer.git CHECKOUT OPTIONS: LookinShared: - :commit: 6a21883a8eb18997d6c6c9ec8ae3ad25739aca7e + :commit: 6ba8231d589c7404064ca8d571c49746a9f67366 :git: https://github.com/nova286/LookinServer.git SPEC CHECKSUMS: AppCenter: b0eca112a27b71e97488ffa1949ee38c7abd4b79 - LookinShared: e5ca2b2b0758a1caafffc29cc12764ff92fb419b + CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 + LookinShared: 6a5ea30b57825f5d8619726ffc72812b4c025771 ReactiveObjC: 2a38ea15335de4119d8b17caf1db1484f61db902 Sparkle: 23f98b268284c8c03e6228230fc8f1807ef041d5 -PODFILE CHECKSUM: 3a6d485c3416742e8189e376785f1d4e1107dccb +PODFILE CHECKSUM: 766ca5f4a0c237b7925e65ee52b8c4eb9c845837 COCOAPODS: 1.17.0 From 1ba2c99033da090e0698eef32fdfb95c3b5e1b74 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Thu, 27 Jul 2023 12:41:30 +0800 Subject: [PATCH 2/8] Fix Bug: Wireless Connection --- LookinClient/Connection/LKConnectionManager.m | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/LookinClient/Connection/LKConnectionManager.m b/LookinClient/Connection/LKConnectionManager.m index 5037b621..9e1cfe95 100644 --- a/LookinClient/Connection/LKConnectionManager.m +++ b/LookinClient/Connection/LKConnectionManager.m @@ -491,10 +491,14 @@ - (void)_startListeningForWirelessDevices { self.wirelessChannel.deviceBlock = ^(ECOChannelDeviceInfo *device, BOOL isConnected) { NSLog(@"🚀 Lookin deviceBlock device:%@", device); if (isConnected && ![self_weak_.allWirelessDevices containsObject:device]) { - NSString *uniId = [NSString stringWithFormat:@"%@_%@",device.uuid, device.appInfo.appId]; - [self_weak_.wirelessChannel sendAuthorizationMessageToDevice:device - state:ECOAuthorizeResponseType_AllowAlways - showAuthAlert:![self_weak_.wirelessChannel.whitelistDevices containsObject:uniId]]; + if (!device.authorizedType) { + NSString *uniId = [NSString stringWithFormat:@"%@_%@",device.uuid, device.appInfo.appId]; + [self_weak_.wirelessChannel sendAuthorizationMessageToDevice:device + state:ECOAuthorizeResponseType_AllowAlways + showAuthAlert:![self_weak_.wirelessChannel.whitelistDevices containsObject:uniId]]; + } else { + [self_weak_ _connectToWirelessDevice:device]; + } } else if (!isConnected) { [self_weak_.allWirelessDevices removeObject:device]; [self_weak_.channelWillEnd sendNext:device]; @@ -504,17 +508,7 @@ - (void)_startListeningForWirelessDevices { self.wirelessChannel.authStateChangedBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { NSLog(@"🚀 Lookin authStateChangedBlock device:%@", device); if (authState) { - if (![self_weak_.allWirelessDevices containsObject:device]) { - // Ping测试 - [self_weak_ _requestWithType:LookinRequestTypePing channel:device data:nil timeoutInterval:2 succ:^(LookinConnectionResponseAttachment *pingResponse) { - // ping 成功了 - // NSLog(@"LookinClient, level1 - ping succ, will send request:%@, port:%@", @(type), @(channel.portNumber)); - - [self_weak_.allWirelessDevices addObject:device]; - } fail:^(NSError *error) { - // ping 失败了 - } completion:nil]; - } + [self_weak_ _connectToWirelessDevice:device]; } else if ([self_weak_.allWirelessDevices containsObject:device]) { [self_weak_.allWirelessDevices removeObject:device]; [self_weak_.channelWillEnd sendNext:device]; @@ -526,6 +520,21 @@ - (void)_startListeningForWirelessDevices { }; } +- (void)_connectToWirelessDevice:(ECOChannelDeviceInfo *)device { + if (device.isConnected && ![self.allWirelessDevices containsObject:device]) { + // Ping测试 + @weakify(self); + [self _requestWithType:LookinRequestTypePing channel:device data:nil timeoutInterval:2 succ:^(LookinConnectionResponseAttachment *pingResponse) { + // ping 成功了 + // NSLog(@"LookinClient, level1 - ping succ, will send request:%@, port:%@", @(type), @(channel.portNumber)); + + [self_weak_.allWirelessDevices addObject:device]; + } fail:^(NSError *error) { + // ping 失败了 + } completion:nil]; + } +} + #pragma mark - - (BOOL)ioFrameChannel:(Lookin_PTChannel*)channel shouldAcceptFrameOfType:(uint32_t)type tag:(uint32_t)tag payloadSize:(uint32_t)payloadSize { From f20816d510a75126f3dce631c096a35317b946a4 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Thu, 27 Jul 2023 17:27:41 +0800 Subject: [PATCH 3/8] Fix Crash: Wireless Connection --- LookinClient/Toolbar/LKWindowToolbarAppButton.m | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/LookinClient/Toolbar/LKWindowToolbarAppButton.m b/LookinClient/Toolbar/LKWindowToolbarAppButton.m index 93e9ab52..99e96872 100644 --- a/LookinClient/Toolbar/LKWindowToolbarAppButton.m +++ b/LookinClient/Toolbar/LKWindowToolbarAppButton.m @@ -66,6 +66,11 @@ - (void)layout { } - (void)setAppInfo:(LookinAppInfo *)appInfo { + if (!NSThread.isMainThread) { + [self performSelectorOnMainThread:_cmd withObject:appInfo waitUntilDone:NO]; + return; + } + _appInfo = appInfo; if (appInfo) { From f639f2d874d27aa1850c69766aedee5063122a54 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Mon, 13 Nov 2023 15:00:18 +0800 Subject: [PATCH 4/8] Fix Build Bug --- Lookin.xcodeproj/project.pbxproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lookin.xcodeproj/project.pbxproj b/Lookin.xcodeproj/project.pbxproj index 7eba9b23..8d3bef5a 100644 --- a/Lookin.xcodeproj/project.pbxproj +++ b/Lookin.xcodeproj/project.pbxproj @@ -1643,6 +1643,7 @@ "$(inherited)", "COCOAPODS=1", "SHOULD_COMPILE_LOOKIN_SERVER=1", + "LOOKIN_SERVER_WIRELESS=1", ); INFOPLIST_FILE = LookinClient/LookinClient_Info.plist; LD_RUNPATH_SEARCH_PATHS = ( @@ -1684,6 +1685,7 @@ "$(inherited)", "COCOAPODS=1", "SHOULD_COMPILE_LOOKIN_SERVER=1", + "LOOKIN_SERVER_WIRELESS=1", ); INFOPLIST_FILE = LookinClient/LookinClient_Info.plist; LD_RUNPATH_SEARCH_PATHS = ( From 3e10f7b5fb657eedcfddeb22b5af4ad9bb6ca0f2 Mon Sep 17 00:00:00 2001 From: mlch911 Date: Fri, 23 Aug 2024 15:32:46 +0800 Subject: [PATCH 5/8] =?UTF-8?q?=E6=94=AF=E6=8C=81=E6=97=A0=E7=BA=BF?= =?UTF-8?q?=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Lookin.xcodeproj/project.pbxproj | 18 +- LookinClient/Connection/LKConnectionManager.h | 8 + LookinClient/Connection/LKConnectionManager.m | 184 ++++++++++++++---- LookinClient/Launch/LKLaunchViewController.m | 125 +++++++++--- .../Launch/LKLaunchWirelessDeviceView.h | 16 ++ .../Launch/LKLaunchWirelessDeviceView.m | 160 +++++++++++++++ .../Static/LKStaticWindowController.m | 103 +++++++--- ...MenuPopoverWirelessDevicesListController.h | 22 +++ ...MenuPopoverWirelessDevicesListController.m | 152 +++++++++++++++ LookinClient/Toolbar/LKWindowToolbarHelper.h | 1 + LookinClient/Toolbar/LKWindowToolbarHelper.m | 97 +++++---- LookinClient/en.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + 13 files changed, 754 insertions(+), 136 deletions(-) create mode 100644 LookinClient/Launch/LKLaunchWirelessDeviceView.h create mode 100644 LookinClient/Launch/LKLaunchWirelessDeviceView.m create mode 100644 LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.h create mode 100644 LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.m diff --git a/Lookin.xcodeproj/project.pbxproj b/Lookin.xcodeproj/project.pbxproj index 8d3bef5a..00367d5e 100644 --- a/Lookin.xcodeproj/project.pbxproj +++ b/Lookin.xcodeproj/project.pbxproj @@ -181,6 +181,8 @@ D4224EEF2358ED3400ED9626 /* LKMeasureController.m in Sources */ = {isa = PBXBuildFile; fileRef = D4224EEE2358ED3400ED9626 /* LKMeasureController.m */; }; D4D1B2D32353975A002A5071 /* LKWindowToolbarScaleView.m in Sources */ = {isa = PBXBuildFile; fileRef = D4D1B2D22353975A002A5071 /* LKWindowToolbarScaleView.m */; }; D4E1FD66236169130070DFB8 /* LKMeasureResultLineData.m in Sources */ = {isa = PBXBuildFile; fileRef = D4E1FD65236169130070DFB8 /* LKMeasureResultLineData.m */; }; + F7C2F32F2B02222C003AD0E7 /* LKMenuPopoverWirelessDevicesListController.m in Sources */ = {isa = PBXBuildFile; fileRef = F7C2F32E2B02222C003AD0E7 /* LKMenuPopoverWirelessDevicesListController.m */; }; + F7C2F3322B02348F003AD0E7 /* LKLaunchWirelessDeviceView.m in Sources */ = {isa = PBXBuildFile; fileRef = F7C2F3312B02348F003AD0E7 /* LKLaunchWirelessDeviceView.m */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -567,6 +569,10 @@ D4E1FD64236169130070DFB8 /* LKMeasureResultLineData.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LKMeasureResultLineData.h; sourceTree = ""; }; D4E1FD65236169130070DFB8 /* LKMeasureResultLineData.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LKMeasureResultLineData.m; sourceTree = ""; }; D97552B9BF9170E090F8CAA7 /* Pods-LookinClient.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-LookinClient.debug.xcconfig"; path = "Target Support Files/Pods-LookinClient/Pods-LookinClient.debug.xcconfig"; sourceTree = ""; }; + F7C2F32D2B02222C003AD0E7 /* LKMenuPopoverWirelessDevicesListController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LKMenuPopoverWirelessDevicesListController.h; sourceTree = ""; }; + F7C2F32E2B02222C003AD0E7 /* LKMenuPopoverWirelessDevicesListController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LKMenuPopoverWirelessDevicesListController.m; sourceTree = ""; }; + F7C2F3302B02348F003AD0E7 /* LKLaunchWirelessDeviceView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LKLaunchWirelessDeviceView.h; sourceTree = ""; }; + F7C2F3312B02348F003AD0E7 /* LKLaunchWirelessDeviceView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = LKLaunchWirelessDeviceView.m; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -1078,10 +1084,12 @@ D4D1B2D22353975A002A5071 /* LKWindowToolbarScaleView.m */, A2C71CBC282165DB0056D991 /* LKWindowToolbarAppButton.h */, A2C71CBD282165DB0056D991 /* LKWindowToolbarAppButton.m */, + AAA8C10521CC17680017A345 /* LKMenuPopoverAppsListController.h */, AAA8C0F821CC17680017A345 /* LKMenuPopoverAppsListController.m */, AA049A1521E61B85008C6366 /* LKMenuPopoverSettingController.h */, AA049A1621E61B85008C6366 /* LKMenuPopoverSettingController.m */, - AAA8C10521CC17680017A345 /* LKMenuPopoverAppsListController.h */, + F7C2F32D2B02222C003AD0E7 /* LKMenuPopoverWirelessDevicesListController.h */, + F7C2F32E2B02222C003AD0E7 /* LKMenuPopoverWirelessDevicesListController.m */, ); path = Toolbar; sourceTree = ""; @@ -1106,10 +1114,12 @@ AAA8C11421CC17680017A345 /* Launch */ = { isa = PBXGroup; children = ( - AAA8C11821CC17680017A345 /* LKLaunchAppView.m */, AAA8C11521CC17680017A345 /* LKLaunchAppView.h */, - AAA8C11621CC17680017A345 /* LKLaunchViewController.m */, + AAA8C11821CC17680017A345 /* LKLaunchAppView.m */, + F7C2F3302B02348F003AD0E7 /* LKLaunchWirelessDeviceView.h */, + F7C2F3312B02348F003AD0E7 /* LKLaunchWirelessDeviceView.m */, AAA8C11921CC17680017A345 /* LKLaunchViewController.h */, + AAA8C11621CC17680017A345 /* LKLaunchViewController.m */, AAA8C11721CC17680017A345 /* LKLaunchWindowController.h */, AAA8C11A21CC17680017A345 /* LKLaunchWindowController.m */, ); @@ -1421,6 +1431,7 @@ AAE61CCE22A5063F00B3FB9F /* LKInputSearchSuggestionsRowView.m in Sources */, A74C63072B540FDD0068FBB8 /* CwlDemangle.swift in Sources */, AAA8C12621CC17680017A345 /* LKProgressIndicatorView.m in Sources */, + F7C2F32F2B02222C003AD0E7 /* LKMenuPopoverWirelessDevicesListController.m in Sources */, A74C63122B5429B60068FBB8 /* LookinAutoLayoutConstraint+LookinClient.m in Sources */, AAA8C12A21CC17680017A345 /* LKBaseControl.m in Sources */, AACBACAF22AE983F00A8F89D /* LKDashboardAttributeRectView.m in Sources */, @@ -1435,6 +1446,7 @@ A78FCEB42B334D3300F1D307 /* LKDanceUIAttrMaker.m in Sources */, AAB65C092320FC30001E4299 /* LKDashboardSearchCardView.m in Sources */, AA290358229824A400073B65 /* LKPanelContentView.m in Sources */, + F7C2F3322B02348F003AD0E7 /* LKLaunchWirelessDeviceView.m in Sources */, A74C630F2B54202E0068FBB8 /* LookinObject+LookinClient.m in Sources */, AAA8C15E21CC17680017A345 /* LKLaunchViewController.m in Sources */, AA7F157822C0F2B9004436AD /* LKConnectionRequest.m in Sources */, diff --git a/LookinClient/Connection/LKConnectionManager.h b/LookinClient/Connection/LKConnectionManager.h index f7453d00..017bfeec 100644 --- a/LookinClient/Connection/LKConnectionManager.h +++ b/LookinClient/Connection/LKConnectionManager.h @@ -42,6 +42,14 @@ /// 在调用该方法发请求时,如果已有相同 type 的旧 request 尚未返回结果,则之前的旧 request 会被报告 Error,然后被丢弃 - (RACSignal *)requestWithType:(unsigned int)requestType data:(NSObject *)requestData channel:(Lookin_PTChannel *)channel; +- (RACSignal *)connectToWireless:(ECOChannelDeviceInfo *)device; + +- (NSArray *)getAllWirelessDevices; + +- (BOOL)isWhiteListDevice:(ECOChannelDeviceInfo *)device; + +- (void)setWhiteListDevice:(ECOChannelDeviceInfo *)device white:(BOOL)white; + /// 取消先前使用 requestWithType:data:channel: 方法发送的尚未完成的 request,这个 request 会被报告为 completion - (void)cancelRequestWithType:(unsigned int)requestType channel:(Lookin_PTChannel *)channel; diff --git a/LookinClient/Connection/LKConnectionManager.m b/LookinClient/Connection/LKConnectionManager.m index 9e1cfe95..fdf51c06 100644 --- a/LookinClient/Connection/LKConnectionManager.m +++ b/LookinClient/Connection/LKConnectionManager.m @@ -26,6 +26,8 @@ return list; } +static NSString *const LKWhiteListDevicesKey = @"LKWhiteListDevicesKey"; + @implementation Lookin_PTChannel (LKConnection) - (void)setActiveRequests:(NSMutableSet *)activeRequests { @@ -88,8 +90,12 @@ @interface LKConnectionManager () @property(nonatomic, copy) NSArray *allSimulatorPorts; @property(nonatomic, strong) NSMutableArray *allUSBPorts; -@property(nonatomic, strong) NSMutableArray *allWirelessDevices; +@property(nonatomic, strong) NSMutableArray *connectWirelessDevices; +@property(nonatomic, strong) NSMutableArray *notConnectWirelessDevices; @property(nonatomic, strong) ECOChannelManager *wirelessChannel; +@property(nonatomic, strong) NSMutableDictionary *authStateChangedBlocks; + +@property(nonatomic, strong) NSMutableArray *whitelistDevices; @end @@ -123,7 +129,9 @@ - (instancetype)init { ports; }); self.allUSBPorts = [NSMutableArray array]; - self.allWirelessDevices = [NSMutableArray array]; + self.connectWirelessDevices = [NSMutableArray array]; + self.notConnectWirelessDevices = [NSMutableArray array]; + self.authStateChangedBlocks = [NSMutableDictionary dictionary]; [self _startListeningForWirelessDevices]; [self _startListeningForUSBDevices]; @@ -133,12 +141,38 @@ - (instancetype)init { return self; } +- (NSArray *)getAllWirelessDevices { + return [self.connectWirelessDevices.copy arrayByAddingObjectsFromArray:self.notConnectWirelessDevices.copy]; +} + +- (RACSignal *)connectToWireless:(ECOChannelDeviceInfo *)device { + return [self _tryToConnectToWirelessDevice:device]; +} + +- (BOOL)isWhiteListDevice:(ECOChannelDeviceInfo *)device { + return [self.whitelistDevices containsObject:device]; +} + +- (void)setWhiteListDevice:(ECOChannelDeviceInfo *)device white:(BOOL)white { + if (white) { + if (![self isWhiteListDevice:device]) { + [self.whitelistDevices addObject:device]; + [self saveWhiteListDevices]; + } + } else { + if ([self isWhiteListDevice:device]) { + [self.whitelistDevices removeObject:device]; + [self saveWhiteListDevices]; + } + } +} + #pragma mark - Ports Connect - (RACSignal *)tryToConnectAllPorts { return [[RACSignal zip:@[[self _tryToConnectAllSimulatorPorts], [self _tryToConnectAllUSBDevices], - [self _tryToConnectToWirelessDevice]]] map:^id _Nullable(RACTuple * _Nullable value) { + [self _tryToConnectToAllWirelessDevice]]] map:^id _Nullable(RACTuple * _Nullable value) { RACTupleUnpack(NSArray *simulatorChannels, NSArray *usbChannels, NSArray *wirelessDevices) = value; NSArray *connectedChannels = [[simulatorChannels arrayByAddingObjectsFromArray:usbChannels] arrayByAddingObjectsFromArray:wirelessDevices]; return connectedChannels; @@ -240,17 +274,14 @@ - (RACSignal *)_connectToUSBPort:(LKUSBConnectionPort *)port { }]; } -- (RACSignal *)_tryToConnectToWirelessDevice { - if (self.allWirelessDevices.count) { - NSArray *devices = [self.allWirelessDevices lookin_filter:^BOOL(ECOChannelDeviceInfo *obj) { - return obj.isConnected; - }]; - if (devices.count != self.allWirelessDevices.count) { - self.allWirelessDevices = [NSMutableArray arrayWithArray:devices]; - } - return [RACSignal return:devices]; +- (RACSignal *)_tryToConnectToAllWirelessDevice { + NSArray *devices = [self.connectWirelessDevices lookin_filter:^BOOL(ECOChannelDeviceInfo *obj) { + return obj.isConnected; + }]; + if (devices.count != self.connectWirelessDevices.count) { + self.connectWirelessDevices = [NSMutableArray arrayWithArray:devices]; } - return [RACSignal return:@[]]; + return [RACSignal return:devices]; } #pragma mark - Request @@ -490,27 +521,36 @@ - (void)_startListeningForWirelessDevices { // 设备连接变更 self.wirelessChannel.deviceBlock = ^(ECOChannelDeviceInfo *device, BOOL isConnected) { NSLog(@"🚀 Lookin deviceBlock device:%@", device); - if (isConnected && ![self_weak_.allWirelessDevices containsObject:device]) { - if (!device.authorizedType) { - NSString *uniId = [NSString stringWithFormat:@"%@_%@",device.uuid, device.appInfo.appId]; - [self_weak_.wirelessChannel sendAuthorizationMessageToDevice:device - state:ECOAuthorizeResponseType_AllowAlways - showAuthAlert:![self_weak_.wirelessChannel.whitelistDevices containsObject:uniId]]; + if (isConnected && ![self_weak_.connectWirelessDevices containsObject:device]) { + if (!device.authorizedType && ![self_weak_ isWhiteListDevice:device]) { + if (![self_weak_.notConnectWirelessDevices containsObject:device]) { + [self_weak_.notConnectWirelessDevices addObject:device]; + } } else { - [self_weak_ _connectToWirelessDevice:device]; + RACSignal *signal = [self_weak_ _connectToWirelessDevice:device]; + [signal subscribeNext:^(ECOChannelDeviceInfo * _Nullable x) { + NSLog(@"%@", x); + }]; } } else if (!isConnected) { - [self_weak_.allWirelessDevices removeObject:device]; + [self_weak_.notConnectWirelessDevices removeObject:device]; + [self_weak_.connectWirelessDevices removeObject:device]; [self_weak_.channelWillEnd sendNext:device]; } }; // 授权状态变更回调 self.wirelessChannel.authStateChangedBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { NSLog(@"🚀 Lookin authStateChangedBlock device:%@", device); - if (authState) { - [self_weak_ _connectToWirelessDevice:device]; - } else if ([self_weak_.allWirelessDevices containsObject:device]) { - [self_weak_.allWirelessDevices removeObject:device]; + void(^block)(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) = self_weak_.authStateChangedBlocks[@(device.hash)]; + if (block) { + block(device, authState); + } else if (authState && [self_weak_ isWhiteListDevice:device] && ![self_weak_.connectWirelessDevices containsObject:device]) { + [[self_weak_ _tryToConnectToWirelessDevice:device] subscribeNext:^(ECOChannelDeviceInfo * _Nullable x) { + NSLog(@"🚀 Lookin auto connect white list device success. device:%@", x); + }]; + } + if (!authState && [self_weak_.connectWirelessDevices containsObject:device]) { + [self_weak_.connectWirelessDevices removeObject:device]; [self_weak_.channelWillEnd sendNext:device]; } }; @@ -520,21 +560,68 @@ - (void)_startListeningForWirelessDevices { }; } -- (void)_connectToWirelessDevice:(ECOChannelDeviceInfo *)device { - if (device.isConnected && ![self.allWirelessDevices containsObject:device]) { - // Ping测试 +- (RACSignal *)_tryToConnectToWirelessDevice:(ECOChannelDeviceInfo *)device { + if (!device.isConnected) + return [RACSignal error:LookinErr_Inner]; + + if (!device.authorizedType) { + // 未信任 @weakify(self); - [self _requestWithType:LookinRequestTypePing channel:device data:nil timeoutInterval:2 succ:^(LookinConnectionResponseAttachment *pingResponse) { - // ping 成功了 - // NSLog(@"LookinClient, level1 - ping succ, will send request:%@, port:%@", @(type), @(channel.portNumber)); - - [self_weak_.allWirelessDevices addObject:device]; - } fail:^(NSError *error) { - // ping 失败了 - } completion:nil]; + RACSignal *signal = [[RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) { + self_weak_.authStateChangedBlocks[@(device.hash)] = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { + if (authState) { + [subscriber sendNext:device]; + [subscriber sendCompleted]; + NSLog(@"🚀 Lookin connect device success. device:%@", device); + } else { + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless Connection rejected", nil)}]; + NSLog(@"🚀 Lookin connect reject. device:%@", device); + dispatch_async(dispatch_get_main_queue(), ^{ + AlertErrorText(NSLocalizedString(@"Wireless Connections", nil), NSLocalizedString(@"Wireless Connection rejected", nil), CurrentKeyWindow); + }); + [subscriber sendError:error]; + } + }; + return nil; + }] flattenMap:^__kindof RACSignal * _Nullable(ECOChannelDeviceInfo *device) { + return [self_weak_ _connectToWirelessDevice:device]; + }]; + NSString *uniId = [NSString stringWithFormat:@"%@_%@",device.uuid, device.appInfo.appId]; + [self.wirelessChannel sendAuthorizationMessageToDevice:device + state:ECOAuthorizeResponseType_AllowAlways + showAuthAlert:![self.wirelessChannel.whitelistDevices containsObject:uniId]]; + return signal; + } else { + return [self _connectToWirelessDevice:device]; } } +- (RACSignal *)_connectToWirelessDevice:(ECOChannelDeviceInfo *)device { + if (!device.isConnected) + return [RACSignal error:LookinErr_Inner]; + + if (![self.connectWirelessDevices containsObject:device]) { + // 已信任 + @weakify(self); + return [RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) { + // Ping测试 + [self _requestWithType:LookinRequestTypePing channel:device data:nil timeoutInterval:2 succ:^(LookinConnectionResponseAttachment *pingResponse) { + // ping 成功了 + // NSLog(@"LookinClient, level1 - ping succ, will send request:%@, port:%@", @(type), @(channel.portNumber)); + + [self_weak_.connectWirelessDevices addObject:device]; + [self_weak_.notConnectWirelessDevices removeObject:device]; + [subscriber sendNext:device]; + [subscriber sendCompleted]; + } fail:^(NSError *error) { + [subscriber sendError:error]; + } completion:nil]; + return nil; + }]; + } + return [RACSignal error:LookinErr_Inner]; +} + #pragma mark - - (BOOL)ioFrameChannel:(Lookin_PTChannel*)channel shouldAcceptFrameOfType:(uint32_t)type tag:(uint32_t)tag payloadSize:(uint32_t)payloadSize { @@ -664,4 +751,29 @@ - (void)ioFrameChannel:(Lookin_PTChannel*)channel didEndWithError:(NSError*)erro [channel close]; } +- (void)saveWhiteListDevices { + NSArray *list = [self.whitelistDevices copy]; + list = [list.rac_sequence map:^id _Nullable(ECOChannelDeviceInfo *device) { + return device.toJSONObject; + }].array; + [[NSUserDefaults standardUserDefaults] setObject:list forKey:LKWhiteListDevicesKey]; + [[NSUserDefaults standardUserDefaults] synchronize]; +} + +- (NSMutableArray *)whitelistDevices { + if (!_whitelistDevices) { + NSArray *list = [[NSUserDefaults standardUserDefaults] objectForKey:LKWhiteListDevicesKey]; + list = [list.rac_sequence map:^id _Nullable(NSDictionary *dic) { + NSError *error = nil; + NSData *data = [NSJSONSerialization dataWithJSONObject:dic options:0 error:&error]; + if (error) { + return nil; + } + return [[ECOChannelDeviceInfo alloc] initWithData:data]; + }].array; + _whitelistDevices = [NSMutableArray arrayWithArray:list ?: @[]]; + } + return _whitelistDevices; +} + @end diff --git a/LookinClient/Launch/LKLaunchViewController.m b/LookinClient/Launch/LKLaunchViewController.m index 43ea1db2..601b72bc 100644 --- a/LookinClient/Launch/LKLaunchViewController.m +++ b/LookinClient/Launch/LKLaunchViewController.m @@ -16,6 +16,8 @@ #import "LKPreferenceManager.h" #import "LKTextControl.h" #import "LKPerformanceReporter.h" +#import "LKConnectionManager.h" +#import "LKMenuPopoverWirelessDevicesListController.h" @interface LKLaunchViewController () @@ -25,6 +27,7 @@ @interface LKLaunchViewController () @property(nonatomic, strong) NSProgressIndicator *reloadingIndicator; @property(nonatomic, strong) LKLabel *noAppsTitleLabel; @property(nonatomic, strong) LKTextControl *tutorialControl; +@property(nonatomic, strong) LKTextControl *wirelessControl; @property(nonatomic, copy) void (^crashBlock)(void); @property(nonatomic, assign) BOOL isEnteringApp; @@ -46,15 +49,15 @@ - (instancetype)initWithWindow:(NSWindow *)window { - (NSView *)makeContainerView { _appViewInterSpace = 10; - _contentHeight = 400; + _contentHeight = 440; _contentHorInset = 30; - + LKVisualEffectView *containerView = [LKVisualEffectView new]; containerView.blendingMode = NSVisualEffectBlendingModeBehindWindow; containerView.state = NSVisualEffectStateActive; - + self.appViews = [NSArray array]; - + self.tutorialControl = [LKTextControl new]; self.tutorialControl.layer.cornerRadius = 4; self.tutorialControl.label.stringValue = NSLocalizedString(@"Can't see your app ?", nil); @@ -63,23 +66,34 @@ - (NSView *)makeContainerView { self.tutorialControl.adjustAlphaWhenClick = YES; [self.tutorialControl addTarget:self clickAction:@selector(_handleTutorial)]; [containerView addSubview:self.tutorialControl]; - + self.bottomIndicatorView = [LKProgressIndicatorView new]; [containerView addSubview:self.bottomIndicatorView]; - + [self.bottomIndicatorView animateToProgress:.7 duration:0.5]; - + + self.wirelessControl = [LKTextControl new]; + self.wirelessControl.layer.cornerRadius = 4; + self.wirelessControl.layer.borderWidth = 1; + self.wirelessControl.layer.borderColor = NSColor.grayColor.CGColor; + self.wirelessControl.label.stringValue = [NSString stringWithFormat:@"ᯤ\n%@", NSLocalizedString(@"Wireless Connections", nil)]; + self.wirelessControl.label.textColor = [NSColor linkColor]; + self.wirelessControl.label.font = NSFontMake(12); + self.wirelessControl.adjustAlphaWhenClick = YES; + [self.wirelessControl addTarget:self clickAction:@selector(_handleWireless)]; + [containerView addSubview:self.wirelessControl]; + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(.2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ // 延时一下,因为如果有 USB 设备被连接,则需要一定时间来使 ConnectionManager 检测到,这个时间实际实验大概 0.1 秒,这里稍微宽裕一点给个 0.2 秒 [self _reloadWithAutoEntering:YES]; }); - + return containerView; } - (void)viewDidLayout { [super viewDidLayout]; - + if (self.reloadingIndicator && self.noAppsTitleLabel) { [self.reloadingIndicator sizeToFit]; $(self.noAppsTitleLabel).sizeToFit.x(self.reloadingIndicator.$maxX + 5).midY(self.reloadingIndicator.$midY); @@ -87,13 +101,15 @@ - (void)viewDidLayout { } $(self.tutorialControl).sizeToFit.horAlign.offsetX(3).bottom(14); - + + $(self.wirelessControl).sizeToFit.horAlign.offsetX(3).bottom(40); + __block CGFloat posX = _contentHorInset; [self.appViews enumerateObjectsUsingBlock:^(LKLaunchAppView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { $(obj).sizeToFit.x(posX).y(30); posX = obj.$maxX + self->_appViewInterSpace; }]; - + $(_bottomIndicatorView).fullWidth.height(3).bottom(0); } @@ -103,15 +119,15 @@ - (void)_reloadWithAutoEntering:(BOOL)autoEnter { if (self.isEnteringApp) { return; } - + @weakify(self); [[[[LKAppsManager sharedInstance] fetchAppInfosWithImage:YES localInfos:self.appInfos] deliverOnMainThread] subscribeNext:^(NSArray *apps) { @strongify(self); - + self.appInfos = [apps lookin_map:^id(NSUInteger idx, LKInspectableApp *value) { return value.appInfo; }]; - + if (autoEnter && apps.count == 1 && !apps.firstObject.serverVersionError) { // 进入页面时自动触发的 refetch,并且只有一个 app,则自动拉取 hierarchy [self _renderWithApps:apps]; @@ -125,9 +141,11 @@ - (void)_reloadWithAutoEntering:(BOOL)autoEnter { [self _renderWithApps:apps]; // 每 1.5 秒刷新一次 - dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - [self _reloadWithAutoEntering:NO]; - }); + + [self performSelector:_cmd withObject:nil afterDelay:1.5]; +// dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ +// [self _reloadWithAutoEntering:NO]; +// }); } }]; } @@ -142,10 +160,10 @@ - (void)_handleClickAppView:(LKLaunchAppView *)view { } else { [LKHelper openLookinWebsiteWithPath:@"faq/server-version-too-high/"]; } - + } else { [self.bottomIndicatorView animateToProgress:.8 duration:1]; - + LKInspectableApp *app = view.app; [self _enterApp:app]; } @@ -154,10 +172,10 @@ - (void)_handleClickAppView:(LKLaunchAppView *)view { - (void)_renderWithApps:(NSArray *)apps { if (!apps || apps.count == 0) { [self.window setContentSize:NSMakeSize(256, _contentHeight)]; - + [self showNoAppsView]; $(self.appViews).hide; - + } else { __block CGFloat windowWidth = _contentHorInset * 2 + (apps.count - 1) * _appViewInterSpace; self.appViews = [self.appViews lookin_resizeWithCount:apps.count add:^LKLaunchAppView *(NSUInteger idx) { @@ -172,12 +190,12 @@ - (void)_renderWithApps:(NSArray *)apps { windowWidth += [obj sizeThatFits:NSSizeMax].width; }]; $(self.appViews).show; - + [self.window setContentSize:NSMakeSize(windowWidth, _contentHeight)]; - + [self hideNoAppsViews]; } - + [self.view setNeedsLayout:YES]; } @@ -186,27 +204,27 @@ - (void)_enterApp:(LKInspectableApp *)app { return; } self.isEnteringApp = YES; - + [LKPerformanceReporter.sharedInstance willStartReload]; - + @weakify(self); [[app fetchHierarchyData] subscribeNext:^(LookinHierarchyInfo *info) { @strongify(self); - + if (!info) { [self _handleEnterAppFailWithError:LookinErr_Inner]; } else { [LKAppsManager sharedInstance].inspectingApp = app; [[LKStaticHierarchyDataSource sharedInstance] reloadWithHierarchyInfo:info keepState:NO]; - + [self.bottomIndicatorView finishWithCompletion:^{ [[LKNavigationManager sharedInstance] showStaticWorkspace]; [[LKNavigationManager sharedInstance] closeLaunch]; }]; } - + [LKPerformanceReporter.sharedInstance didFetchHierarchy]; - + } error:^(NSError * _Nullable error) { @strongify(self); [self _handleEnterAppFailWithError:error]; @@ -243,11 +261,56 @@ - (void)showNoAppsView { - (void)hideNoAppsViews { [_noAppsTitleLabel removeFromSuperview]; _noAppsTitleLabel = nil; - + [self.reloadingIndicator removeFromSuperview]; self.reloadingIndicator = nil; } +- (void)_handleWireless { + __auto_type devices = [LKConnectionManager.sharedInstance getAllWirelessDevices]; + __auto_type vc = [[LKMenuPopoverWirelessDevicesListController alloc] initWithDevices:devices]; + NSView *appItemView = self.wirelessControl; + NSPopover *popover = [[NSPopover alloc] init]; + @weakify(popover); + @weakify(self); + vc.didSelectDevice = ^(ECOChannelDeviceInfo *device) { + @strongify(popover); + [popover close]; + + void (^connectBlock)(ECOChannelDeviceInfo *) = ^(ECOChannelDeviceInfo *device){ + if (!device.isConnected) { + NSAssert(NO, @""); + return; + } + @strongify(self); + [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_reloadWithAutoEntering:) object:nil]; + [self.bottomIndicatorView animateToProgress:.8 duration:1]; + @weakify(self); + [[[LKAppsManager.sharedInstance fetchAppInfosWithImage:NO localInfos:self.appInfos] deliverOnMainThread] subscribeNext:^(NSArray *apps) { + @strongify(self); + LKInspectableApp *app = [apps lookin_firstFiltered:^BOOL(LKInspectableApp *app) { + return app.channel == device; + }]; + NSAssert(app != nil, @""); + [self _enterApp:app]; + }]; + }; + + if (!device.authorizedType) { + [[LKConnectionManager.sharedInstance connectToWireless:device] subscribeNext:^(ECOChannelDeviceInfo *d) { + connectBlock(d); + }]; + } else { + connectBlock(device); + } + }; + popover.behavior = NSPopoverBehaviorTransient; + popover.animates = NO; + popover.contentSize = vc.bestSize; + popover.contentViewController = vc; + [popover showRelativeToRect:NSMakeRect(0, 0, appItemView.bounds.size.width, appItemView.bounds.size.height) ofView:appItemView preferredEdge:NSRectEdgeMaxY]; +} + - (void)_handleTutorial { [LKHelper openLookinWebsiteWithPath:@"faq/cannot-see/"]; } diff --git a/LookinClient/Launch/LKLaunchWirelessDeviceView.h b/LookinClient/Launch/LKLaunchWirelessDeviceView.h new file mode 100644 index 00000000..6e0cbef8 --- /dev/null +++ b/LookinClient/Launch/LKLaunchWirelessDeviceView.h @@ -0,0 +1,16 @@ +// +// LKLaunchWirelessDeviceView.h +// LookinClient +// +// Created by mlch911 on 2023/11/13. +// Copyright © 2023 hughkli. All rights reserved. +// + +#import "LKBaseView.h" +#import "ECOChannelDeviceInfo.h" + +@interface LKLaunchWirelessDeviceView : LKBaseControl + +@property(nonatomic, strong) ECOChannelDeviceInfo *device; + +@end diff --git a/LookinClient/Launch/LKLaunchWirelessDeviceView.m b/LookinClient/Launch/LKLaunchWirelessDeviceView.m new file mode 100644 index 00000000..fa919809 --- /dev/null +++ b/LookinClient/Launch/LKLaunchWirelessDeviceView.m @@ -0,0 +1,160 @@ +// +// LKLaunchWirelessDeviceView.m +// LookinClient +// +// Created by mlch911 on 2023/11/13. +// Copyright © 2023 hughkli. All rights reserved. +// + +#import "LKLaunchWirelessDeviceView.h" +#import "LKConnectionManager.h" + +@interface LKLaunchWirelessDeviceView () + +@property(nonatomic, strong) CALayer *hoverBgLayer; +@property(nonatomic, strong) NSImageView *iconImageView; +@property(nonatomic, strong) LKLabel *titleLabel; +@property(nonatomic, strong) LKLabel *subtitleLabel; +@property(nonatomic, strong) LKLabel *stateLabel; +@property(nonatomic, strong) LKTextControl *autoConnectControl; + +@end + +@implementation LKLaunchWirelessDeviceView { + NSEdgeInsets _insets; + CGFloat _iconMarginRight; +} + +- (instancetype)initWithFrame:(NSRect)frameRect { + if (self = [super initWithFrame:frameRect]) { + self.layer.cornerRadius = 4; + + self.hoverBgLayer = [CALayer layer]; + self.hoverBgLayer.opacity = 0; + self.hoverBgLayer.cornerRadius = 4; + [self.layer addSublayer:self.hoverBgLayer]; + + self.iconImageView = [NSImageView new]; + [self addSubview:self.iconImageView]; + + self.titleLabel = [LKLabel new]; + self.titleLabel.textColor = [NSColor labelColor]; + [self addSubview:self.titleLabel]; + + self.subtitleLabel = [LKLabel new]; + self.subtitleLabel.textColor = [NSColor secondaryLabelColor]; + [self addSubview:self.subtitleLabel]; + + self.stateLabel = [LKLabel new]; + self.stateLabel.textColor = [NSColor labelColor]; + [self addSubview:self.stateLabel]; + + self.autoConnectControl = LKTextControl.new; + self.autoConnectControl.label.textColor = [NSColor labelColor]; + self.autoConnectControl.spaceBetweenLabelAndImage = 4; + [self addSubview:self.autoConnectControl]; + + _insets = NSEdgeInsetsMake(12, 13, 8, 13); + _iconMarginRight = 6; + self.titleLabel.font = NSFontMake(12); + self.subtitleLabel.font = NSFontMake(11); + + [self.autoConnectControl addTarget:self clickAction:@selector(handleAutoConnectControl)]; + } + return self; +} + +- (void)layout { + [super layout]; + + self.hoverBgLayer.frame = self.layer.bounds; + + $(self.iconImageView).sizeToFit.y(_insets.top); + + $(self.titleLabel).sizeToFit; + $(self.subtitleLabel).sizeToFit.y(self.titleLabel.$maxY + 2); + $(self.titleLabel, self.subtitleLabel).x(self.iconImageView.$maxX + _iconMarginRight).groupMidY(self.iconImageView.$midY); + + $(self.autoConnectControl).sizeToFit.maxX(self.$maxX - _insets.right - 10).midY(self.subtitleLabel.$midY); + $(self.stateLabel).sizeToFit.maxX(self.autoConnectControl.hidden ? self.autoConnectControl.$maxX : self.autoConnectControl.$x - 6).midY(self.subtitleLabel.$midY); + + $(self.iconImageView, self.titleLabel, self.subtitleLabel).groupHorAlign.offsetX(-2); +} + +- (NSSize)sizeThatFits:(NSSize)limitedSize { + CGFloat width = self.iconImageView.image.size.width + _iconMarginRight + MAX([self.titleLabel sizeThatFits:NSSizeMax].width, [self.subtitleLabel sizeThatFits:NSSizeMax].width) + _insets.left + _insets.right; + CGFloat height = _insets.top + self.iconImageView.image.size.height + _insets.bottom; + return NSMakeSize(width, height); +} + +- (void)sizeToFit { + NSSize size = [self sizeThatFits:NSSizeMax]; + [self setFrameSize:size]; +} + +- (void)setDevice:(ECOChannelDeviceInfo *)device { + _device = device; + switch (device.deviceType) { + case ECODeviceType_Simulator: + self.iconImageView.image = NSImageMake(@"icon_simulator_big"); + break; + case ECODeviceType_Device: + self.iconImageView.image = NSImageMake(@"icon_iphone_big"); + break; + case ECODeviceType_iPad_Device: + self.iconImageView.image = NSImageMake(@"icon_ipad_big"); + break; + case ECODeviceType_MacApp: + NSAssert(NO, @""); + break; + default: + break; + } + self.titleLabel.stringValue = [NSString stringWithFormat:@"%@ - %@(%@.%@)", device.deviceName, device.appInfo.appName, device.appInfo.appVersion, device.appInfo.appShortVersion]; + self.subtitleLabel.stringValue = [NSString stringWithFormat:@"iOS %@", device.systemVersion]; + self.stateLabel.stringValue = device.authorizedType ? @"已连接" : @"点击连接"; + + self.autoConnectControl.hidden = device.authorizedType != ECOAuthorizeResponseType_AllowAlways; + self.autoConnectControl.label.stringValue = @"自动连接"; + BOOL isWhiteDevice = [LKConnectionManager.sharedInstance isWhiteListDevice:device]; + self.autoConnectControl.rightImage = [NSImage imageWithSystemSymbolName:isWhiteDevice ? @"checkmark.square" : @"square" accessibilityDescription:nil]; +} + +- (void)handleAutoConnectControl { + BOOL isWhiteDevice = [LKConnectionManager.sharedInstance isWhiteListDevice:self.device]; + isWhiteDevice = !isWhiteDevice; + self.autoConnectControl.rightImage = [NSImage imageWithSystemSymbolName:isWhiteDevice ? @"checkmark.square" : @"square" accessibilityDescription:nil]; + [LKConnectionManager.sharedInstance setWhiteListDevice:self.device white:isWhiteDevice]; +} + +- (void)mouseEntered:(NSEvent *)event { + [super mouseEntered:event]; + if (!self.device.authorizedType) { + self.hoverBgLayer.opacity = 1; + } +} + +- (void)mouseExited:(NSEvent *)event { + [super mouseExited:event]; + if (!self.device.authorizedType) { + self.hoverBgLayer.opacity = 0; + } +} + +- (void)updateLayer { + [super updateLayer]; + self.hoverBgLayer.backgroundColor = self.effectiveAppearance.lk_isDarkMode ? LookinColorRGBAMake(0, 0, 0, .17).CGColor : LookinColorRGBAMake(0, 0, 0, .08).CGColor; + self.layer.backgroundColor = [NSColor clearColor].CGColor; +} + +- (void)updateTrackingAreas { + [super updateTrackingAreas]; + [self.trackingAreas enumerateObjectsUsingBlock:^(NSTrackingArea * _Nonnull oldArea, NSUInteger idx, BOOL * _Nonnull stop) { + [self removeTrackingArea:oldArea]; + }]; + + NSTrackingArea *newArea = [[NSTrackingArea alloc] initWithRect:self.bounds options:NSTrackingMouseEnteredAndExited|NSTrackingActiveAlways owner:self userInfo:nil]; + [self addTrackingArea:newArea]; +} + +@end diff --git a/LookinClient/Static/LKStaticWindowController.m b/LookinClient/Static/LKStaticWindowController.m index 154e222c..f897f914 100644 --- a/LookinClient/Static/LKStaticWindowController.m +++ b/LookinClient/Static/LKStaticWindowController.m @@ -31,6 +31,8 @@ #import "LKMessageManager.h" #import "LKServerVersionRequestor.h" #import "LKVersionComparer.h" +#import "LKConnectionManager.h" +#import "LKMenuPopoverWirelessDevicesListController.h" @import AppCenter; @import AppCenterAnalytics; @@ -112,32 +114,12 @@ - (void)popupAllInspectableAppsWithSource:(MenuPopoverAppsListControllerEventSou LKMenuPopoverAppsListController *vc = [[LKMenuPopoverAppsListController alloc] initWithApps:apps source:source]; NSPopover *popover = [[NSPopover alloc] init]; @weakify(popover); + @weakify(self); vc.didSelectApp = ^(LKInspectableApp *app) { + @strongify(self); @strongify(popover); [popover close]; - - if (app.serverVersionError) { - if (app.serverVersionError.code == LookinErrCode_ServerVersionTooLow) { - [LKHelper openLookinWebsiteWithPath:@"faq/server-version-too-low/"]; - } else { - [LKHelper openLookinWebsiteWithPath:@"faq/server-version-too-high/"]; - } - - } else { - [self.viewController.progressView animateToProgress:InitialIndicatorProgressWhenFetchHierarchy]; - - BOOL isTheSameApp = [[LKAppsManager sharedInstance].inspectingApp.appInfo isEqualToAppInfo:app.appInfo]; - - [[app fetchHierarchyData] subscribeNext:^(LookinHierarchyInfo *info) { - [self.viewController.progressView finishWithCompletion:nil]; - [LKAppsManager sharedInstance].inspectingApp = app; - [[LKStaticHierarchyDataSource sharedInstance] reloadWithHierarchyInfo:info keepState:isTheSameApp]; - - } error:^(NSError * _Nullable error) { - AlertError(error, self.window); - [self.viewController.progressView resetToZero]; - }]; - } + [self _enterApp:app]; }; popover.behavior = NSPopoverBehaviorTransient; @@ -151,6 +133,72 @@ - (void)popupAllInspectableAppsWithSource:(MenuPopoverAppsListControllerEventSou }]; } +- (void)popupNotConnnectWirelessInspectableApps { + __auto_type devices = [LKConnectionManager.sharedInstance getAllWirelessDevices]; + __auto_type vc = [[LKMenuPopoverWirelessDevicesListController alloc] initWithDevices:devices]; + NSView *appItemView = self.toolbarItemsMap[LKToolBarIdentifier_App_Wireless].view; + NSPopover *popover = [[NSPopover alloc] init]; + @weakify(popover); + @weakify(self); + vc.didSelectDevice = ^(ECOChannelDeviceInfo *device) { + @strongify(popover); + [popover close]; + + void (^connectBlock)(ECOChannelDeviceInfo *) = ^(ECOChannelDeviceInfo *device){ + if (!device.isConnected) { + NSAssert(NO, @""); + return; + } + [[[LKAppsManager.sharedInstance fetchAppInfosWithImage:NO localInfos:nil] deliverOnMainThread] subscribeNext:^(NSArray *apps) { + @strongify(self); + LKInspectableApp *app = [apps lookin_firstFiltered:^BOOL(LKInspectableApp *app) { + return app.channel == device; + }]; + NSAssert(app != nil, @""); + [self _enterApp:app]; + }]; + }; + + if (!device.authorizedType) { + [[LKConnectionManager.sharedInstance connectToWireless:device] subscribeNext:^(ECOChannelDeviceInfo *d) { + connectBlock(d); + }]; + } else { + connectBlock(device); + } + }; + popover.behavior = NSPopoverBehaviorTransient; + popover.animates = NO; + popover.contentSize = vc.bestSize; + popover.contentViewController = vc; + [popover showRelativeToRect:NSMakeRect(0, 0, appItemView.bounds.size.width, appItemView.bounds.size.height) ofView:appItemView preferredEdge:NSRectEdgeMaxY]; +} + +- (void)_enterApp:(LKInspectableApp *)app { + if (app.serverVersionError) { + if (app.serverVersionError.code == LookinErrCode_ServerVersionTooLow) { + [LKHelper openLookinWebsiteWithPath:@"faq/server-version-too-low/"]; + } else { + [LKHelper openLookinWebsiteWithPath:@"faq/server-version-too-high/"]; + } + + } else { + [self.viewController.progressView animateToProgress:InitialIndicatorProgressWhenFetchHierarchy]; + + BOOL isTheSameApp = [[LKAppsManager sharedInstance].inspectingApp.appInfo isEqualToAppInfo:app.appInfo]; + + [[app fetchHierarchyData] subscribeNext:^(LookinHierarchyInfo *info) { + [self.viewController.progressView finishWithCompletion:nil]; + [LKAppsManager sharedInstance].inspectingApp = app; + [[LKStaticHierarchyDataSource sharedInstance] reloadWithHierarchyInfo:info keepState:isTheSameApp]; + + } error:^(NSError * _Nullable error) { + AlertError(error, self.window); + [self.viewController.progressView resetToZero]; + }]; + } +} + #pragma mark - NSToolbarDelegate - (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar { @@ -158,7 +206,7 @@ - (void)popupAllInspectableAppsWithSource:(MenuPopoverAppsListControllerEventSou } - (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar { - NSMutableArray *ret = @[LKToolBarIdentifier_Reload, LKToolBarIdentifier_FastMode, LKToolBarIdentifier_App, LKToolBarIdentifier_SwiftUI, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Dimension, LKToolBarIdentifier_Rotation, LKToolBarIdentifier_Setting, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Scale, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Measure, LKToolBarIdentifier_Console].mutableCopy; + NSMutableArray *ret = @[LKToolBarIdentifier_Reload, LKToolBarIdentifier_FastMode, LKToolBarIdentifier_App, LKToolBarIdentifier_App_Wireless, LKToolBarIdentifier_SwiftUI, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Dimension, LKToolBarIdentifier_Rotation, LKToolBarIdentifier_Setting, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Scale, NSToolbarFlexibleSpaceItemIdentifier, LKToolBarIdentifier_Measure, LKToolBarIdentifier_Console].mutableCopy; if ([[[LKMessageManager sharedInstance] queryMessages] count] > 0) { [ret addObject:LKToolBarIdentifier_Message]; [MSACAnalytics trackEvent:@"Show Notification"]; @@ -181,6 +229,9 @@ - (nullable NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:( } else if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_App]) { item.target = self; item.action = @selector(_handleApp); + } else if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_App_Wireless]) { + item.target = self; + item.action = @selector(_handleWirelessApp); } else if ([item.itemIdentifier isEqualToString:LKToolBarIdentifier_Rotation]) { item.target = self; item.action = @selector(_handleFreeRotation); @@ -275,6 +326,10 @@ - (void)_handleApp { [self popupAllInspectableAppsWithSource:MenuPopoverAppsListControllerEventSourceAppButton]; } +- (void)_handleWirelessApp { + [self popupNotConnnectWirelessInspectableApps]; +} + - (void)_handleSetting:(NSButton *)button { NSPopover *popover = [[NSPopover alloc] init]; popover.behavior = NSPopoverBehaviorTransient; diff --git a/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.h b/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.h new file mode 100644 index 00000000..8835fe17 --- /dev/null +++ b/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.h @@ -0,0 +1,22 @@ +// +// LKMenuPopoverWirelessDevicesListController.h +// LookinClient +// +// Created by mlch911 on 2023/11/13. +// Copyright © 2023 hughkli. All rights reserved. +// + +#import "LKBaseViewController.h" +#import "ECOChannelDeviceInfo.h" + +@class LKInspectableApp; + +@interface LKMenuPopoverWirelessDevicesListController : LKBaseViewController + +- (instancetype)initWithDevices:(NSArray *)devices; + +@property(nonatomic, copy) void (^didSelectDevice)(ECOChannelDeviceInfo *device); + +- (NSSize)bestSize; + +@end diff --git a/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.m b/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.m new file mode 100644 index 00000000..0792e885 --- /dev/null +++ b/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.m @@ -0,0 +1,152 @@ +// +// LKMenuPopoverWirelessAppsListController.m +// LookinClient +// +// Created by mlch911 on 2023/11/13. +// Copyright © 2023 hughkli. All rights reserved. +// + +#import "LKMenuPopoverWirelessDevicesListController.h" +#import "LKAppsManager.h" +#import "LKLaunchWirelessDeviceView.h" +#import "LookinHierarchyInfo.h" +#import "LKStaticHierarchyDataSource.h" + +@interface LKMenuPopoverWirelessDevicesListController () + +@property(nonatomic, strong) NSArray *deviceViews; + +@property(nonatomic, strong) LKLabel *titleLabel; +@property(nonatomic, strong) LKLabel *subtitleLabel; +@property(nonatomic, strong) LKTextControl *tutorialControl; + +@end + +@implementation LKMenuPopoverWirelessDevicesListController { + CGFloat _appViewInterSpace; + NSEdgeInsets _insets; + CGFloat _titleMarginBottom; + CGFloat _subtitleMarginBottom; +} + +- (instancetype)initWithDevices:(NSArray *)devices { + if (self = [self init]) { + _insets = NSEdgeInsetsMake(9, 18, 35, 14); + _titleMarginBottom = 3; + _subtitleMarginBottom = 5; + _appViewInterSpace = 1; + + NSString *title = nil; + NSString *subtitle = nil; + + if (devices.count) { + self.deviceViews = [devices.rac_sequence map:^id _Nullable(ECOChannelDeviceInfo *device) { + LKLaunchWirelessDeviceView *view = [LKLaunchWirelessDeviceView new]; + view.device = device; + [view addTarget:self clickAction:@selector(handleClickAppView:)]; + [self.view addSubview:view]; + return view; + }].array; + } + + if (title.length) { + self.titleLabel = [LKLabel new]; + self.titleLabel.alignment = NSTextAlignmentCenter; + self.titleLabel.font = NSFontMake(14); + self.titleLabel.textColor = [NSColor labelColor]; + self.titleLabel.stringValue = title; + [self.view addSubview:self.titleLabel]; + } + + if (subtitle.length) { + self.subtitleLabel = [LKLabel new]; + self.subtitleLabel.alignment = NSTextAlignmentCenter; + self.subtitleLabel.font = NSFontMake(12); + self.subtitleLabel.textColor = [NSColor labelColor]; + self.subtitleLabel.stringValue = subtitle; + [self.view addSubview:self.subtitleLabel]; + } + + self.tutorialControl = [LKTextControl new]; + self.tutorialControl.layer.cornerRadius = 4; + self.tutorialControl.label.stringValue = NSLocalizedString(@"Can't see your app ?", nil); + self.tutorialControl.label.textColor = [NSColor linkColor]; + self.tutorialControl.label.font = NSFontMake(12); + self.tutorialControl.adjustAlphaWhenClick = YES; + [self.tutorialControl addTarget:self clickAction:@selector(_handleTutorial)]; + [self.view addSubview:self.tutorialControl]; + } + return self; +} + +- (void)viewDidLayout { + [super viewDidLayout]; + + __block CGFloat y = _insets.top; + if (self.titleLabel) { + $(self.titleLabel).fullWidth.heightToFit.y(y); + y = self.titleLabel.$maxY + _titleMarginBottom; + } + if (self.subtitleLabel) { + $(self.subtitleLabel).fullWidth.heightToFit.y(y); + y = self.subtitleLabel.$maxY + _subtitleMarginBottom; + } + + if (self.deviceViews.count) { + [self.deviceViews enumerateObjectsUsingBlock:^(LKLaunchWirelessDeviceView * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + $(obj).sizeToFit.x(0).y(y); + y = obj.$maxY + self->_appViewInterSpace; + }]; + $(self.deviceViews).groupHorAlign; + + $(self.tutorialControl).sizeToFit.horAlign.offsetX(3).bottom(10); + } else { + $(self.tutorialControl).sizeToFit.horAlign.offsetX(3); + if (self.subtitleLabel.isVisible) { + $(self.tutorialControl).y(y); + } else { + $(self.tutorialControl).y(y + 8); + } + $(self.titleLabel, self.subtitleLabel, self.tutorialControl).visibles.groupVerAlign; + } + +} + +- (void)handleClickAppView:(LKLaunchWirelessDeviceView *)view { + ECOChannelDeviceInfo *device = view.device; + if (!device.authorizedType && self.didSelectDevice) { + self.didSelectDevice(device); + } +} + +- (void)_handleTutorial { + [[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://lookin.work/faq/cannot-see/"]]; +} + +- (NSSize)bestSize { + if (self.deviceViews.count <= 0) { + return NSMakeSize(245, 80); + } + __block CGFloat width = 0; + __block CGFloat height = _insets.top + _insets.bottom; + [self.deviceViews enumerateObjectsUsingBlock:^(LKLaunchWirelessDeviceView * _Nonnull view, NSUInteger idx, BOOL * _Nonnull stop) { + NSSize size = [view sizeThatFits:NSSizeMax]; + width = MAX(width, size.width + _insets.left + _insets.right); + height += size.height; + }]; + + if (self.titleLabel) { + NSSize titleSize = [self.titleLabel sizeThatFits:NSSizeMax]; + height += titleSize.height + _titleMarginBottom; + width = MAX(width, titleSize.width + _insets.left + _insets.right); + } + if (self.subtitleLabel) { + NSSize subtitleSize = [self.subtitleLabel sizeThatFits:NSSizeMax]; + height += subtitleSize.height + _subtitleMarginBottom; + width = MAX(width, subtitleSize.width + _insets.left + _insets.right); + } + + return NSMakeSize(MAX(245, width), height); +} + +@end diff --git a/LookinClient/Toolbar/LKWindowToolbarHelper.h b/LookinClient/Toolbar/LKWindowToolbarHelper.h index 979e895d..e72907dd 100644 --- a/LookinClient/Toolbar/LKWindowToolbarHelper.h +++ b/LookinClient/Toolbar/LKWindowToolbarHelper.h @@ -14,6 +14,7 @@ extern NSToolbarItemIdentifier const LKToolBarIdentifier_Rotation; extern NSToolbarItemIdentifier const LKToolBarIdentifier_Setting; extern NSToolbarItemIdentifier const LKToolBarIdentifier_Reload; extern NSToolbarItemIdentifier const LKToolBarIdentifier_App; +extern NSToolbarItemIdentifier const LKToolBarIdentifier_App_Wireless; extern NSToolbarItemIdentifier const LKToolBarIdentifier_AppInReadMode; extern NSToolbarItemIdentifier const LKToolBarIdentifier_Console; extern NSToolbarItemIdentifier const LKToolBarIdentifier_Add; diff --git a/LookinClient/Toolbar/LKWindowToolbarHelper.m b/LookinClient/Toolbar/LKWindowToolbarHelper.m index 226fb671..1f6bc560 100644 --- a/LookinClient/Toolbar/LKWindowToolbarHelper.m +++ b/LookinClient/Toolbar/LKWindowToolbarHelper.m @@ -20,6 +20,7 @@ NSToolbarItemIdentifier const LKToolBarIdentifier_Setting = @"2"; NSToolbarItemIdentifier const LKToolBarIdentifier_Reload = @"3"; NSToolbarItemIdentifier const LKToolBarIdentifier_App = @"5"; +NSToolbarItemIdentifier const LKToolBarIdentifier_App_Wireless = @"6"; NSToolbarItemIdentifier const LKToolBarIdentifier_AppInReadMode = @"12"; NSToolbarItemIdentifier const LKToolBarIdentifier_Add = @"13"; NSToolbarItemIdentifier const LKToolBarIdentifier_Remove = @"14"; @@ -55,11 +56,11 @@ + (id)allocWithZone:(struct _NSZone *)zone{ - (NSToolbarItem *)makeToolBarItemWithIdentifier:(NSToolbarItemIdentifier)identifier preferenceManager:(LKPreferenceManager *)manager { NSAssert(![identifier isEqualToString:LKToolBarIdentifier_AppInReadMode], @"请使用 makeAppInReadModeItemWithAppInfo: 方法"); - + if ([identifier isEqualToString:LKToolBarIdentifier_Measure]) { NSImage *image = NSImageMake(@"icon_measure"); image.template = YES; - + NSButton *button = [NSButton new]; [button setImage:image]; button.bezelStyle = NSBezelStyleTexturedRounded; @@ -67,46 +68,46 @@ - (NSToolbarItem *)makeToolBarItemWithIdentifier:(NSToolbarItemIdentifier)identi button.target = self; button.action = @selector(_handleToggleMeasureButton:); [button lookin_bindObject:manager forKey:@"manager"]; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Measure]; item.label = NSLocalizedString(@"Measure", nil); item.view = button; item.minSize = NSMakeSize(48, 34); [manager.measureState subscribe:self action:@selector(_handleMeasureStateDidChange:) relatedObject:button sendAtOnce:YES]; - + return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_Rotation]) { NSImage *image = NSImageMake(@"icon_rotation"); image.template = YES; - + NSButton *button = [NSButton new]; [button setImage:image]; button.bezelStyle = NSBezelStyleTexturedRounded; [button setButtonType:NSButtonTypePushOnPushOff]; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Rotation]; item.label = NSLocalizedString(@"Free Rotation", nil); item.view = button; item.minSize = NSMakeSize(48, 34); [manager.freeRotation subscribe:self action:@selector(_handleFreeRotationDidChange:) relatedObject:button sendAtOnce:YES]; - + return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_Dimension]) { NSImage *image_2d = NSImageMake(@"icon_2d"); image_2d.template = YES; NSImage *image_3d = NSImageMake(@"icon_3d"); image_3d.template = YES; - + NSSegmentedControl *control = [NSSegmentedControl segmentedControlWithImages:@[image_2d, image_3d] trackingMode:NSSegmentSwitchTrackingSelectOne target:self action:@selector(_handleDimension:)]; [control lookin_bindObjectWeakly:manager forKey:Key_BindingPreferenceManager]; control.segmentDistribution = NSSegmentDistributionFillEqually; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Dimension]; item.label = @"2D / 3D"; item.view = control; @@ -116,10 +117,10 @@ - (NSToolbarItem *)makeToolBarItemWithIdentifier:(NSToolbarItemIdentifier)identi return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_Scale]) { double scale = manager.previewScale.currentDoubleValue; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Scale]; LKWindowToolbarScaleView *scaleView = [LKWindowToolbarScaleView new]; scaleView.slider.minValue = LookinPreviewMinScale; @@ -134,54 +135,54 @@ - (NSToolbarItem *)makeToolBarItemWithIdentifier:(NSToolbarItemIdentifier)identi [scaleView.slider lookin_bindObjectWeakly:manager forKey:Key_BindingPreferenceManager]; [scaleView.increaseButton lookin_bindObjectWeakly:manager forKey:Key_BindingPreferenceManager]; [scaleView.decreaseButton lookin_bindObjectWeakly:manager forKey:Key_BindingPreferenceManager]; - + item.label = NSLocalizedString(@"Zoom", nil); item.view = scaleView; item.minSize = NSMakeSize(160, 34); - + [manager.previewScale subscribe:self action:@selector(_handlePreviewScaleDidChange:) relatedObject:scaleView.slider sendAtOnce:YES]; - + return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_Setting]) { NSImage *image = NSImageMake(@"icon_setting"); image.template = YES; - + NSButton *button = [NSButton new]; [button setImage:image]; button.bezelStyle = NSBezelStyleTexturedRounded; [button lookin_bindObjectWeakly:manager forKey:Key_BindingPreferenceManager]; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Setting]; item.view = button; item.minSize = NSMakeSize(48, 34); return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_Reload]) { NSImage *image = NSImageMake(@"icon_reload"); image.template = YES; - + NSButton *button = [NSButton new]; [button setImage:image]; button.bezelStyle = NSBezelStyleTexturedRounded; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Reload]; item.label = NSLocalizedString(@"Reload", nil); item.view = button; item.minSize = NSMakeSize(68, 34); return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_App]) { LKWindowToolbarAppButton *button = [LKWindowToolbarAppButton new]; button.bezelStyle = NSBezelStyleTexturedRounded; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_App]; item.label = NSLocalizedString(@"Select App", nil); item.view = button; - + [[RACObserve([LKAppsManager sharedInstance], inspectingApp) takeUntil:item.rac_willDeallocSignal] subscribeNext:^(LKInspectableApp *app) { button.appInfo = app.appInfo; if (app) { @@ -194,7 +195,19 @@ - (NSToolbarItem *)makeToolBarItemWithIdentifier:(NSToolbarItemIdentifier)identi }]; return item; } - + + if ([identifier isEqualToString:LKToolBarIdentifier_App_Wireless]) { + NSButton *button = [NSButton new]; + [button setTitle:@"ᯤ"]; + button.bezelStyle = NSBezelStyleTexturedRounded; + + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_App_Wireless]; + item.label = NSLocalizedString(@"Wireless Connections", nil); + item.view = button; + item.minSize = NSMakeSize(48, 34); + return item; + } + if ([identifier isEqualToString:LKToolBarIdentifier_Console]) { NSImage *image = NSImageMake(@"icon_console"); image.template = YES; @@ -203,14 +216,14 @@ - (NSToolbarItem *)makeToolBarItemWithIdentifier:(NSToolbarItemIdentifier)identi [button setImage:image]; button.bezelStyle = NSBezelStyleTexturedRounded; [button setButtonType:NSButtonTypePushOnPushOff]; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Console]; item.label = NSLocalizedString(@"Console", nil); item.view = button; item.minSize = NSMakeSize(48, 34); return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_FastMode]) { NSImage *image = NSImageMake(@"icon_turbo"); image.template = YES; @@ -219,12 +232,12 @@ - (NSToolbarItem *)makeToolBarItemWithIdentifier:(NSToolbarItemIdentifier)identi [button setImage:image]; button.bezelStyle = NSBezelStyleTexturedRounded; [button setButtonType:NSButtonTypePushOnPushOff]; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_FastMode]; item.label = NSLocalizedString(@"Fast Mode", nil); item.view = button; item.minSize = NSMakeSize(60, 34); - + [manager.fastMode subscribe:self action:@selector(_handleFastModeDidChange:) relatedObject:button sendAtOnce:YES]; return item; } @@ -240,49 +253,49 @@ - (NSToolbarItem *)makeToolBarItemWithIdentifier:(NSToolbarItemIdentifier)identi item.minSize = NSMakeSize(62, 34); return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_Add]) { NSImage *image = [NSImage imageNamed:NSImageNameAddTemplate]; image.template = YES; - + NSButton *button = [NSButton new]; [button setImage:image]; button.bezelStyle = NSBezelStyleTexturedRounded; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Add]; item.view = button; item.minSize = NSMakeSize(48, 34); return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_Remove]) { NSImage *image = NSImageMake(@"icon_delete"); image.template = YES; - + NSButton *button = [NSButton new]; [button setImage:image]; button.bezelStyle = NSBezelStyleTexturedRounded; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Remove]; item.view = button; item.minSize = NSMakeSize(48, 34); return item; } - + if ([identifier isEqualToString:LKToolBarIdentifier_Message]) { NSImage *image = NSImageMake(@"icon_notification"); image.template = YES; - + NSButton *button = [NSButton new]; [button setImage:image]; button.bezelStyle = NSBezelStyleTexturedRounded; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_Message]; item.view = button; item.minSize = NSMakeSize(48, 34); return item; } - + NSAssert(NO, @""); return nil; } @@ -292,12 +305,12 @@ - (NSToolbarItem *)makeAppInReadModeItemWithAppInfo:(LookinAppInfo *)appInfo { button.bezelStyle = NSBezelStyleTexturedRounded; [button lookin_bindObject:appInfo forKey:Key_BindingAppInfo]; button.appInfo = appInfo; - + NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:LKToolBarIdentifier_AppInReadMode]; item.label = @"iOS App"; item.view = button; item.minSize = NSMakeSize(button.bestWidth + 6, 34); - + item.maxSize = item.minSize; return item; } diff --git a/LookinClient/en.lproj/Localizable.strings b/LookinClient/en.lproj/Localizable.strings index e5ee3b55..89c54398 100644 --- a/LookinClient/en.lproj/Localizable.strings +++ b/LookinClient/en.lproj/Localizable.strings @@ -1,6 +1,7 @@ "Reload" = "Reload"; "Syncing…" = "Syncing…"; "Select App" = "Select App"; +"Wireless Connections" = "Wireless Connections"; "View" = "View"; "Zoom" = "Zoom"; "Console" = "Console"; @@ -218,3 +219,4 @@ "Some layer data failed to transmit." = "Some layer data failed to transmit."; "It may be due to changes in the layer structure within the iOS app. You can try reloading the entire structure in Lookin." = "It may be due to changes in the layer structure within the iOS app. You can try reloading the entire structure in Lookin."; "properties or methods" = "properties or methods"; +"Wireless Connection rejected" = "Wireless Connection rejected."; diff --git a/LookinClient/zh-Hans.lproj/Localizable.strings b/LookinClient/zh-Hans.lproj/Localizable.strings index f2ff599a..33f64bd1 100644 --- a/LookinClient/zh-Hans.lproj/Localizable.strings +++ b/LookinClient/zh-Hans.lproj/Localizable.strings @@ -1,6 +1,7 @@ "Reload" = "刷新"; "Syncing…" = "图像传输中…"; "Select App" = "App 切换"; +"Wireless Connections" = "无线连接"; "View" = "图像"; "Zoom" = "大小"; "Console" = "控制台"; @@ -216,3 +217,4 @@ "Some layer data failed to transmit." = "某些图层信息传输失败"; "It may be due to changes in the layer structure within the iOS app. You can try reloading the entire structure in Lookin." = "可能因为 iOS App 中的图层结构有变化,可尝试在 Lookin 中整体重新刷新。"; "properties or methods" = "搜索属性或方法"; +"Wireless Connection rejected" = "无线连接被拒绝"; From 401ab79afb7b0c9ce46460b9ff30c26b79604878 Mon Sep 17 00:00:00 2001 From: hongbo <1049145827@qq.com> Date: Sat, 18 Jul 2026 17:08:35 +0800 Subject: [PATCH 6/8] Harden paired wireless inspection --- LookinClient/Connection/LKAppsManager.m | 10 +- LookinClient/Connection/LKConnectionManager.h | 8 +- LookinClient/Connection/LKConnectionManager.m | 204 +++++++++++------- LookinClient/Connection/LKInspectableApp.h | 5 +- LookinClient/Launch/LKLaunchViewController.m | 10 +- .../Launch/LKLaunchWirelessDeviceView.m | 8 +- .../Static/LKStaticWindowController.m | 28 +-- LookinClient/en.lproj/Localizable.strings | 7 + .../zh-Hans.lproj/Localizable.strings | 7 + Podfile | 2 +- Podfile.lock | 10 +- README.md | 21 ++ UPSTREAM_PATCHES.md | 1 + 13 files changed, 201 insertions(+), 120 deletions(-) diff --git a/LookinClient/Connection/LKAppsManager.m b/LookinClient/Connection/LKAppsManager.m index 3e9dbd76..2767de22 100644 --- a/LookinClient/Connection/LKAppsManager.m +++ b/LookinClient/Connection/LKAppsManager.m @@ -43,11 +43,11 @@ - (instancetype)init { _didAutoReconnectSucc = [RACSubject subject]; @weakify(self); - [[[[LKConnectionManager sharedInstance].channelWillEnd filter:^BOOL(Lookin_PTChannel *channel) { + [[[[LKConnectionManager sharedInstance].channelWillEnd filter:^BOOL(id channel) { @strongify(self); return channel == self.inspectingApp.channel; - }] flattenMap:^__kindof RACSignal * _Nullable(Lookin_PTChannel *channel) { + }] flattenMap:^__kindof RACSignal * _Nullable(id channel) { @strongify(self); NSLog(@"current connection end"); @@ -120,13 +120,13 @@ - (RACSignal *)fetchAppInfosWithImage:(BOOL)needImages localInfos:(NSArray *connectedChannels) { + return [[[[LKConnectionManager sharedInstance] tryToConnectAllPorts] flattenMap:^__kindof RACSignal * _Nullable(NSArray> *connectedChannels) { if (!connectedChannels.count) { // 没有任何 channel return [RACSignal return:nil]; } - NSArray *signals = [connectedChannels lookin_map:^id(NSUInteger idx, Lookin_PTChannel *channel) { + NSArray *signals = [connectedChannels lookin_map:^id(NSUInteger idx, id channel) { return [[[LKConnectionManager sharedInstance] requestWithType:LookinRequestTypeApp data:params channel:channel] catch:^RACSignal * _Nonnull(NSError * _Nonnull error) { if (error.code == LookinErrCode_ServerVersionTooHigh || error.code == LookinErrCode_ServerVersionTooLow) { // 这些 Lookin 版本不匹配的错误应该被保留,因为业务需要显示这些错误 @@ -154,7 +154,7 @@ - (RACSignal *)fetchAppInfosWithImage:(BOOL)needImages localInfos:(NSArray relatedChannel) = value; if (response.error) { NSAssert(NO, @""); return nil; diff --git a/LookinClient/Connection/LKConnectionManager.h b/LookinClient/Connection/LKConnectionManager.h index 017bfeec..40d5b9d6 100644 --- a/LookinClient/Connection/LKConnectionManager.h +++ b/LookinClient/Connection/LKConnectionManager.h @@ -38,9 +38,9 @@ /// 该方法不会 sendError - (RACSignal *)tryToConnectAllPorts; -/// 返回的 data 为 RACTuple +/// 返回的 data 为 RACTuple> /// 在调用该方法发请求时,如果已有相同 type 的旧 request 尚未返回结果,则之前的旧 request 会被报告 Error,然后被丢弃 -- (RACSignal *)requestWithType:(unsigned int)requestType data:(NSObject *)requestData channel:(Lookin_PTChannel *)channel; +- (RACSignal *)requestWithType:(unsigned int)requestType data:(NSObject *)requestData channel:(id)channel; - (RACSignal *)connectToWireless:(ECOChannelDeviceInfo *)device; @@ -51,11 +51,11 @@ - (void)setWhiteListDevice:(ECOChannelDeviceInfo *)device white:(BOOL)white; /// 取消先前使用 requestWithType:data:channel: 方法发送的尚未完成的 request,这个 request 会被报告为 completion -- (void)cancelRequestWithType:(unsigned int)requestType channel:(Lookin_PTChannel *)channel; +- (void)cancelRequestWithType:(unsigned int)requestType channel:(id)channel; /// 如果发送的消息不需要 server 端回复,则请使用该方法而非 requestWithType: /// 如果此时 server 端不在前台或处于断点等模式,则 server 端可能无法收到该消息 -- (void)pushWithType:(unsigned int)pushType data:(NSObject *)requestData channel:(Lookin_PTChannel *)channel; +- (void)pushWithType:(unsigned int)pushType data:(NSObject *)requestData channel:(id)channel; /// 即将关闭某个 channel,一般是因为 server 端断开(比如 iOS app 被 kill 掉或 USB 被拔掉) @property(nonatomic, strong, readonly) RACSubject *channelWillEnd; diff --git a/LookinClient/Connection/LKConnectionManager.m b/LookinClient/Connection/LKConnectionManager.m index fdf51c06..d7ff797b 100644 --- a/LookinClient/Connection/LKConnectionManager.m +++ b/LookinClient/Connection/LKConnectionManager.m @@ -16,7 +16,7 @@ #import "LKServerVersionRequestor.h" #import "ECOChannelManager.h" -static NSIndexSet * PushFrameTypeList() { +static NSIndexSet *PushFrameTypeList(void) { static NSIndexSet *list; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -27,6 +27,14 @@ } static NSString *const LKWhiteListDevicesKey = @"LKWhiteListDevicesKey"; +static NSTimeInterval const LKWirelessAuthorizationTimeout = 30; + +static NSString *LKWirelessDeviceIdentifier(ECOChannelDeviceInfo *device) { + if (!device.uuid.length || !device.appInfo.appId.length) { + return nil; + } + return [NSString stringWithFormat:@"%@_%@", device.uuid, device.appInfo.appId]; +} @implementation Lookin_PTChannel (LKConnection) @@ -93,7 +101,7 @@ @interface LKConnectionManager () @property(nonatomic, strong) NSMutableArray *connectWirelessDevices; @property(nonatomic, strong) NSMutableArray *notConnectWirelessDevices; @property(nonatomic, strong) ECOChannelManager *wirelessChannel; -@property(nonatomic, strong) NSMutableDictionary *authStateChangedBlocks; +@property(nonatomic, strong) NSMutableDictionary *authStateChangedBlocks; @property(nonatomic, strong) NSMutableArray *whitelistDevices; @@ -173,9 +181,12 @@ - (RACSignal *)tryToConnectAllPorts { return [[RACSignal zip:@[[self _tryToConnectAllSimulatorPorts], [self _tryToConnectAllUSBDevices], [self _tryToConnectToAllWirelessDevice]]] map:^id _Nullable(RACTuple * _Nullable value) { - RACTupleUnpack(NSArray *simulatorChannels, NSArray *usbChannels, NSArray *wirelessDevices) = value; - NSArray *connectedChannels = [[simulatorChannels arrayByAddingObjectsFromArray:usbChannels] arrayByAddingObjectsFromArray:wirelessDevices]; - return connectedChannels; + RACTupleUnpack(NSArray *simulatorChannels, NSArray *usbChannels, NSArray *wirelessDevices) = value; + NSMutableArray> *connectedChannels = [NSMutableArray array]; + [connectedChannels addObjectsFromArray:simulatorChannels]; + [connectedChannels addObjectsFromArray:usbChannels]; + [connectedChannels addObjectsFromArray:wirelessDevices]; + return connectedChannels.copy; }]; } @@ -286,20 +297,25 @@ - (RACSignal *)_tryToConnectToAllWirelessDevice { #pragma mark - Request -- (void)pushWithType:(unsigned int)pushType data:(NSObject *)data channel:(Lookin_PTChannel *)channel { +- (void)pushWithType:(unsigned int)pushType data:(NSObject *)data channel:(id)channel { if (!channel || !channel.isConnected) { return; } NSError *archiveError = nil; - dispatch_data_t payload = [[NSKeyedArchiver archivedDataWithRootObject:data requiringSecureCoding:YES error:&archiveError] createReferencingDispatchData]; + NSData *sendData = [NSKeyedArchiver archivedDataWithRootObject:data requiringSecureCoding:YES error:&archiveError]; if (archiveError) { NSAssert(NO, @""); } NSLog(@"LookinClient - pushData, type:%@", @(pushType)); - [channel sendFrameOfType:pushType tag:0 withPayload:payload callback:nil]; + if ([channel isKindOfClass:ECOChannelDeviceInfo.class]) { + [self.wirelessChannel sendPacket:sendData extraInfo:@{@"tag": @0, @"type": @(pushType)} toDevice:(ECOChannelDeviceInfo *)channel]; + } else if ([channel isKindOfClass:Lookin_PTChannel.class]) { + dispatch_data_t payload = [sendData createReferencingDispatchData]; + [(Lookin_PTChannel *)channel sendFrameOfType:pushType tag:0 withPayload:payload callback:nil]; + } } -- (RACSignal *)requestWithType:(unsigned int)requestType data:(NSObject *)requestData channel:(Lookin_PTChannel *)channel { +- (RACSignal *)requestWithType:(unsigned int)requestType data:(NSObject *)requestData channel:(id)channel { return [RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) { // NSLog(@"LookinClient, level1 - will ping for request:%@, port:%@", @(type), @(channel.portNumber)); NSTimeInterval timeoutInterval; @@ -457,7 +473,7 @@ - (void)_requestWithType:(unsigned int)requestType channel:(id)channel { LKConnectionRequest *activeRequest = [channel.activeRequests lookin_firstFiltered:^BOOL(LKConnectionRequest *obj) { return obj.type == requestType; }]; @@ -506,91 +522,133 @@ - (void)_startListeningForWirelessDevices { @weakify(self); // 接收到数据回调 self.wirelessChannel.receivedBlock = ^(ECOChannelDeviceInfo *device, NSData *data, NSDictionary *extraInfo) { - NSLog(@"🚀 Lookin receivedBlock device:%@", device); - NSNumber *tag = extraInfo[@"tag"]; - NSNumber *type = extraInfo[@"type"]; - LKConnectionRequest *activeRequest = [device.activeRequests lookin_firstFiltered:^BOOL(LKConnectionRequest *obj) { - return [@(obj.type) isEqualToNumber:type] && [@(obj.tag) isEqualToNumber:tag]; - }]; - if (!activeRequest) { - // 也许在 shouldAcceptFrameOfType 和 didReceiveFrame 两个时机之间,该 request 因为超时而被销毁了?有点玄学但确实偶尔会走到这里。 - return; - } - [self_weak_ _didReceiveDataWithChannel:device data:data activeRequest:activeRequest]; + dispatch_async(dispatch_get_main_queue(), ^{ + @strongify(self); + NSNumber *tag = [extraInfo[@"tag"] isKindOfClass:NSNumber.class] ? extraInfo[@"tag"] : nil; + NSNumber *type = [extraInfo[@"type"] isKindOfClass:NSNumber.class] ? extraInfo[@"type"] : nil; + if (!tag || !type) { + return; + } + LKConnectionRequest *activeRequest = [device.activeRequests lookin_firstFiltered:^BOOL(LKConnectionRequest *obj) { + return [@(obj.type) isEqualToNumber:type] && [@(obj.tag) isEqualToNumber:tag]; + }]; + if (!activeRequest) { + // 请求可能在数据到达前已超时。 + return; + } + [self _didReceiveDataWithChannel:device data:data activeRequest:activeRequest]; + }); }; // 设备连接变更 self.wirelessChannel.deviceBlock = ^(ECOChannelDeviceInfo *device, BOOL isConnected) { - NSLog(@"🚀 Lookin deviceBlock device:%@", device); - if (isConnected && ![self_weak_.connectWirelessDevices containsObject:device]) { - if (!device.authorizedType && ![self_weak_ isWhiteListDevice:device]) { - if (![self_weak_.notConnectWirelessDevices containsObject:device]) { - [self_weak_.notConnectWirelessDevices addObject:device]; + dispatch_async(dispatch_get_main_queue(), ^{ + @strongify(self); + if (isConnected && ![self.connectWirelessDevices containsObject:device]) { + if (!device.authorizedType && ![self isWhiteListDevice:device]) { + if (![self.notConnectWirelessDevices containsObject:device]) { + [self.notConnectWirelessDevices addObject:device]; + } + } else { + [[self _connectToWirelessDevice:device] subscribeNext:^(__unused ECOChannelDeviceInfo *connectedDevice) { + } error:^(__unused NSError *error) { + }]; } - } else { - RACSignal *signal = [self_weak_ _connectToWirelessDevice:device]; - [signal subscribeNext:^(ECOChannelDeviceInfo * _Nullable x) { - NSLog(@"%@", x); - }]; + } else if (!isConnected) { + [self.notConnectWirelessDevices removeObject:device]; + [self.connectWirelessDevices removeObject:device]; + [self.channelWillEnd sendNext:device]; } - } else if (!isConnected) { - [self_weak_.notConnectWirelessDevices removeObject:device]; - [self_weak_.connectWirelessDevices removeObject:device]; - [self_weak_.channelWillEnd sendNext:device]; - } + }); }; // 授权状态变更回调 self.wirelessChannel.authStateChangedBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { - NSLog(@"🚀 Lookin authStateChangedBlock device:%@", device); - void(^block)(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) = self_weak_.authStateChangedBlocks[@(device.hash)]; - if (block) { - block(device, authState); - } else if (authState && [self_weak_ isWhiteListDevice:device] && ![self_weak_.connectWirelessDevices containsObject:device]) { - [[self_weak_ _tryToConnectToWirelessDevice:device] subscribeNext:^(ECOChannelDeviceInfo * _Nullable x) { - NSLog(@"🚀 Lookin auto connect white list device success. device:%@", x); - }]; - } - if (!authState && [self_weak_.connectWirelessDevices containsObject:device]) { - [self_weak_.connectWirelessDevices removeObject:device]; - [self_weak_.channelWillEnd sendNext:device]; - } - }; - // 请求授权状态认证回调 - self.wirelessChannel.requestAuthBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { - NSLog(@"🚀 Lookin requestAuthBlock device:%@ authState:%ld", device, authState); + dispatch_async(dispatch_get_main_queue(), ^{ + @strongify(self); + NSString *identifier = LKWirelessDeviceIdentifier(device); + ECOChannelAuthStateChangedBlock block = identifier.length ? self.authStateChangedBlocks[identifier] : nil; + if (block) { + block(device, authState); + } else if (authState && [self isWhiteListDevice:device] && ![self.connectWirelessDevices containsObject:device]) { + [[self _tryToConnectToWirelessDevice:device] subscribeNext:^(__unused ECOChannelDeviceInfo *connectedDevice) { + } error:^(__unused NSError *error) { + }]; + } + if (!authState && [self.connectWirelessDevices containsObject:device]) { + [self.connectWirelessDevices removeObject:device]; + [self.channelWillEnd sendNext:device]; + } + }); }; } - (RACSignal *)_tryToConnectToWirelessDevice:(ECOChannelDeviceInfo *)device { - if (!device.isConnected) - return [RACSignal error:LookinErr_Inner]; + if (!device.isConnected) { + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless device disconnected.", nil)}]; + return [RACSignal error:error]; + } + + NSString *identifier = LKWirelessDeviceIdentifier(device); + if (!identifier.length) { + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless device identity is invalid.", nil)}]; + return [RACSignal error:error]; + } if (!device.authorizedType) { // 未信任 @weakify(self); - RACSignal *signal = [[RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) { - self_weak_.authStateChangedBlocks[@(device.hash)] = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { + RACSignal *authorizationSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) { + @strongify(self); + if (self.authStateChangedBlocks[identifier]) { + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"A wireless connection request is already in progress.", nil)}]; + [subscriber sendError:error]; + return nil; + } + + __weak ECOChannelAuthStateChangedBlock weakCallback = nil; + ECOChannelAuthStateChangedBlock callback = nil; + callback = [^(ECOChannelDeviceInfo *authorizedDevice, ECOAuthorizeResponseType authState) { + ECOChannelAuthStateChangedBlock currentCallback = self.authStateChangedBlocks[identifier]; + if (currentCallback != weakCallback) { + return; + } + [self.authStateChangedBlocks removeObjectForKey:identifier]; if (authState) { - [subscriber sendNext:device]; + [subscriber sendNext:authorizedDevice]; [subscriber sendCompleted]; - NSLog(@"🚀 Lookin connect device success. device:%@", device); } else { - NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless Connection rejected", nil)}]; - NSLog(@"🚀 Lookin connect reject. device:%@", device); - dispatch_async(dispatch_get_main_queue(), ^{ - AlertErrorText(NSLocalizedString(@"Wireless Connections", nil), NSLocalizedString(@"Wireless Connection rejected", nil), CurrentKeyWindow); - }); + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless Connection rejected", nil)}]; [subscriber sendError:error]; } - }; - return nil; - }] flattenMap:^__kindof RACSignal * _Nullable(ECOChannelDeviceInfo *device) { - return [self_weak_ _connectToWirelessDevice:device]; + } copy]; + weakCallback = callback; + self.authStateChangedBlocks[identifier] = callback; + + BOOL showAuthorizationAlert = ![self.wirelessChannel.whitelistDevices containsObject:identifier]; + [self.wirelessChannel sendAuthorizationMessageToDevice:device + state:ECOAuthorizeResponseType_AllowAlways + showAuthAlert:showAuthorizationAlert]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(LKWirelessAuthorizationTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + ECOChannelAuthStateChangedBlock currentCallback = self.authStateChangedBlocks[identifier]; + if (currentCallback != callback) { + return; + } + [self.authStateChangedBlocks removeObjectForKey:identifier]; + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless connection timed out.", nil)}]; + [subscriber sendError:error]; + }); + + return [RACDisposable disposableWithBlock:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.authStateChangedBlocks[identifier] == callback) { + [self.authStateChangedBlocks removeObjectForKey:identifier]; + } + }); + }]; + }]; + return [authorizationSignal flattenMap:^__kindof RACSignal * _Nullable(ECOChannelDeviceInfo *authorizedDevice) { + return [self_weak_ _connectToWirelessDevice:authorizedDevice]; }]; - NSString *uniId = [NSString stringWithFormat:@"%@_%@",device.uuid, device.appInfo.appId]; - [self.wirelessChannel sendAuthorizationMessageToDevice:device - state:ECOAuthorizeResponseType_AllowAlways - showAuthAlert:![self.wirelessChannel.whitelistDevices containsObject:uniId]]; - return signal; } else { return [self _connectToWirelessDevice:device]; } diff --git a/LookinClient/Connection/LKInspectableApp.h b/LookinClient/Connection/LKInspectableApp.h index b78caa24..21092149 100644 --- a/LookinClient/Connection/LKInspectableApp.h +++ b/LookinClient/Connection/LKInspectableApp.h @@ -12,7 +12,8 @@ #import "LookinCustomAttrModification.h" #import "LookinAttributesGroup.h" -@class Lookin_PTChannel, LookinDisplayItemTrace, LookinInvocationRequest, LookinHierarchyInfo, LookinStaticAsyncUpdateTasksPackage, LookinStaticAsyncUpdateTask; +@class LookinDisplayItemTrace, LookinInvocationRequest, LookinHierarchyInfo, LookinStaticAsyncUpdateTasksPackage, LookinStaticAsyncUpdateTask; +@protocol LookinChannelProtocol; @interface LKInspectableApp : NSObject @@ -20,7 +21,7 @@ @property(nonatomic, strong) LookinAppInfo *appInfo; -@property(nonatomic, weak) Lookin_PTChannel *channel; +@property(nonatomic, weak) id channel; - (RACSignal *)fetchHierarchyData; diff --git a/LookinClient/Launch/LKLaunchViewController.m b/LookinClient/Launch/LKLaunchViewController.m index 601b72bc..9eeeaaf3 100644 --- a/LookinClient/Launch/LKLaunchViewController.m +++ b/LookinClient/Launch/LKLaunchViewController.m @@ -296,10 +296,12 @@ - (void)_handleWireless { }]; }; - if (!device.authorizedType) { - [[LKConnectionManager.sharedInstance connectToWireless:device] subscribeNext:^(ECOChannelDeviceInfo *d) { - connectBlock(d); - }]; + if (!device.authorizedType) { + [[LKConnectionManager.sharedInstance connectToWireless:device] subscribeNext:^(ECOChannelDeviceInfo *d) { + connectBlock(d); + } error:^(NSError *error) { + AlertErrorText(NSLocalizedString(@"Wireless Connections", nil), error.localizedDescription, CurrentKeyWindow); + }]; } else { connectBlock(device); } diff --git a/LookinClient/Launch/LKLaunchWirelessDeviceView.m b/LookinClient/Launch/LKLaunchWirelessDeviceView.m index fa919809..7f6da210 100644 --- a/LookinClient/Launch/LKLaunchWirelessDeviceView.m +++ b/LookinClient/Launch/LKLaunchWirelessDeviceView.m @@ -112,18 +112,18 @@ - (void)setDevice:(ECOChannelDeviceInfo *)device { } self.titleLabel.stringValue = [NSString stringWithFormat:@"%@ - %@(%@.%@)", device.deviceName, device.appInfo.appName, device.appInfo.appVersion, device.appInfo.appShortVersion]; self.subtitleLabel.stringValue = [NSString stringWithFormat:@"iOS %@", device.systemVersion]; - self.stateLabel.stringValue = device.authorizedType ? @"已连接" : @"点击连接"; + self.stateLabel.stringValue = device.authorizedType ? NSLocalizedString(@"Connected", nil) : NSLocalizedString(@"Click to connect", nil); self.autoConnectControl.hidden = device.authorizedType != ECOAuthorizeResponseType_AllowAlways; - self.autoConnectControl.label.stringValue = @"自动连接"; + self.autoConnectControl.label.stringValue = NSLocalizedString(@"Connect automatically", nil); BOOL isWhiteDevice = [LKConnectionManager.sharedInstance isWhiteListDevice:device]; - self.autoConnectControl.rightImage = [NSImage imageWithSystemSymbolName:isWhiteDevice ? @"checkmark.square" : @"square" accessibilityDescription:nil]; + self.autoConnectControl.rightImage = [NSImage imageWithSystemSymbolName:isWhiteDevice ? @"checkmark.square" : @"square" accessibilityDescription:NSLocalizedString(@"Connect automatically", nil)]; } - (void)handleAutoConnectControl { BOOL isWhiteDevice = [LKConnectionManager.sharedInstance isWhiteListDevice:self.device]; isWhiteDevice = !isWhiteDevice; - self.autoConnectControl.rightImage = [NSImage imageWithSystemSymbolName:isWhiteDevice ? @"checkmark.square" : @"square" accessibilityDescription:nil]; + self.autoConnectControl.rightImage = [NSImage imageWithSystemSymbolName:isWhiteDevice ? @"checkmark.square" : @"square" accessibilityDescription:NSLocalizedString(@"Connect automatically", nil)]; [LKConnectionManager.sharedInstance setWhiteListDevice:self.device white:isWhiteDevice]; } diff --git a/LookinClient/Static/LKStaticWindowController.m b/LookinClient/Static/LKStaticWindowController.m index f897f914..a48773a9 100644 --- a/LookinClient/Static/LKStaticWindowController.m +++ b/LookinClient/Static/LKStaticWindowController.m @@ -159,10 +159,12 @@ - (void)popupNotConnnectWirelessInspectableApps { }]; }; - if (!device.authorizedType) { - [[LKConnectionManager.sharedInstance connectToWireless:device] subscribeNext:^(ECOChannelDeviceInfo *d) { - connectBlock(d); - }]; + if (!device.authorizedType) { + [[LKConnectionManager.sharedInstance connectToWireless:device] subscribeNext:^(ECOChannelDeviceInfo *d) { + connectBlock(d); + } error:^(NSError *error) { + AlertErrorText(NSLocalizedString(@"Wireless Connections", nil), error.localizedDescription, CurrentKeyWindow); + }]; } else { connectBlock(device); } @@ -435,24 +437,6 @@ - (void)_handleFreeRotation { [[LKPreferenceManager mainManager].freeRotation setBOOLValue:!boolValue ignoreSubscriber:nil]; } -#pragma mark - Others - -- (void)_showUSBLowSpeedTipsIfNeeded { - if (TutorialMng.hasAlreadyShowedTipsThisLaunch || TutorialMng.USBLowSpeed) { - return; - } - if (!InspectingApp || InspectingApp.appInfo.isWireless || InspectingApp.appInfo.deviceType == LookinAppInfoDeviceSimulator || [LKStaticHierarchyDataSource sharedInstance].flatItems.count < 170) { - return; - } - - TutorialMng.hasAlreadyShowedTipsThisLaunch = YES; - dispatch_async(dispatch_get_main_queue(), ^{ - [[LKTutorialManager sharedInstance] showPopoverOfView:self.toolbarItemsMap[LKToolBarIdentifier_Reload].view text:NSLocalizedString(@"Inspecting via USB is slower than inspecting a Xcode simulator.", nil) learned:^{ - [LKTutorialManager sharedInstance].USBLowSpeed = YES; - }]; - }); -} - #pragma mark - - (void)appMenuManagerDidSelectReload { diff --git a/LookinClient/en.lproj/Localizable.strings b/LookinClient/en.lproj/Localizable.strings index 89c54398..3098e4b3 100644 --- a/LookinClient/en.lproj/Localizable.strings +++ b/LookinClient/en.lproj/Localizable.strings @@ -2,6 +2,13 @@ "Syncing…" = "Syncing…"; "Select App" = "Select App"; "Wireless Connections" = "Wireless Connections"; +"Connected" = "Connected"; +"Click to connect" = "Click to connect"; +"Connect automatically" = "Connect automatically"; +"Wireless device disconnected." = "Wireless device disconnected."; +"Wireless device identity is invalid." = "Wireless device identity is invalid."; +"A wireless connection request is already in progress." = "A wireless connection request is already in progress."; +"Wireless connection timed out." = "Wireless connection timed out."; "View" = "View"; "Zoom" = "Zoom"; "Console" = "Console"; diff --git a/LookinClient/zh-Hans.lproj/Localizable.strings b/LookinClient/zh-Hans.lproj/Localizable.strings index 33f64bd1..3041f748 100644 --- a/LookinClient/zh-Hans.lproj/Localizable.strings +++ b/LookinClient/zh-Hans.lproj/Localizable.strings @@ -2,6 +2,13 @@ "Syncing…" = "图像传输中…"; "Select App" = "App 切换"; "Wireless Connections" = "无线连接"; +"Connected" = "已连接"; +"Click to connect" = "点击连接"; +"Connect automatically" = "自动连接"; +"Wireless device disconnected." = "无线设备已断开连接。"; +"Wireless device identity is invalid." = "无线设备标识无效。"; +"A wireless connection request is already in progress." = "已有一个无线连接请求正在进行。"; +"Wireless connection timed out." = "无线连接超时。"; "View" = "图像"; "Zoom" = "大小"; "Console" = "控制台"; diff --git a/Podfile b/Podfile index 7953e06c..44ab8f0d 100644 --- a/Podfile +++ b/Podfile @@ -7,7 +7,7 @@ target 'LookinClient' do pod 'AppCenter' pod 'ReactiveObjC', '3.1.0' pod 'Sparkle', '~> 1.0' - pod 'LookinShared/Wireless', :git=>'https://github.com/nova286/LookinServer.git', :commit => '6ba8231d589c7404064ca8d571c49746a9f67366' + pod 'LookinShared/Wireless', :git=>'https://github.com/nova286/LookinServer.git', :commit => '01460f6d6b857893afde77f7bbe2ca7adb70c012' #pod 'LookinShared', :path=>'../LookinServer/' end diff --git a/Podfile.lock b/Podfile.lock index 17d6385c..5154c841 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -15,7 +15,7 @@ PODS: DEPENDENCIES: - AppCenter - - LookinShared/Wireless (from `https://github.com/nova286/LookinServer.git`, commit `6ba8231d589c7404064ca8d571c49746a9f67366`) + - LookinShared/Wireless (from `https://github.com/nova286/LookinServer.git`, commit `01460f6d6b857893afde77f7bbe2ca7adb70c012`) - ReactiveObjC (= 3.1.0) - Sparkle (~> 1.0) @@ -28,21 +28,21 @@ SPEC REPOS: EXTERNAL SOURCES: LookinShared: - :commit: 6ba8231d589c7404064ca8d571c49746a9f67366 + :commit: 01460f6d6b857893afde77f7bbe2ca7adb70c012 :git: https://github.com/nova286/LookinServer.git CHECKOUT OPTIONS: LookinShared: - :commit: 6ba8231d589c7404064ca8d571c49746a9f67366 + :commit: 01460f6d6b857893afde77f7bbe2ca7adb70c012 :git: https://github.com/nova286/LookinServer.git SPEC CHECKSUMS: AppCenter: b0eca112a27b71e97488ffa1949ee38c7abd4b79 CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 - LookinShared: 6a5ea30b57825f5d8619726ffc72812b4c025771 + LookinShared: 5817ca9a7d7e961026f690eadd4ebd9bf293db00 ReactiveObjC: 2a38ea15335de4119d8b17caf1db1484f61db902 Sparkle: 23f98b268284c8c03e6228230fc8f1807ef041d5 -PODFILE CHECKSUM: 766ca5f4a0c237b7925e65ee52b8c4eb9c845837 +PODFILE CHECKSUM: 435182e61e9172a68a6c9d3b888128cd9bd2bb7c COCOAPODS: 1.17.0 diff --git a/README.md b/README.md index 2214b621..9f707eff 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,19 @@ See [`LookinCLI/README.md`](LookinCLI/README.md) for CLI usage and [`Docs/lookin ## via Swift Package Manager: `https://github.com/QMUI/LookinServer/` +## Experimental wireless connection + +This fork pairs with the `codex/upstream-wireless` branch of [nova286/LookinServer](https://github.com/nova286/LookinServer). The inspected app and Mac must be on the same trusted local network. Start wireless discovery explicitly in the iOS app as documented by LookinServer, then use **Wireless Connections** in the launch window or workspace toolbar and confirm the request on the iOS device. + +The transport is not encrypted or cryptographically authenticated. Remembered identifiers reduce repeated prompts but do not prove peer identity, so this feature is for Debug builds on trusted networks only. The client limits authorization waits to 30 seconds, accepts bounded protocol frames from the paired server, does not log device identifiers, and supports one inspecting Mac per app session. + +Manual verification requires a physical iOS device: + +1. Start the Debug app and post `Lookin_startWirelessConnection` after it becomes active. +2. Open this Lookin client, choose **Wireless Connections**, and select the device. +3. Approve the prompt on the iOS device, load the hierarchy, and verify an attribute edit. +4. Post `Lookin_endWirelessConnection` and verify that the wireless session closes. + ## Experimental SwiftUI inspector This fork pairs with the `codex/swiftui-attached-macro` branch of [nova286/LookinServer](https://github.com/nova286/LookinServer). When the inspected app exposes SwiftUI semantic nodes, the toolbar shows a **SwiftUI** mode with source types and source locations. Registered nodes provide editable temporary overrides for size, offset, scale, visibility, opacity, and background color. After an edit, Lookin automatically reloads the hierarchy and screenshot while preserving the selected node and expansion state. @@ -106,6 +119,14 @@ CLI 使用见 [`LookinCLI/README.md`](LookinCLI/README.md),开发、构建、 ## 通过 Swift Package Manager: `https://github.com/QMUI/LookinServer/` +## 实验性无线连接 + +此 fork 与 [nova286/LookinServer](https://github.com/nova286/LookinServer) 的 `codex/upstream-wireless` 分支配套使用。被检查 App 与 Mac 必须处于同一个可信局域网;按 LookinServer 文档在 iOS App 中显式启动无线发现后,在启动窗口或工作区工具栏点击 **Wireless Connections**,选择设备并在 iOS 端确认。 + +传输内容没有加密,记住的设备标识也不是密码学身份证明,因此只应用于可信网络上的 Debug 构建。客户端会在 30 秒后结束未完成的授权等待,配套 Server 会限制协议帧大小;两端不会记录设备标识,且同一 App 会话同时只允许一台 Mac 检查。 + +完整验证需要真机:启动 Debug App 并在 active 后发送 `Lookin_startWirelessConnection`,从 macOS 客户端选择设备,在 iOS 端允许连接,然后验证层级加载与属性修改;最后发送 `Lookin_endWirelessConnection` 并确认会话关闭。 + ## 实验性 SwiftUI Inspector 此 fork 与 [nova286/LookinServer](https://github.com/nova286/LookinServer) 的 `codex/swiftui-attached-macro` 分支配套使用。被检查的 App 暴露 SwiftUI 语义节点后,工具栏会出现独立的 **SwiftUI** 模式,显示真实源码类型和源码位置。每个注册节点都提供尺寸、偏移、缩放、隐藏、透明度和背景色等临时调试属性;修改后客户端会自动重新抓取层级与截图,并保留当前选中节点和展开状态。 diff --git a/UPSTREAM_PATCHES.md b/UPSTREAM_PATCHES.md index 3d9e07a5..3050502a 100644 --- a/UPSTREAM_PATCHES.md +++ b/UPSTREAM_PATCHES.md @@ -5,5 +5,6 @@ This fork incorporates selected community pull requests that were not merged by | Local commits | Upstream pull request | Original commit | Local adaptation | | --- | --- | --- | --- | | `7d801b3`, `6711b86` | [hughkli/Lookin#63](https://github.com/hughkli/Lookin/pull/63) | `700efa8a73600ab69f4c17631970850c5e46bd69` | Targets the maintained forks, pins LookinShared, verifies release checksums and signatures, removes quarantine bypass and npm install-time scripts, applies GPL-3.0-only packaging, and builds CLI artifacts in GitHub Actions. | +| `codex/upstream-wireless` | [hughkli/Lookin#42](https://github.com/hughkli/Lookin/pull/42) | `02465ec`, `9327cf3`, `faa6f62`, `9e8ff7d`, `9e3b10c` | Paired with `nova286/LookinServer#164`; retains the existing SwiftUI toolbar, generalizes request/push/cancel paths to both Peertalk and wireless channels, bounds authorization lifetime, serializes UI state changes on the main thread, localizes new UI, and removes device-identity logs. Personal signing changes, merge commits, and dependencies on contributor forks were intentionally excluded. | Large or cross-repository features are ported on dedicated branches instead of being applied directly. Every imported patch must build against the fork's current default branch before it is merged. From e09989ffc3dc47eae6f19d800d60a6a652a277dc Mon Sep 17 00:00:00 2001 From: hongbo <1049145827@qq.com> Date: Sat, 18 Jul 2026 17:13:50 +0800 Subject: [PATCH 7/8] Keep the CLI on its Peertalk source set --- LookinCLI/build.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/LookinCLI/build.sh b/LookinCLI/build.sh index abd04b31..ce350452 100755 --- a/LookinCLI/build.sh +++ b/LookinCLI/build.sh @@ -37,7 +37,9 @@ done SHARED_SOURCES=() while IFS= read -r source_file; do SHARED_SOURCES+=("${source_file}") -done < <(find "${LOOKIN_SHARED_DIR}/Src/Main/Shared" "${LOOKIN_SHARED_DIR}/Src/Base" -name '*.m' | sort) +done < <(find "${LOOKIN_SHARED_DIR}/Src/Main/Shared" "${LOOKIN_SHARED_DIR}/Src/Base" \ + -path "${LOOKIN_SHARED_DIR}/Src/Main/Shared/Channel" -prune -o \ + -name '*.m' -print | sort) xcrun --sdk macosx clang \ "${ARCH_FLAGS[@]}" \ From 6b3a27db4771b7861a34e675bdc2aba1b7b9ea7b Mon Sep 17 00:00:00 2001 From: hongbo <1049145827@qq.com> Date: Sat, 18 Jul 2026 20:14:07 +0800 Subject: [PATCH 8/8] Fix wireless device retry and connection UI --- LookinClient/Connection/LKConnectionManager.m | 98 ++++++++++++------- LookinClient/Launch/LKLaunchViewController.m | 16 ++- .../Launch/LKLaunchWirelessDeviceView.m | 36 +++++-- ...MenuPopoverWirelessDevicesListController.m | 2 +- LookinClient/en.lproj/Localizable.strings | 1 - .../zh-Hans.lproj/Localizable.strings | 1 - 6 files changed, 99 insertions(+), 55 deletions(-) diff --git a/LookinClient/Connection/LKConnectionManager.m b/LookinClient/Connection/LKConnectionManager.m index d7ff797b..9b0e673e 100644 --- a/LookinClient/Connection/LKConnectionManager.m +++ b/LookinClient/Connection/LKConnectionManager.m @@ -60,6 +60,17 @@ - (void)setActiveRequests:(NSMutableSet *)activeRequests @end +@interface LKWirelessAuthorizationRequest : NSObject + +@property(nonatomic, strong) ECOChannelDeviceInfo *device; +@property(nonatomic, strong) id subscriber; + +@end + +@implementation LKWirelessAuthorizationRequest + +@end + @interface LKSimulatorConnectionPort : NSObject @property(nonatomic, assign) int portNumber; @@ -101,7 +112,7 @@ @interface LKConnectionManager () @property(nonatomic, strong) NSMutableArray *connectWirelessDevices; @property(nonatomic, strong) NSMutableArray *notConnectWirelessDevices; @property(nonatomic, strong) ECOChannelManager *wirelessChannel; -@property(nonatomic, strong) NSMutableDictionary *authStateChangedBlocks; +@property(nonatomic, strong) NSMutableDictionary *pendingWirelessAuthorizationRequests; @property(nonatomic, strong) NSMutableArray *whitelistDevices; @@ -139,7 +150,7 @@ - (instancetype)init { self.allUSBPorts = [NSMutableArray array]; self.connectWirelessDevices = [NSMutableArray array]; self.notConnectWirelessDevices = [NSMutableArray array]; - self.authStateChangedBlocks = [NSMutableDictionary dictionary]; + self.pendingWirelessAuthorizationRequests = [NSMutableDictionary dictionary]; [self _startListeningForWirelessDevices]; [self _startListeningForUSBDevices]; @@ -550,12 +561,25 @@ - (void)_startListeningForWirelessDevices { } } else { [[self _connectToWirelessDevice:device] subscribeNext:^(__unused ECOChannelDeviceInfo *connectedDevice) { - } error:^(__unused NSError *error) { + } error:^(NSError *error) { + if (device.isConnected && ![self.notConnectWirelessDevices containsObject:device]) { + [self.notConnectWirelessDevices addObject:device]; + } + NSLog(@"Lookin - wireless device ping failed: %@", error); }]; } } else if (!isConnected) { [self.notConnectWirelessDevices removeObject:device]; [self.connectWirelessDevices removeObject:device]; + NSString *identifier = LKWirelessDeviceIdentifier(device); + LKWirelessAuthorizationRequest *request = identifier.length ? self.pendingWirelessAuthorizationRequests[identifier] : nil; + if (request && request.device == device) { + [self.pendingWirelessAuthorizationRequests removeObjectForKey:identifier]; + id subscriber = request.subscriber; + request.subscriber = nil; + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless device disconnected.", nil)}]; + [subscriber sendError:error]; + } [self.channelWillEnd sendNext:device]; } }); @@ -565,12 +589,25 @@ - (void)_startListeningForWirelessDevices { dispatch_async(dispatch_get_main_queue(), ^{ @strongify(self); NSString *identifier = LKWirelessDeviceIdentifier(device); - ECOChannelAuthStateChangedBlock block = identifier.length ? self.authStateChangedBlocks[identifier] : nil; - if (block) { - block(device, authState); + LKWirelessAuthorizationRequest *request = identifier.length ? self.pendingWirelessAuthorizationRequests[identifier] : nil; + if (request && request.device == device) { + [self.pendingWirelessAuthorizationRequests removeObjectForKey:identifier]; + id subscriber = request.subscriber; + request.subscriber = nil; + if (authState) { + [subscriber sendNext:device]; + [subscriber sendCompleted]; + } else { + NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless Connection rejected", nil)}]; + [subscriber sendError:error]; + } } else if (authState && [self isWhiteListDevice:device] && ![self.connectWirelessDevices containsObject:device]) { [[self _tryToConnectToWirelessDevice:device] subscribeNext:^(__unused ECOChannelDeviceInfo *connectedDevice) { - } error:^(__unused NSError *error) { + } error:^(NSError *error) { + if (device.isConnected && ![self.notConnectWirelessDevices containsObject:device]) { + [self.notConnectWirelessDevices addObject:device]; + } + NSLog(@"Lookin - trusted wireless device reconnect failed: %@", error); }]; } if (!authState && [self.connectWirelessDevices containsObject:device]) { @@ -598,30 +635,18 @@ - (void)_startListeningForWirelessDevices { @weakify(self); RACSignal *authorizationSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) { @strongify(self); - if (self.authStateChangedBlocks[identifier]) { - NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"A wireless connection request is already in progress.", nil)}]; - [subscriber sendError:error]; - return nil; + LKWirelessAuthorizationRequest *previousRequest = self.pendingWirelessAuthorizationRequests[identifier]; + if (previousRequest) { + [self.pendingWirelessAuthorizationRequests removeObjectForKey:identifier]; + id previousSubscriber = previousRequest.subscriber; + previousRequest.subscriber = nil; + [previousSubscriber sendCompleted]; } - __weak ECOChannelAuthStateChangedBlock weakCallback = nil; - ECOChannelAuthStateChangedBlock callback = nil; - callback = [^(ECOChannelDeviceInfo *authorizedDevice, ECOAuthorizeResponseType authState) { - ECOChannelAuthStateChangedBlock currentCallback = self.authStateChangedBlocks[identifier]; - if (currentCallback != weakCallback) { - return; - } - [self.authStateChangedBlocks removeObjectForKey:identifier]; - if (authState) { - [subscriber sendNext:authorizedDevice]; - [subscriber sendCompleted]; - } else { - NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless Connection rejected", nil)}]; - [subscriber sendError:error]; - } - } copy]; - weakCallback = callback; - self.authStateChangedBlocks[identifier] = callback; + LKWirelessAuthorizationRequest *request = [LKWirelessAuthorizationRequest new]; + request.device = device; + request.subscriber = subscriber; + self.pendingWirelessAuthorizationRequests[identifier] = request; BOOL showAuthorizationAlert = ![self.wirelessChannel.whitelistDevices containsObject:identifier]; [self.wirelessChannel sendAuthorizationMessageToDevice:device @@ -629,20 +654,23 @@ - (void)_startListeningForWirelessDevices { showAuthAlert:showAuthorizationAlert]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(LKWirelessAuthorizationTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ - ECOChannelAuthStateChangedBlock currentCallback = self.authStateChangedBlocks[identifier]; - if (currentCallback != callback) { + LKWirelessAuthorizationRequest *currentRequest = self.pendingWirelessAuthorizationRequests[identifier]; + if (currentRequest != request) { return; } - [self.authStateChangedBlocks removeObjectForKey:identifier]; + [self.pendingWirelessAuthorizationRequests removeObjectForKey:identifier]; + id timeoutSubscriber = request.subscriber; + request.subscriber = nil; NSError *error = [NSError errorWithDomain:LookinErrorDomain code:LookinErrCode_Inner userInfo:@{NSLocalizedDescriptionKey: NSLocalizedString(@"Wireless connection timed out.", nil)}]; - [subscriber sendError:error]; + [timeoutSubscriber sendError:error]; }); return [RACDisposable disposableWithBlock:^{ dispatch_async(dispatch_get_main_queue(), ^{ - if (self.authStateChangedBlocks[identifier] == callback) { - [self.authStateChangedBlocks removeObjectForKey:identifier]; + if (self.pendingWirelessAuthorizationRequests[identifier] == request) { + [self.pendingWirelessAuthorizationRequests removeObjectForKey:identifier]; } + request.subscriber = nil; }); }]; }]; diff --git a/LookinClient/Launch/LKLaunchViewController.m b/LookinClient/Launch/LKLaunchViewController.m index 9eeeaaf3..18e06d36 100644 --- a/LookinClient/Launch/LKLaunchViewController.m +++ b/LookinClient/Launch/LKLaunchViewController.m @@ -296,16 +296,12 @@ - (void)_handleWireless { }]; }; - if (!device.authorizedType) { - [[LKConnectionManager.sharedInstance connectToWireless:device] subscribeNext:^(ECOChannelDeviceInfo *d) { - connectBlock(d); - } error:^(NSError *error) { - AlertErrorText(NSLocalizedString(@"Wireless Connections", nil), error.localizedDescription, CurrentKeyWindow); - }]; - } else { - connectBlock(device); - } - }; + [[LKConnectionManager.sharedInstance connectToWireless:device] subscribeNext:^(ECOChannelDeviceInfo *d) { + connectBlock(d); + } error:^(NSError *error) { + AlertErrorText(NSLocalizedString(@"Wireless Connections", nil), error.localizedDescription, CurrentKeyWindow); + }]; + }; popover.behavior = NSPopoverBehaviorTransient; popover.animates = NO; popover.contentSize = vc.bestSize; diff --git a/LookinClient/Launch/LKLaunchWirelessDeviceView.m b/LookinClient/Launch/LKLaunchWirelessDeviceView.m index 7f6da210..87b22385 100644 --- a/LookinClient/Launch/LKLaunchWirelessDeviceView.m +++ b/LookinClient/Launch/LKLaunchWirelessDeviceView.m @@ -58,6 +58,7 @@ - (instancetype)initWithFrame:(NSRect)frameRect { _iconMarginRight = 6; self.titleLabel.font = NSFontMake(12); self.subtitleLabel.font = NSFontMake(11); + self.stateLabel.font = NSFontMake(11); [self.autoConnectControl addTarget:self clickAction:@selector(handleAutoConnectControl)]; } @@ -69,20 +70,28 @@ - (void)layout { self.hoverBgLayer.frame = self.layer.bounds; - $(self.iconImageView).sizeToFit.y(_insets.top); + $(self.iconImageView).sizeToFit.x(_insets.left).y(_insets.top); $(self.titleLabel).sizeToFit; $(self.subtitleLabel).sizeToFit.y(self.titleLabel.$maxY + 2); $(self.titleLabel, self.subtitleLabel).x(self.iconImageView.$maxX + _iconMarginRight).groupMidY(self.iconImageView.$midY); - $(self.autoConnectControl).sizeToFit.maxX(self.$maxX - _insets.right - 10).midY(self.subtitleLabel.$midY); - $(self.stateLabel).sizeToFit.maxX(self.autoConnectControl.hidden ? self.autoConnectControl.$maxX : self.autoConnectControl.$x - 6).midY(self.subtitleLabel.$midY); - - $(self.iconImageView, self.titleLabel, self.subtitleLabel).groupHorAlign.offsetX(-2); + CGFloat trailingX = self.$width - _insets.right; + if (self.autoConnectControl.hidden) { + $(self.stateLabel).sizeToFit.maxX(trailingX).midY(self.iconImageView.$midY); + } else { + $(self.autoConnectControl).sizeToFit.maxX(trailingX).midY(self.iconImageView.$midY); + $(self.stateLabel).sizeToFit.maxX(self.autoConnectControl.$x - 8).midY(self.iconImageView.$midY); + } } - (NSSize)sizeThatFits:(NSSize)limitedSize { - CGFloat width = self.iconImageView.image.size.width + _iconMarginRight + MAX([self.titleLabel sizeThatFits:NSSizeMax].width, [self.subtitleLabel sizeThatFits:NSSizeMax].width) + _insets.left + _insets.right; + CGFloat labelsWidth = MAX([self.titleLabel sizeThatFits:NSSizeMax].width, [self.subtitleLabel sizeThatFits:NSSizeMax].width); + CGFloat trailingWidth = [self.stateLabel sizeThatFits:NSSizeMax].width; + if (!self.autoConnectControl.hidden) { + trailingWidth += 8 + [self.autoConnectControl sizeThatFits:NSSizeMax].width; + } + CGFloat width = _insets.left + self.iconImageView.image.size.width + _iconMarginRight + labelsWidth + 16 + trailingWidth + _insets.right; CGFloat height = _insets.top + self.iconImageView.image.size.height + _insets.bottom; return NSMakeSize(width, height); } @@ -110,14 +119,27 @@ - (void)setDevice:(ECOChannelDeviceInfo *)device { default: break; } - self.titleLabel.stringValue = [NSString stringWithFormat:@"%@ - %@(%@.%@)", device.deviceName, device.appInfo.appName, device.appInfo.appVersion, device.appInfo.appShortVersion]; + self.titleLabel.stringValue = [NSString stringWithFormat:@"%@ - %@", device.deviceName, device.appInfo.appName]; self.subtitleLabel.stringValue = [NSString stringWithFormat:@"iOS %@", device.systemVersion]; self.stateLabel.stringValue = device.authorizedType ? NSLocalizedString(@"Connected", nil) : NSLocalizedString(@"Click to connect", nil); + self.stateLabel.textColor = device.authorizedType ? [NSColor secondaryLabelColor] : [NSColor linkColor]; self.autoConnectControl.hidden = device.authorizedType != ECOAuthorizeResponseType_AllowAlways; self.autoConnectControl.label.stringValue = NSLocalizedString(@"Connect automatically", nil); BOOL isWhiteDevice = [LKConnectionManager.sharedInstance isWhiteListDevice:device]; self.autoConnectControl.rightImage = [NSImage imageWithSystemSymbolName:isWhiteDevice ? @"checkmark.square" : @"square" accessibilityDescription:NSLocalizedString(@"Connect automatically", nil)]; + [self setNeedsLayout:YES]; +} + +- (NSView *)hitTest:(NSPoint)point { + NSView *hitView = [super hitTest:point]; + if (!hitView) { + return nil; + } + if (!self.autoConnectControl.hidden && (hitView == self.autoConnectControl || [hitView isDescendantOf:self.autoConnectControl])) { + return self.autoConnectControl; + } + return self; } - (void)handleAutoConnectControl { diff --git a/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.m b/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.m index 0792e885..b6692958 100644 --- a/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.m +++ b/LookinClient/Toolbar/LKMenuPopoverWirelessDevicesListController.m @@ -114,7 +114,7 @@ - (void)viewDidLayout { - (void)handleClickAppView:(LKLaunchWirelessDeviceView *)view { ECOChannelDeviceInfo *device = view.device; - if (!device.authorizedType && self.didSelectDevice) { + if (self.didSelectDevice) { self.didSelectDevice(device); } } diff --git a/LookinClient/en.lproj/Localizable.strings b/LookinClient/en.lproj/Localizable.strings index 3098e4b3..c99ea1ce 100644 --- a/LookinClient/en.lproj/Localizable.strings +++ b/LookinClient/en.lproj/Localizable.strings @@ -7,7 +7,6 @@ "Connect automatically" = "Connect automatically"; "Wireless device disconnected." = "Wireless device disconnected."; "Wireless device identity is invalid." = "Wireless device identity is invalid."; -"A wireless connection request is already in progress." = "A wireless connection request is already in progress."; "Wireless connection timed out." = "Wireless connection timed out."; "View" = "View"; "Zoom" = "Zoom"; diff --git a/LookinClient/zh-Hans.lproj/Localizable.strings b/LookinClient/zh-Hans.lproj/Localizable.strings index 3041f748..c462a876 100644 --- a/LookinClient/zh-Hans.lproj/Localizable.strings +++ b/LookinClient/zh-Hans.lproj/Localizable.strings @@ -7,7 +7,6 @@ "Connect automatically" = "自动连接"; "Wireless device disconnected." = "无线设备已断开连接。"; "Wireless device identity is invalid." = "无线设备标识无效。"; -"A wireless connection request is already in progress." = "已有一个无线连接请求正在进行。"; "Wireless connection timed out." = "无线连接超时。"; "View" = "图像"; "Zoom" = "大小";