diff --git a/Lookin.xcodeproj/project.pbxproj b/Lookin.xcodeproj/project.pbxproj index 60a5296b..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 */ @@ -920,10 +926,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 */, ); @@ -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 */, @@ -1643,6 +1655,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 +1697,7 @@ "$(inherited)", "COCOAPODS=1", "SHOULD_COMPILE_LOOKIN_SERVER=1", + "LOOKIN_SERVER_WIRELESS=1", ); INFOPLIST_FILE = LookinClient/LookinClient_Info.plist; LD_RUNPATH_SEARCH_PATHS = ( 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[@]}" \ 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 b44adcff..40d5b9d6 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 端 @@ -28,16 +38,24 @@ /// 该方法不会 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; + +- (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; +- (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; @@ -46,3 +64,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..9b0e673e 100644 --- a/LookinClient/Connection/LKConnectionManager.m +++ b/LookinClient/Connection/LKConnectionManager.m @@ -14,8 +14,9 @@ #import "LookinAppInfo.h" #import "LKConnectionRequest.h" #import "LKServerVersionRequestor.h" +#import "ECOChannelManager.h" -static NSIndexSet * PushFrameTypeList() { +static NSIndexSet *PushFrameTypeList(void) { static NSIndexSet *list; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ @@ -25,12 +26,15 @@ return list; } -@interface Lookin_PTChannel (LKConnection) +static NSString *const LKWhiteListDevicesKey = @"LKWhiteListDevicesKey"; +static NSTimeInterval const LKWirelessAuthorizationTimeout = 30; -/// 已经发送但尚未收到全部回复的请求 -@property(nonatomic, strong) NSMutableSet *activeRequests; - -@end +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) @@ -44,6 +48,29 @@ - (void)setActiveRequests:(NSMutableSet *)activeRequests @end +@implementation ECOChannelDeviceInfo (LKConnection) + +- (void)setActiveRequests:(NSMutableSet *)activeRequests { + [self lookin_bindObject:activeRequests forKey:@"activeRequest"]; +} + +- (NSMutableSet *)activeRequests { + return [self lookin_getBindObjectForKey:@"activeRequest"]; +} + +@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; @@ -82,6 +109,12 @@ @interface LKConnectionManager () @property(nonatomic, copy) NSArray *allSimulatorPorts; @property(nonatomic, strong) NSMutableArray *allUSBPorts; +@property(nonatomic, strong) NSMutableArray *connectWirelessDevices; +@property(nonatomic, strong) NSMutableArray *notConnectWirelessDevices; +@property(nonatomic, strong) ECOChannelManager *wirelessChannel; +@property(nonatomic, strong) NSMutableDictionary *pendingWirelessAuthorizationRequests; + +@property(nonatomic, strong) NSMutableArray *whitelistDevices; @end @@ -104,7 +137,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,22 +148,56 @@ - (instancetype)init { ports; }); self.allUSBPorts = [NSMutableArray array]; - + self.connectWirelessDevices = [NSMutableArray array]; + self.notConnectWirelessDevices = [NSMutableArray array]; + self.pendingWirelessAuthorizationRequests = [NSMutableDictionary dictionary]; + + [self _startListeningForWirelessDevices]; [self _startListeningForUSBDevices]; - + [[LKServerVersionRequestor shared] preload]; } 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]]] map:^id _Nullable(RACTuple * _Nullable value) { - RACTupleUnpack(NSArray *simulatorChannels, NSArray *usbChannels) = value; - NSArray *connectedChannels = [simulatorChannels arrayByAddingObjectsFromArray:usbChannels]; - return connectedChannels; + [self _tryToConnectAllUSBDevices], + [self _tryToConnectToAllWirelessDevice]]] map:^id _Nullable(RACTuple * _Nullable value) { + RACTupleUnpack(NSArray *simulatorChannels, NSArray *usbChannels, NSArray *wirelessDevices) = value; + NSMutableArray> *connectedChannels = [NSMutableArray array]; + [connectedChannels addObjectsFromArray:simulatorChannels]; + [connectedChannels addObjectsFromArray:usbChannels]; + [connectedChannels addObjectsFromArray:wirelessDevices]; + return connectedChannels.copy; }]; } @@ -159,7 +226,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 +274,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,22 +296,37 @@ - (RACSignal *)_connectToUSBPort:(LKUSBConnectionPort *)port { }]; } +- (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:devices]; +} + #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; @@ -254,7 +336,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 +355,11 @@ - (RACSignal *)requestWithType:(unsigned int)requestType data:(NSObject *)reques [subscriber sendCompleted]; }]; } - + } fail:^(NSError *error) { // ping 失败了 [subscriber sendError:error]; - + } completion:nil]; return nil; }]; @@ -290,26 +372,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 +405,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 +427,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,33 +446,45 @@ - (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 { +- (void)cancelRequestWithType:(unsigned int)requestType channel:(id)channel { LKConnectionRequest *activeRequest = [channel.activeRequests lookin_firstFiltered:^BOOL(LKConnectionRequest *obj) { return obj.type == requestType; }]; @@ -397,10 +501,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 +514,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 +526,195 @@ - (void)_startListeningForUSBDevices { }]; } +- (void)_startListeningForWirelessDevices { + if (!self.wirelessChannel) { + self.wirelessChannel = ECOChannelManager.new; + } + @weakify(self); + // 接收到数据回调 + self.wirelessChannel.receivedBlock = ^(ECOChannelDeviceInfo *device, NSData *data, NSDictionary *extraInfo) { + 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) { + 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:^(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]; + } + }); + }; + // 授权状态变更回调 + self.wirelessChannel.authStateChangedBlock = ^(ECOChannelDeviceInfo *device, ECOAuthorizeResponseType authState) { + dispatch_async(dispatch_get_main_queue(), ^{ + @strongify(self); + 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; + 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:^(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]) { + [self.connectWirelessDevices removeObject:device]; + [self.channelWillEnd sendNext:device]; + } + }); + }; +} + +- (RACSignal *)_tryToConnectToWirelessDevice:(ECOChannelDeviceInfo *)device { + 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 *authorizationSignal = [RACSignal createSignal:^RACDisposable * _Nullable(id _Nonnull subscriber) { + @strongify(self); + LKWirelessAuthorizationRequest *previousRequest = self.pendingWirelessAuthorizationRequests[identifier]; + if (previousRequest) { + [self.pendingWirelessAuthorizationRequests removeObjectForKey:identifier]; + id previousSubscriber = previousRequest.subscriber; + previousRequest.subscriber = nil; + [previousSubscriber sendCompleted]; + } + + 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 + state:ECOAuthorizeResponseType_AllowAlways + showAuthAlert:showAuthorizationAlert]; + + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(LKWirelessAuthorizationTimeout * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + LKWirelessAuthorizationRequest *currentRequest = self.pendingWirelessAuthorizationRequests[identifier]; + if (currentRequest != request) { + return; + } + [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)}]; + [timeoutSubscriber sendError:error]; + }); + + return [RACDisposable disposableWithBlock:^{ + dispatch_async(dispatch_get_main_queue(), ^{ + if (self.pendingWirelessAuthorizationRequests[identifier] == request) { + [self.pendingWirelessAuthorizationRequests removeObjectForKey:identifier]; + } + request.subscriber = nil; + }); + }]; + }]; + return [authorizationSignal flattenMap:^__kindof RACSignal * _Nullable(ECOChannelDeviceInfo *authorizedDevice) { + return [self_weak_ _connectToWirelessDevice:authorizedDevice]; + }]; + } 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 { if ([PushFrameTypeList() containsIndex:type]) { return YES; } - + LKConnectionRequest *activeRequest = [channel.activeRequests lookin_firstFiltered:^BOOL(LKConnectionRequest *obj) { return (obj.type == type && obj.tag == tag); }]; @@ -448,12 +734,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 +750,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,8 +833,33 @@ - (void)ioFrameChannel:(Lookin_PTChannel*)channel didEndWithError:(NSError*)erro } }]; [self.channelWillEnd sendNext:channel]; - + [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/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 43ea1db2..18e06d36 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,54 @@ - (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]; + }]; + }; + + [[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; + 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..87b22385 --- /dev/null +++ b/LookinClient/Launch/LKLaunchWirelessDeviceView.m @@ -0,0 +1,182 @@ +// +// 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.stateLabel.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.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); + + 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 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); +} + +- (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]; + 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 { + BOOL isWhiteDevice = [LKConnectionManager.sharedInstance isWhiteListDevice:self.device]; + isWhiteDevice = !isWhiteDevice; + self.autoConnectControl.rightImage = [NSImage imageWithSystemSymbolName:isWhiteDevice ? @"checkmark.square" : @"square" accessibilityDescription:NSLocalizedString(@"Connect automatically", 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 de24842a..a48773a9 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; @@ -58,25 +60,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 +94,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,52 +107,100 @@ - (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); 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; 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"); }]; } +- (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); + } error:^(NSError *error) { + AlertErrorText(NSLocalizedString(@"Wireless Connections", nil), error.localizedDescription, CurrentKeyWindow); + }]; + } 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 +208,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"]; @@ -174,13 +224,16 @@ - (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); } 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); @@ -191,7 +244,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,45 +289,49 @@ - (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]; } +- (void)_handleWirelessApp { + [self popupNotConnnectWirelessInspectableApps]; +} + - (void)_handleSetting:(NSButton *)button { NSPopover *popover = [[NSPopover alloc] init]; popover.behavior = NSPopoverBehaviorTransient; @@ -295,7 +352,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 +367,7 @@ - (void)_handleMessage:(NSButton *)button { [menu addItem:[NSMenuItem separatorItem]]; continue; } - + if ([msg isEqualToString:LKMessage_NewServerVersion]) { [menu addItem:({ NSMenuItem *menuItem = [NSMenuItem new]; @@ -337,7 +394,7 @@ - (void)_handleMessage:(NSButton *)button { [menu addItem:[NSMenuItem separatorItem]]; continue; } - + if ([msg isEqualToString:LKMessage_SwiftSubspec]) { [menu addItem:({ NSMenuItem *menuItem = [NSMenuItem new]; @@ -365,9 +422,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])], @@ -382,7 +439,7 @@ - (void)_handleFreeRotation { #pragma mark - -- (void)appMenuManagerDidSelectReload { +- (void)appMenuManagerDidSelectReload { if (self.isFetchingHierarchy) { return; } @@ -433,7 +490,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 +501,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 +512,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 +534,7 @@ - (void)appMenuManagerDidSelectExport { } } }]; - + [MSACAnalytics trackEvent:@"Export Document"]; } @@ -487,7 +544,7 @@ - (void)appMenuManagerDidSelectOpenInNewWindow { file.serverVersion = newHierarchyInfo.serverVersion; file.hierarchyInfo = newHierarchyInfo; [[LKNavigationManager sharedInstance] showReaderWithHierarchyFile:file title:nil]; - + [MSACAnalytics trackEvent:@"Open New Window"]; } @@ -514,28 +571,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/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..b6692958 --- /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 (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/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) { 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..c99ea1ce 100644 --- a/LookinClient/en.lproj/Localizable.strings +++ b/LookinClient/en.lproj/Localizable.strings @@ -1,6 +1,13 @@ "Reload" = "Reload"; "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."; +"Wireless connection timed out." = "Wireless connection timed out."; "View" = "View"; "Zoom" = "Zoom"; "Console" = "Console"; @@ -218,3 +225,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..c462a876 100644 --- a/LookinClient/zh-Hans.lproj/Localizable.strings +++ b/LookinClient/zh-Hans.lproj/Localizable.strings @@ -1,6 +1,13 @@ "Reload" = "刷新"; "Syncing…" = "图像传输中…"; "Select App" = "App 切换"; +"Wireless Connections" = "无线连接"; +"Connected" = "已连接"; +"Click to connect" = "点击连接"; +"Connect automatically" = "自动连接"; +"Wireless device disconnected." = "无线设备已断开连接。"; +"Wireless device identity is invalid." = "无线设备标识无效。"; +"Wireless connection timed out." = "无线连接超时。"; "View" = "图像"; "Zoom" = "大小"; "Console" = "控制台"; @@ -216,3 +223,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" = "无线连接被拒绝"; diff --git a/Podfile b/Podfile index 792f5a3c..44ab8f0d 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 => '01460f6d6b857893afde77f7bbe2ca7adb70c012' #pod 'LookinShared', :path=>'../LookinServer/' end diff --git a/Podfile.lock b/Podfile.lock index f3f0da76..5154c841 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 `01460f6d6b857893afde77f7bbe2ca7adb70c012`) - ReactiveObjC (= 3.1.0) - Sparkle (~> 1.0) SPEC REPOS: trunk: - AppCenter + - CocoaAsyncSocket - ReactiveObjC - Sparkle EXTERNAL SOURCES: LookinShared: - :commit: 6a21883a8eb18997d6c6c9ec8ae3ad25739aca7e + :commit: 01460f6d6b857893afde77f7bbe2ca7adb70c012 :git: https://github.com/nova286/LookinServer.git CHECKOUT OPTIONS: LookinShared: - :commit: 6a21883a8eb18997d6c6c9ec8ae3ad25739aca7e + :commit: 01460f6d6b857893afde77f7bbe2ca7adb70c012 :git: https://github.com/nova286/LookinServer.git SPEC CHECKSUMS: AppCenter: b0eca112a27b71e97488ffa1949ee38c7abd4b79 - LookinShared: e5ca2b2b0758a1caafffc29cc12764ff92fb419b + CocoaAsyncSocket: 065fd1e645c7abab64f7a6a2007a48038fdc6a99 + LookinShared: 5817ca9a7d7e961026f690eadd4ebd9bf293db00 ReactiveObjC: 2a38ea15335de4119d8b17caf1db1484f61db902 Sparkle: 23f98b268284c8c03e6228230fc8f1807ef041d5 -PODFILE CHECKSUM: 3a6d485c3416742e8189e376785f1d4e1107dccb +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.