433 lines
16 KiB
Objective-C
433 lines
16 KiB
Objective-C
//
|
||
// FloatingWindow.m
|
||
// nochange
|
||
//
|
||
// Created by mac on 2024/7/29.
|
||
//
|
||
|
||
#import "FloatingWindow.h"
|
||
#import "IPhoneHertbeat.h"
|
||
#import "MyAdTask2.h"
|
||
#import "MyEventBus.h"
|
||
#import "UIView+Toast.h"
|
||
#import "XSHelper.h"
|
||
#import "XSPhoneConfig.h"
|
||
#import "XSPhoneInfo.h"
|
||
#import <Foundation/Foundation.h>
|
||
|
||
@interface FloatingWindow () {
|
||
NSMutableArray *loadTimestamps;
|
||
NSMutableArray *showTimestamps;
|
||
NSTimer *updateTimer;
|
||
}
|
||
|
||
@end
|
||
|
||
@implementation FloatingWindow
|
||
|
||
- (instancetype)initWithFrame {
|
||
CGRect screenBounds = [UIScreen mainScreen].bounds;
|
||
CGFloat floatingWindowWidth = 180;
|
||
CGFloat floatingWindowHeight = 160;
|
||
CGFloat xPosition = 0;
|
||
CGFloat yPosition = screenBounds.size.height - floatingWindowHeight;
|
||
|
||
CGRect frame = CGRectMake(xPosition, yPosition, floatingWindowWidth,
|
||
floatingWindowHeight);
|
||
self = [super initWithFrame:frame];
|
||
if (self) {
|
||
UIPanGestureRecognizer *panRecognizer =
|
||
[[UIPanGestureRecognizer alloc] initWithTarget:self
|
||
action:@selector(detectPan:)];
|
||
[self addGestureRecognizer:panRecognizer];
|
||
|
||
self.backgroundColor = [UIColor grayColor]; // 默认灰色背景
|
||
self.layer.cornerRadius = 10;
|
||
self.clipsToBounds = YES;
|
||
|
||
[self setupUI];
|
||
[self updateInfo];
|
||
self.http = [[XSHttpHelper alloc] init];
|
||
[[MyEventBus sharedInstance] registerSubscriber:self];
|
||
|
||
loadTimestamps = [[NSMutableArray alloc] init];
|
||
showTimestamps = [[NSMutableArray alloc] init];
|
||
|
||
[self startUpdatingCounts]; // 启动定时任务
|
||
}
|
||
return self;
|
||
}
|
||
|
||
- (void)dealloc {
|
||
[[MyEventBus sharedInstance] unregisterSubscriber:self];
|
||
[self stopUpdatingCounts]; // 停止定时任务
|
||
}
|
||
|
||
// 定时任务方法
|
||
- (void)startUpdatingCounts {
|
||
updateTimer = [NSTimer scheduledTimerWithTimeInterval:5.0
|
||
target:self
|
||
selector:@selector(updateCounts)
|
||
userInfo:nil
|
||
repeats:YES];
|
||
[[NSRunLoop mainRunLoop] addTimer:updateTimer forMode:NSRunLoopCommonModes];
|
||
}
|
||
|
||
// 停止定时任务
|
||
- (void)stopUpdatingCounts {
|
||
if ([updateTimer isValid]) {
|
||
[updateTimer invalidate];
|
||
updateTimer = nil;
|
||
}
|
||
}
|
||
|
||
// 更新文件夹中的文件数量
|
||
- (void)updateCounts {
|
||
// 将耗时的目录统计放到后台线程,避免阻塞主线程导致卡顿
|
||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
|
||
NSString *loadDir = @"/User/Documents/ad/load";
|
||
NSString *showDir = @"/User/Documents/ad/show";
|
||
|
||
// 统计当前有效文件数量(不在此处删除)
|
||
NSInteger loadCount = [self countValidFilesInDirectory:loadDir];
|
||
NSInteger showCount = [self countValidFilesInDirectory:showDir];
|
||
|
||
// 异步执行清理过期文件,避免与其他进程写入产生竞争
|
||
[self cleanupExpiredFilesInDirectory:loadDir];
|
||
[self cleanupExpiredFilesInDirectory:showDir];
|
||
|
||
// UI 更新在主线程
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
self.infoLabel.text = [NSString stringWithFormat:@"S:%@ / L:%@", @(showCount), @(loadCount)];
|
||
[self updateBackgroundColorByShowCount:showCount];
|
||
});
|
||
});
|
||
}
|
||
|
||
// 用于统计有效(1小时内修改)的文件数量,跳过不可读/属性异常的文件
|
||
- (NSInteger)countValidFilesInDirectory:(NSString *)directoryPath {
|
||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||
NSURL *dirURL = [NSURL fileURLWithPath:directoryPath isDirectory:YES];
|
||
NSDate *oneHourAgo = [NSDate dateWithTimeIntervalSinceNow:-3600];
|
||
__block NSInteger count = 0;
|
||
|
||
// 使用目录枚举器,减少一次性加载全部文件列表的内存压力
|
||
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:dirURL
|
||
includingPropertiesForKeys:@[NSURLIsRegularFileKey, NSURLContentModificationDateKey]
|
||
options:(NSDirectoryEnumerationSkipsSubdirectoryDescendants |
|
||
NSDirectoryEnumerationSkipsHiddenFiles)
|
||
errorHandler:^BOOL(NSURL *url, NSError *error) {
|
||
// 读取错误时跳过该条目,保证健壮性
|
||
return YES;
|
||
}];
|
||
|
||
for (NSURL *fileURL in enumerator) {
|
||
// 仅统计普通文件
|
||
NSNumber *isRegular = nil;
|
||
if (![fileURL getResourceValue:&isRegular forKey:NSURLIsRegularFileKey error:nil] || !isRegular.boolValue) {
|
||
continue;
|
||
}
|
||
// 跳过不可读文件,避免其他进程占用导致阻塞/错误
|
||
if (![fileManager isReadableFileAtPath:fileURL.path]) {
|
||
continue;
|
||
}
|
||
// 读取修改时间
|
||
NSDate *modDate = nil;
|
||
if (![fileURL getResourceValue:&modDate forKey:NSURLContentModificationDateKey error:nil] || modDate == nil) {
|
||
// 属性获取失败视为不统计
|
||
continue;
|
||
}
|
||
if ([modDate compare:oneHourAgo] == NSOrderedDescending) {
|
||
count++;
|
||
}
|
||
}
|
||
|
||
return count;
|
||
}
|
||
|
||
// 后台清理过期文件:与统计分离执行,删除失败忽略;仅删除普通文件
|
||
- (void)cleanupExpiredFilesInDirectory:(NSString *)directoryPath {
|
||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
|
||
NSFileManager *fileManager = [NSFileManager defaultManager];
|
||
NSURL *dirURL = [NSURL fileURLWithPath:directoryPath isDirectory:YES];
|
||
NSDate *oneHourAgo = [NSDate dateWithTimeIntervalSinceNow:-3600];
|
||
|
||
NSDirectoryEnumerator *enumerator = [fileManager enumeratorAtURL:dirURL
|
||
includingPropertiesForKeys:@[NSURLIsRegularFileKey, NSURLContentModificationDateKey]
|
||
options:(NSDirectoryEnumerationSkipsSubdirectoryDescendants |
|
||
NSDirectoryEnumerationSkipsHiddenFiles)
|
||
errorHandler:^BOOL(NSURL *url, NSError *error) {
|
||
return YES;
|
||
}];
|
||
|
||
for (NSURL *fileURL in enumerator) {
|
||
NSNumber *isRegular = nil;
|
||
if (![fileURL getResourceValue:&isRegular forKey:NSURLIsRegularFileKey error:nil] || !isRegular.boolValue) {
|
||
continue;
|
||
}
|
||
// 跳过不可读/不可删除的文件,减少与写入进程的竞争
|
||
if (![fileManager isReadableFileAtPath:fileURL.path]) {
|
||
continue;
|
||
}
|
||
|
||
NSDate *modDate = nil;
|
||
if (![fileURL getResourceValue:&modDate forKey:NSURLContentModificationDateKey error:nil] || modDate == nil) {
|
||
continue;
|
||
}
|
||
|
||
if ([modDate compare:oneHourAgo] != NSOrderedDescending) {
|
||
// 尝试删除过期文件,失败则忽略
|
||
NSError *removeError = nil;
|
||
[fileManager removeItemAtURL:fileURL error:&removeError];
|
||
// 可选:根据需要打印错误日志
|
||
// if (removeError) { NSLog(@"Cleanup skip %@ error: %@", fileURL.path, removeError); }
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// 原方法保留声明但不再使用,避免并发删除引发卡顿/统计异常
|
||
- (NSInteger)countAndCleanFilesInDirectory:(NSString *)directoryPath {
|
||
// 已弃用:请使用 countValidFilesInDirectory 与 cleanupExpiredFilesInDirectory
|
||
// 保留空实现或转发以兼容旧调用方
|
||
return [self countValidFilesInDirectory:directoryPath];
|
||
}
|
||
|
||
// 添加清理过期数据的辅助方法
|
||
- (void)cleanExpiredTimestamps:(NSMutableArray *)timestamps {
|
||
NSDate *now = [NSDate date];
|
||
NSTimeInterval oneHourAgo = [now timeIntervalSince1970] - 3600; // 3600秒 = 1小时
|
||
|
||
NSPredicate *predicate = [NSPredicate predicateWithBlock:^BOOL(NSDate *timestamp, NSDictionary *bindings) {
|
||
return [timestamp timeIntervalSince1970] > oneHourAgo;
|
||
}];
|
||
|
||
NSArray *validTimestamps = [timestamps filteredArrayUsingPredicate:predicate];
|
||
[timestamps removeAllObjects];
|
||
[timestamps addObjectsFromArray:validTimestamps];
|
||
}
|
||
|
||
// 更新背景颜色
|
||
- (void)updateBackgroundColorByShowCount:(NSInteger)showCount {
|
||
UIColor *color;
|
||
|
||
if (showCount < 80) {
|
||
// 蓝色 #07A6E4
|
||
color = [UIColor colorWithRed:7/255.0 green:166/255.0 blue:228/255.0 alpha:1.0];
|
||
} else if (showCount < 160) {
|
||
// 红色 #D92727
|
||
color = [UIColor colorWithRed:217/255.0 green:39/255.0 blue:39/255.0 alpha:1.0];
|
||
} else if (showCount < 240) {
|
||
// 橙黄 #EF811C
|
||
color = [UIColor colorWithRed:239/255.0 green:129/255.0 blue:28/255.0 alpha:1.0];
|
||
} else {
|
||
// 绿色 #08C951
|
||
color = [UIColor colorWithRed:8/255.0 green:201/255.0 blue:81/255.0 alpha:1.0];
|
||
}
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
self.backgroundColor = color;
|
||
});
|
||
}
|
||
|
||
// 添加 onEventLoad 方法
|
||
- (void)onEventLoad:(id)data {
|
||
@synchronized(loadTimestamps) {
|
||
[loadTimestamps addObject:[NSDate date]];
|
||
[self cleanExpiredTimestamps:loadTimestamps];
|
||
|
||
NSInteger loadCount = loadTimestamps.count;
|
||
NSInteger showCount = showTimestamps.count;
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
self.infoLabel.text = [NSString stringWithFormat:@"S:%@ / L:%@", @(showCount), @(loadCount)];
|
||
});
|
||
|
||
NSLog(@"Load event: total in last hour = %ld", (long)loadCount);
|
||
}
|
||
}
|
||
|
||
// 添加 onEventShow 方法
|
||
- (void)onEventShow:(id)data {
|
||
@synchronized(showTimestamps) {
|
||
[showTimestamps addObject:[NSDate date]];
|
||
[self cleanExpiredTimestamps:showTimestamps];
|
||
|
||
NSInteger showCount = showTimestamps.count;
|
||
NSInteger loadCount = loadTimestamps.count;
|
||
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
self.infoLabel.text = [NSString stringWithFormat:@"S:%@ / L:%@", @(showCount), @(loadCount)];
|
||
});
|
||
|
||
// 根据 show 数量更新背景颜色
|
||
[self updateBackgroundColorByShowCount:showCount];
|
||
|
||
NSLog(@"Show event: total in last hour = %ld", (long)showCount);
|
||
}
|
||
}
|
||
|
||
- (void)setupUI {
|
||
// Name Label
|
||
self.nameLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 10, 180, 18)];
|
||
self.nameLabel.textColor = [UIColor whiteColor];
|
||
self.nameLabel.font = [UIFont systemFontOfSize:14.0];
|
||
[self addSubview:self.nameLabel];
|
||
|
||
// IP Label
|
||
self.ipLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 35, 180, 20)];
|
||
[self addSubview:self.ipLabel];
|
||
|
||
self.infoLabel = [[UILabel alloc] initWithFrame:CGRectMake(10, 55, 180, 20)];
|
||
[self addSubview:self.infoLabel];
|
||
|
||
// Device Type Label
|
||
self.deviceTypeLabel =
|
||
[[UILabel alloc] initWithFrame:CGRectMake(10, 75, 180, 30)];
|
||
self.deviceTypeLabel.textColor = [UIColor whiteColor];
|
||
self.deviceTypeLabel.numberOfLines = 0;
|
||
self.deviceTypeLabel.font = [UIFont systemFontOfSize:12.0];
|
||
[self addSubview:self.deviceTypeLabel];
|
||
|
||
// Action Button
|
||
self.actionButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||
self.actionButton.frame = CGRectMake(10, 120, 70, 30);
|
||
[self.actionButton setTitleColor:[UIColor whiteColor]
|
||
forState:UIControlStateNormal];
|
||
[self.actionButton setTitleColor:[UIColor redColor]
|
||
forState:UIControlStateHighlighted];
|
||
// 'rgb(113, 201, 206)'
|
||
self.actionButton.backgroundColor = [UIColor colorWithRed:113 / 255.0
|
||
green:201 / 255.0
|
||
blue:206 / 255.0
|
||
alpha:1.0];
|
||
// self.actionButton.layer.borderWidth = 1.0;
|
||
// self.actionButton.layer.borderColor = [UIColor blueColor].CGColor;
|
||
self.actionButton.layer.cornerRadius = 4.0;
|
||
NSString *btnTitle = @"已停止";
|
||
[self.actionButton setTitle:btnTitle forState:UIControlStateNormal];
|
||
[self.actionButton addTarget:self
|
||
action:@selector(actionButtonTapped:)
|
||
forControlEvents:UIControlEventTouchUpInside];
|
||
[self addSubview:self.actionButton];
|
||
|
||
// Settings Button
|
||
self.settingsButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||
self.settingsButton.frame = CGRectMake(90, 120, 70, 30);
|
||
[self.settingsButton setTitleColor:[UIColor whiteColor]
|
||
forState:UIControlStateNormal];
|
||
[self.settingsButton setTitleColor:[UIColor redColor]
|
||
forState:UIControlStateHighlighted];
|
||
self.settingsButton.backgroundColor = [UIColor colorWithRed:113 / 255.0
|
||
green:201 / 255.0
|
||
blue:206 / 255.0
|
||
alpha:1.0];
|
||
// self.actionButton.layer.borderWidth = 1.0;
|
||
// self.actionButton.layer.borderColor = [UIColor blueColor].CGColor;
|
||
self.settingsButton.layer.cornerRadius = 4.0;
|
||
[self.settingsButton setTitle:@"刷新" forState:UIControlStateNormal];
|
||
[self.settingsButton addTarget:self
|
||
action:@selector(settingsButtonTapped)
|
||
forControlEvents:UIControlEventTouchUpInside];
|
||
[self addSubview:self.settingsButton];
|
||
|
||
self.infoLabel.text = @"S:0 / L:0";
|
||
// self.center = CGPointMake(90, self.superview.bounds.size.height - 100);
|
||
}
|
||
|
||
- (void)updateInfo {
|
||
XSPhoneConfig *info = [XSPhoneConfig sharedInstance];
|
||
self.nameLabel.text = [NSString stringWithFormat:@"%@", [info IPhoneName]];
|
||
self.ipLabel.text = [NSString
|
||
stringWithFormat:@"IP: %@", [[XSPhoneInfo sharedInstance] IPAddress]];
|
||
self.deviceTypeLabel.text = @"unknow";
|
||
}
|
||
|
||
|
||
- (void)onEventUpdateStatus:(id)data {
|
||
__weak typeof(self) weakSelf = self;
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
weakSelf.deviceTypeLabel.text = [NSString stringWithFormat:@"%@", data];
|
||
});
|
||
}
|
||
|
||
- (void)onEventUpdateRunStatus:(id)data {
|
||
__weak typeof(self) weakSelf = self;
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
// BOOL b = data;
|
||
|
||
NSString *btnTitle = ([data isEqual:@(YES)] ? @"运行中" : @"已停止");
|
||
NSLog(@"onEventUpdateRunStatus: %@,%@", data, btnTitle);
|
||
[weakSelf.actionButton setTitle:btnTitle forState:UIControlStateNormal];
|
||
});
|
||
}
|
||
- (void)showMyToast:(NSString *)msg {
|
||
__weak typeof(self) weakSelf = self;
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
// UIView usage
|
||
[weakSelf makeToast:msg];
|
||
});
|
||
}
|
||
|
||
- (void)onEventUpdateName:(id)data {
|
||
__weak typeof(self) weakSelf = self;
|
||
dispatch_async(dispatch_get_main_queue(), ^{
|
||
XSPhoneConfig *info = [XSPhoneConfig sharedInstance];
|
||
weakSelf.nameLabel.text =
|
||
[NSString stringWithFormat:@"%@", [info IPhoneName]];
|
||
});
|
||
}
|
||
|
||
- (void)actionButtonTapped:(UIButton *)sender {
|
||
NSLog(@"Action button tapped");
|
||
MyAdTask2Mangger *man = [MyAdTask2Mangger sharedInstance];
|
||
// MyAdTaskManager *man = [MyAdTaskManager sharedInstance];
|
||
// IPhoneHertbeat *hertBeat = [IPhoneHertbeat sharedInstance];
|
||
NSString *status = [man toggle]; // @"已停止";
|
||
/*
|
||
if ([self.actionButton.titleLabel.text isEqual:@"运行中"]) {
|
||
[man stop];
|
||
} else {
|
||
[man start];
|
||
status = @"运行中";
|
||
}
|
||
*/
|
||
//[self.actionButton setTitle:status forState:UIControlStateNormal];
|
||
}
|
||
|
||
- (void)settingsButtonTapped {
|
||
NSLog(@"Settings button tapped");
|
||
[self updateInfo];
|
||
//[self appendLog:@"设置按钮被按下"];
|
||
}
|
||
|
||
- (void)changeBackgroundColor {
|
||
self.backgroundColor = [UIColor colorWithRed:arc4random_uniform(256) / 255.0
|
||
green:arc4random_uniform(256) / 255.0
|
||
blue:arc4random_uniform(256) / 255.0
|
||
alpha:1.0];
|
||
}
|
||
|
||
- (void)appendLog:(NSString *)logMessage {
|
||
NSString *timestamp =
|
||
[NSDateFormatter localizedStringFromDate:[NSDate date]
|
||
dateStyle:NSDateFormatterNoStyle
|
||
timeStyle:NSDateFormatterMediumStyle];
|
||
NSString *fullLogMessage =
|
||
[NSString stringWithFormat:@"%@: %@\n", timestamp, logMessage];
|
||
self.logTextView.text =
|
||
[self.logTextView.text stringByAppendingString:fullLogMessage];
|
||
[self.logTextView
|
||
scrollRangeToVisible:NSMakeRange(self.logTextView.text.length - 1, 1)];
|
||
}
|
||
|
||
- (void)detectPan:(UIPanGestureRecognizer *)panGesture {
|
||
UIWindow *floatingWindow = (UIWindow *)panGesture.view;
|
||
CGPoint translation = [panGesture translationInView:floatingWindow];
|
||
floatingWindow.center = CGPointMake(floatingWindow.center.x + translation.x,
|
||
floatingWindow.center.y + translation.y);
|
||
[panGesture setTranslation:CGPointZero inView:floatingWindow];
|
||
}
|
||
|
||
@end
|