// // QQShareManager.m // msext // // Created on 2025/06/15. // Copyright © 2025年. All rights reserved. // #import "QQShareManager.h" #import "FuncPublic.h" // 用于截图功能 #import // 引入 LinkPresentation 用于自定义分享预览 // 尝试检查是否存在 TencentOpenAPI SDK #if __has_include() #import #import #define HAS_QQ_SDK 1 #else #define HAS_QQ_SDK 0 #endif // ----------------------------------------------------------------------------- // QQShareActivityItemSource // This class is used to customize the preview title and icon in the Share Sheet. // ----------------------------------------------------------------------------- @interface QQShareActivityItemSource : NSObject @property (nonatomic, strong) id content; // URL, String, or Image @property (nonatomic, strong) NSString *title; // Preview Title @property (nonatomic, strong) UIImage *image; // Preview Image (Icon) @property (nonatomic, strong) NSString *desc; // Preview Description (Optional) - (instancetype)initWithContent:(id)content title:(NSString *)title image:(UIImage *)image description:(NSString *)desc; @end @implementation QQShareActivityItemSource - (instancetype)initWithContent:(id)content title:(NSString *)title image:(UIImage *)image description:(NSString *)desc { self = [super init]; if (self) { _content = content; _title = title; _image = image; _desc = desc; } return self; } #pragma mark - UIActivityItemSource Methods - (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController { // Placeholder matches the content type return self.content; } - (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType { // Return the actual content to share return self.content; } // Ensure the preview metadata is provided (iOS 13+) - (LPLinkMetadata *)activityViewControllerLinkMetadata:(UIActivityViewController *)activityViewController API_AVAILABLE(ios(13.0)) { LPLinkMetadata *metadata = [[LPLinkMetadata alloc] init]; // Set Preview Title (User requested App Name or specific title) metadata.title = self.title ? self.title : @"分享内容"; // Set Preview Icon / Image // 用户反馈 "图片分享正常,但其他情况(链接分享)图标有白边"。 // 根本原因: LPLinkMetadata 对于 content-type 为 URL 的 preview, // 如果提供的 image 是 iconProvider,iOS 默认会将其渲染为 "Icon" 样式(带白边、圆角遮罩等)。 // 如果提供的 image 是 imageProvider,iOS 会将其渲染为 "Image/Thumbnail" 样式(通常是大图)。 // 如果我们希望它像 "图片分享" 那样全屏、无白边地展示,我们需要把它伪装成 imageProvider, // 并且可能调整 NSItemProvider 类型。 // 经调研,system share sheet 的左侧预览区逻辑如下: // 1. 如果有 imageProvider,尝试显示大图 (Thumbnail)。如果图是正方形,系统默认还是有 padding。 // 2. 如果只有 iconProvider,由于它仅仅是一个图标,系统会将其放置在一个方形容器中,必定有白边。 // 最终尝试: // 将其作为 imageProvider 提供,但这要求图片本身具备足够分辨率,且系统对于 URL Share 的 Layout 默认就是 Sidebar 模式。 // 我们这里强制使用 imageProvider 试试看,这是目前看来最可能消除“仅Icon白边”的办法。 if (self.image) { NSItemProvider *itemProvider = [[NSItemProvider alloc] initWithObject:self.image]; metadata.imageProvider = itemProvider; // 改回 imageProvider,这是大图预览的关键 metadata.iconProvider = itemProvider; // 同时设置 } // Set Original URL (if content is URL) if ([self.content isKindOfClass:[NSURL class]]) { metadata.originalURL = (NSURL *)self.content; metadata.URL = (NSURL *)self.content; } return metadata; } @end // QQ URL Schemes #define kQQScheme @"mqqapi://" #define kQQShareScheme @"mqqapi://share/" #define kQQFriendScheme @"mqqapi://share/to_fri?" #define kQQZoneScheme @"mqqapi://share/to_qzone?" #define kQQUniversalScheme @"mqq://share/to_fri?" // 通用分享scheme,无需AppID验证 // QQ 分享必需的参数 #define kQQAppName @"进贤聚友棋牌" // 应用名称,与Info.plist中的CFBundleDisplayName和CFBundleName保持一致 #define kQQCallbackScheme @"msext" // 回调scheme #define kQQAppID @"102793577" // 您当前的AppID #define kQQFallbackAppID @"100312206" // 腾讯官方测试AppID,通常可用 // QQ 应用回调的处理 static void(^QQShareCompletion)(BOOL) = nil; @implementation QQShareManager #pragma mark - Public Methods + (BOOL)isQQInstalled { return [[UIApplication sharedApplication] canOpenURL:[NSURL URLWithString:kQQScheme]]; } + (BOOL)validateQQAppIDConfiguration { NSLog(@"🔍 开始验证QQ AppID配置..."); NSLog(@"🔍 当前AppID值:%@", kQQAppID); NSLog(@"🔍 AppID长度:%lu", (unsigned long)[kQQAppID length]); // 检查AppID是否配置 if (!kQQAppID || [kQQAppID isEqualToString:@"YOUR_QQ_APPID"]) { NSLog(@"❌ QQ AppID 未配置!请在 QQShareManager.m 中设置正确的 kQQAppID"); return NO; } // 检查AppID格式(应该是纯数字,长度在8-10位之间) NSCharacterSet *nonDigits = [[NSCharacterSet decimalDigitCharacterSet] invertedSet]; if ([kQQAppID rangeOfCharacterFromSet:nonDigits].location != NSNotFound) { NSLog(@"❌ QQ AppID 格式错误!AppID应该是纯数字,当前:%@", kQQAppID); return NO; } // 检查长度(现代QQ AppID通常是8-10位数字) if (kQQAppID.length < 8 || kQQAppID.length > 10) { NSLog(@"❌ QQ AppID 长度错误!AppID应该是8-10位数字,当前长度:%lu,AppID:%@", (unsigned long)kQQAppID.length, kQQAppID); return NO; } NSLog(@"✅ QQ AppID 配置正确:%@", kQQAppID); NSLog(@"⚠️ 重要提示:如果出现900101错误,请确认:"); NSLog(@"⚠️ 1. AppID是否已在QQ开放平台审核通过"); NSLog(@"⚠️ 2. Bundle ID是否与申请时一致"); NSLog(@"⚠️ 3. 应用是否已上线或处于测试白名单"); return YES; } + (NSString *)getCurrentQQAppID { if ([self validateQQAppIDConfiguration]) { return kQQAppID; } return nil; } + (void)shareToQQFriend:(QQShareType)type title:(NSString *)title description:(NSString *)description thumbImage:(UIImage *)thumbImage url:(NSString *)url image:(UIImage *)image completion:(void (^)(BOOL))completion { // 检查QQ是否已安装 if (![self isQQInstalled]) { if (completion) { completion(NO); } [self showAppNotInstalledAlert:@"QQ"]; return; } // 验证AppID配置(解决9000101错误的关键) if (![self validateQQAppIDConfiguration]) { if (completion) { completion(NO); } [self showAppIDConfigurationErrorAlert]; return; } // 保存回调 QQShareCompletion = completion; // 构建URL parameters based on OpenShare implementation logic to fix 900101 // 900101 is often "Invalid AppID" or "Signature Mismatch". // OpenShare uses Base64 for almost all text fields. NSMutableString *urlString = [NSMutableString stringWithString:kQQFriendScheme]; // 1. Basic Parameters [urlString appendString:@"version=1"]; [urlString appendString:@"&src_type=app"]; // Required // [urlString appendString:@"&cflag=0"]; // OpenShare doesn't always send this, but let's keep it off for now or 0 // 2. App Identification // thirdAppDisplayName MUST be Base64 encoded for mqqapi usually NSString *base64Name = [self base64Encode:kQQAppName]; NSString *encodedName = [self encodeString:base64Name]; [urlString appendFormat:@"&thirdAppDisplayName=%@", encodedName]; [urlString appendFormat:@"&app_name=%@", encodedName]; [urlString appendFormat:@"&app_id=%@", kQQAppID]; [urlString appendFormat:@"&share_id=%@", kQQAppID]; // 3. Callback // mqqapi usually validates that callback_name matches "QQ" + Hex(AppID) // 102793577 (Decimal) -> 06208169 (Hex) // So callback_name should be QQ06208169 NSString *hexCallbackName = @"QQ06208169"; [urlString appendString:@"&callback_type=scheme"]; [urlString appendFormat:@"&callback_name=%@", hexCallbackName]; // 4. Content Parameters // ------------------------------------------------------------------------- // 策略选择: // 1. useSystemShare: 使用 iOS 系统分享面板 (UIActivityViewController) // 2. useQQSDK: 使用腾讯官方 SDK (TencentOpenAPI) - 需确保已导入SDK // 3. Fallback: 使用 URL Scheme (mqqapi://) - 无需SDK,直接调起 (Legacy) // ------------------------------------------------------------------------- BOOL useSystemShare = NO; // 默认为NO BOOL useQQSDK = NO; // 默认为YES, 优先使用SDK // 决策逻辑: 是否运行系统分享 BOOL runSystemShare = NO; BOOL sdkIsAvailableAndEnabled = NO; #if HAS_QQ_SDK if (useQQSDK) sdkIsAvailableAndEnabled = YES; #endif if (sdkIsAvailableAndEnabled) { // 如果 SDK 可用且开启,优先走 SDK 逻辑 (后面处理),跳过 SystemShare runSystemShare = NO; } else { // 如果 SDK 不可用或未开启 if (useSystemShare) { // 用户显式开启系统分享 -> 运行 runSystemShare = YES; } else { // 若都未开启,检查特殊情况: // 图片分享无法通过 Scheme 直接调起会话,降级使用 SystemShare if (type == QQShareTypeImage) { runSystemShare = YES; } } } if (runSystemShare) { NSMutableArray *activityItems = [NSMutableArray array]; // 准备预览图标 (App Icon) UIImage *appIcon = [self getAppIcon]; // 准备预览标题 (App Name 优先,或者 Title) NSString *previewTitle = kQQAppName; // 默认显示App名称 if (title && title.length > 0) { previewTitle = title; // 如果有特定标题,也可以显示标题,或者拼接 "App Name - Title" } // 使用 QQShareActivityItemSource 封装内容以自定义预览 if (@available(iOS 13.0, *)) { switch (type) { case QQShareTypeText: if (description) { // 用户需求:通过系统分享文本时希望是纯文本形式,而不是外部链接/卡片 // 直接传递 NSString 对象,避免使用 UIActivityItemSource 或 LPLinkMetadata 封装 [activityItems addObject:description]; } break; case QQShareTypeImage: if (image) { // 图片分享本身预览就是图片,通常无需干预。 // 用户反馈使用 ActivityItemSource 封装后会导致图标显示有白边等问题。 // 因此,对于 Image 分享,我们恢复直接传递 image 对象的原生方式,让系统自己处理最佳预览。 [activityItems addObject:image]; } break; case QQShareTypeNews: case QQShareTypeAudio: case QQShareTypeVideo: if (url) { NSURL *shareURL = [NSURL URLWithString:url]; if (shareURL) { // 关键:将 URL 封装,并强制指定 appIcon 为 iconProvider QQShareActivityItemSource *item = [[QQShareActivityItemSource alloc] initWithContent:shareURL title:previewTitle image:appIcon description:description]; [activityItems addObject:item]; } } // 注意:对于链接分享,通常只放一个 NSURL 对象即可。 // 放入 Title/Image 可能会导致变成混合分享,某些 App 处理不好。 // 通过 ItemSource,我们只分享 URL,但显示的元数据是自定义的。 break; } } else { // iOS 13 以下降级处理 (不支持自定义预览) switch (type) { case QQShareTypeText: if (title) [activityItems addObject:title]; if (description) [activityItems addObject:description]; break; case QQShareTypeImage: if (image) [activityItems addObject:image]; break; case QQShareTypeNews: case QQShareTypeAudio: case QQShareTypeVideo: if (url) { NSURL *shareURL = [NSURL URLWithString:url]; if (shareURL) [activityItems addObject:shareURL]; } if (title) [activityItems addObject:title]; // iOS 12及以下,直接传内容,系统自己处理 break; } } if (activityItems.count > 0) { NSLog(@"🔍 [QQShareManager] 策略: 使用系统 UIActivityViewController 分享"); dispatch_async(dispatch_get_main_queue(), ^{ UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:activityItems applicationActivities:nil]; activityVC.completionWithItemsHandler = ^(UIActivityType _Nullable activityType, BOOL completed, NSArray * _Nullable returnedItems, NSError * _Nullable activityError) { NSLog(@"[QQShareManager] 系统分享回调: completed=%d", completed); if (completion) completion(completed); }; UIViewController *topVC = [self topViewController]; if (topVC) { if ([activityVC respondsToSelector:@selector(popoverPresentationController)]) { activityVC.popoverPresentationController.sourceView = topVC.view; activityVC.popoverPresentationController.sourceRect = CGRectMake(topVC.view.bounds.size.width/2.0, topVC.view.bounds.size.height/2.0, 1.0, 1.0); activityVC.popoverPresentationController.permittedArrowDirections = 0; } [topVC presentViewController:activityVC animated:YES completion:nil]; } else { if (completion) completion(NO); } }); return; // 结束执行,不再走下面的 URL Scheme 逻辑 } } #if HAS_QQ_SDK // ------------------------------------------------------------------------- // 策略: 腾讯官方 SDK 分享 // ------------------------------------------------------------------------- if (useQQSDK) { NSLog(@"🔍 [QQShareManager] 策略: 使用腾讯官方 SDK 分享"); // 确保在主线程调用 (SDK要求) dispatch_async(dispatch_get_main_queue(), ^{ QQApiObject *msgObj = nil; switch (type) { case QQShareTypeText: { QQApiTextObject *textObj = [QQApiTextObject objectWithText:description ? description : @""]; textObj.title = title; msgObj = textObj; break; } case QQShareTypeImage: { if (!image) { if (completion) completion(NO); return; } NSData *imgData = UIImageJPEGRepresentation(image, 0.8); // Image Object QQApiImageObject *imgObj = [QQApiImageObject objectWithData:imgData previewImageData:imgData title:title description:description]; msgObj = imgObj; break; } case QQShareTypeNews: case QQShareTypeAudio: case QQShareTypeVideo: { if (!url) { if (completion) completion(NO); return; } NSData *previewData = nil; if (thumbImage) { previewData = UIImageJPEGRepresentation(thumbImage, 0.5); } NSURL *targetUrl = [NSURL URLWithString:url]; if (type == QQShareTypeAudio) { QQApiAudioObject *audioObj = [QQApiAudioObject objectWithURL:targetUrl title:title description:description previewImageData:previewData]; msgObj = audioObj; } else if (type == QQShareTypeVideo) { // Video usually needs flashURL, here acts as News Link basically QQApiNewsObject *newsObj = [QQApiNewsObject objectWithURL:targetUrl title:title description:description previewImageData:previewData]; msgObj = newsObj; } else { // News QQApiNewsObject *newsObj = [QQApiNewsObject objectWithURL:targetUrl title:title description:description previewImageData:previewData]; msgObj = newsObj; } break; } } if (msgObj) { SendMessageToQQReq *req = [SendMessageToQQReq reqWithContent:msgObj]; // 调起QQ QQApiSendResultCode sent = [QQApiInterface sendReq:req]; NSLog(@"[QQShareManager] SDK发送请求结果: %d", sent); // 处理同步结果 // 注意: 最终成功与否通常依赖 AppDelegate 的 onResp 回调 // EQQAPISENDSUCESS = 0 if (sent == 0) { NSLog(@"✅ SDK请求发送成功"); // 这里不立即调用 completion(YES),等待回调 } else { NSLog(@"❌ SDK请求发送失败"); if (completion) completion(NO); } } else { NSLog(@"❌ [QQShareManager] 构建 SDK 对象失败"); if (completion) completion(NO); } }); return; // 拦截后续 Scheme 逻辑 } #endif // Falls through to Legacy Scheme Logic if useSystemShare is NO or activityItems is empty switch (type) { case QQShareTypeText: [urlString appendString:@"&file_type=text"]; [urlString appendString:@"&req_type=0"]; // Text if (title) [urlString appendFormat:@"&title=%@", [self encodeString:[self base64Encode:title]]]; if (description) [urlString appendFormat:@"&description=%@", [self encodeString:[self base64Encode:description]]]; break; case QQShareTypeImage: { if (!image) { NSLog(@"❌ [QQShareManager] 图片对象为空"); if (completion) completion(NO); return; } // 恢复原有的 URL Scheme 图片分享逻辑 (Dual Strategy) // 策略: Base64 (小图) vs FilePath (大图/持久化) BOOL useBase64Scheme = YES; if (useBase64Scheme) { NSData *imgData = UIImageJPEGRepresentation(image, 0.5); if (imgData.length > 1 * 1024 * 1024) imgData = UIImageJPEGRepresentation(image, 0.3); NSString *base64Str = [imgData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithLineFeed]; [urlString appendString:@"&file_type=image"]; [urlString appendFormat:@"&image_base64=%@", [self encodeString:base64Str]]; [urlString appendString:@"&cflag=0"]; } else { NSString *imagePath = [self saveImageToDocumentsDirectory:image]; [urlString appendString:@"&file_type=image"]; [urlString appendString:@"&req_type=5"]; [urlString appendString:@"&cflag=0"]; UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; [pasteboard setData:UIImageJPEGRepresentation(image, 0.6) forPasteboardType:@"com.tencent.mqq.api.apiLargeData"]; if (title) [urlString appendFormat:@"&title=%@", [self encodeString:title]]; if (description) [urlString appendFormat:@"&description=%@", [self encodeString:description]]; NSString *fullPath = [@"file://" stringByAppendingString:imagePath]; NSString *encodedPath = [self encodeString:fullPath]; [urlString appendFormat:@"&file_path=%@", encodedPath]; [urlString appendFormat:@"&image_url=%@", encodedPath]; [urlString appendFormat:@"&object_location=%@", encodedPath]; } break; } case QQShareTypeNews: // "News" (Link) share [urlString appendString:@"&file_type=news"]; [urlString appendString:@"&req_type=1"]; if (title) [urlString appendFormat:@"&title=%@", [self encodeString:[self base64Encode:title]]]; if (description) [urlString appendFormat:@"&description=%@", [self encodeString:[self base64Encode:description]]]; if (url) [urlString appendFormat:@"&url=%@", [self encodeString:[self base64Encode:url]]]; if (thumbImage) { NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; NSString *fullPath = [@"file://" stringByAppendingString:thumbPath]; // Preview Image URL often requires BASE64 of the path string for OpenShare compatibility [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[self base64Encode:fullPath]]]; } break; case QQShareTypeAudio: [urlString appendString:@"&file_type=audio"]; [urlString appendString:@"&req_type=2"]; if (title) [urlString appendFormat:@"&title=%@", [self encodeString:[self base64Encode:title]]]; if (description) [urlString appendFormat:@"&description=%@", [self encodeString:[self base64Encode:description]]]; if (url) [urlString appendFormat:@"&url=%@", [self encodeString:[self base64Encode:url]]]; if (thumbImage) { NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; NSString *fullPath = [@"file://" stringByAppendingString:thumbPath]; [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[self base64Encode:fullPath]]]; } break; case QQShareTypeVideo: // Video might be similar break; } // 详细的URL调试日志 NSLog(@"🔍 ======== 完整QQ分享URL ========"); NSLog(@"🔍 URL: %@", urlString); NSLog(@"🔍 URL长度: %lu", (unsigned long)[urlString length]); // 检查关键参数是否正确 if ([urlString containsString:[NSString stringWithFormat:@"app_id=%@", kQQAppID]]) { NSLog(@"✅ AppID参数已包含"); } else { NSLog(@"❌ AppID参数缺失!"); } NSURL *qqURL = [NSURL URLWithString:urlString]; if ([[UIApplication sharedApplication] canOpenURL:qqURL]) { if (@available(iOS 10.0, *)) { [[UIApplication sharedApplication] openURL:qqURL options:@{} completionHandler:^(BOOL success) { // 这里不调用completion,等待QQ返回再调用 NSLog(@"QQ分享调用结果: %@", success ? @"成功" : @"失败"); }]; } else { [[UIApplication sharedApplication] openURL:qqURL]; } } else { if (completion) { completion(NO); } } } + (void)shareToQZone:(QQShareType)type title:(NSString *)title description:(NSString *)description thumbImage:(UIImage *)thumbImage url:(NSString *)url images:(NSArray *)images completion:(void (^)(BOOL))completion { // 检查QQ是否已安装 if (![self isQQInstalled]) { if (completion) { completion(NO); } [self showAppNotInstalledAlert:@"QQ"]; return; } // 验证AppID配置(解决9000101错误的关键) if (![self validateQQAppIDConfiguration]) { if (completion) { completion(NO); } [self showAppIDConfigurationErrorAlert]; return; } // 保存回调 QQShareCompletion = completion; // 构建URL参数 - 修复QQ空间分享格式 NSMutableString *urlString = [NSMutableString stringWithString:kQQZoneScheme]; // 基础必需参数,严格按照QQ要求的格式和顺序 [urlString appendString:@"version=1"]; [urlString appendString:@"&cflag=0"]; [urlString appendFormat:@"&app_id=%@", kQQAppID]; // AppID必须在前面位置 [urlString appendString:@"&src_type=app"]; [urlString appendString:@"&sdkv=2.9.0"]; [urlString appendString:@"&sdkp=i"]; // 应用信息参数 - AppID是解决9000101错误的关键参数 [urlString appendFormat:@"&appid=%@", kQQAppID]; // QQ AppID - 必需参数 [urlString appendFormat:@"&app_name=%@", [self encodeString:kQQAppName]]; [urlString appendString:@"&callback_type=scheme"]; [urlString appendFormat:@"&callback_name=%@", [self encodeString:kQQCallbackScheme]]; // 添加内容参数 if (title && title.length > 0) { [urlString appendFormat:@"&title=%@", [self encodeString:title]]; } if (description && description.length > 0) { [urlString appendFormat:@"&description=%@", [self encodeString:description]]; } if (url && url.length > 0) { [urlString appendFormat:@"&url=%@", [self encodeString:url]]; } // 处理图片 if (images && images.count > 0) { NSMutableArray *imagePaths = [NSMutableArray array]; for (UIImage *image in images) { NSString *imagePath = [self saveImageToTempDirectory:image]; [imagePaths addObject:[@"file://" stringByAppendingString:imagePath]]; } if (imagePaths.count > 0) { [urlString appendFormat:@"&imageUrl=%@", [self encodeString:[imagePaths componentsJoinedByString:@","]]]; } } else if (thumbImage) { NSString *thumbPath = [self saveImageToTempDirectory:thumbImage]; [urlString appendFormat:@"&previewimageUrl=%@", [self encodeString:[@"file://" stringByAppendingString:thumbPath]]]; } // 设置分享类型 switch (type) { case QQShareTypeText: [urlString appendString:@"&req_type=0"]; break; case QQShareTypeImage: [urlString appendString:@"&req_type=2"]; break; case QQShareTypeNews: [urlString appendString:@"&req_type=1"]; break; case QQShareTypeAudio: [urlString appendString:@"&req_type=3"]; break; case QQShareTypeVideo: [urlString appendString:@"&req_type=4"]; break; } // 打开URL NSLog(@"QQ空间分享URL: %@", urlString); NSURL *qzoneURL = [NSURL URLWithString:urlString]; if ([[UIApplication sharedApplication] canOpenURL:qzoneURL]) { if (@available(iOS 10.0, *)) { [[UIApplication sharedApplication] openURL:qzoneURL options:@{} completionHandler:^(BOOL success) { // 这里不调用completion,等待QQ返回再调用 }]; } else { [[UIApplication sharedApplication] openURL:qzoneURL]; } } else { if (completion) { completion(NO); } } } + (BOOL)handleOpenURL:(NSURL *)url { // 判断是否是QQ返回的URL NSString *urlString = [url absoluteString]; NSLog(@"QQ分享回调URL: %@", urlString); if ([urlString hasPrefix:@"msext://"]) { // 解析返回的参数 NSString *errorDescription = [self getUrlParam:urlString paramName:@"error_description"]; NSString *error = [self getUrlParam:urlString paramName:@"error"]; BOOL success = YES; if (error && ![error isEqualToString:@"0"]) { success = NO; NSLog(@"QQ分享失败: error=%@, description=%@", error, errorDescription); } else { NSLog(@"QQ分享成功"); } // 执行回调 if (QQShareCompletion) { QQShareCompletion(success); QQShareCompletion = nil; } return YES; } return NO; } #pragma mark - 简化版QQ分享方法(解决900101错误) /** * 简化版QQ分享方法,使用最基础的参数避免900101错误 */ + (void)simpleShareToQQFriend:(NSString *)title description:(NSString *)description url:(NSString *)url completion:(void(^)(BOOL success))completion { if (![self isQQInstalled]) { if (completion) { completion(NO); } [self showAppNotInstalledAlert:@"QQ"]; return; } // 保存回调 QQShareCompletion = completion; // 使用最简单的URL格式,避免复杂参数导致的900101错误 NSMutableString *urlString = [NSMutableString stringWithString:@"mqqapi://share/to_fri?"]; // 最基础的必需参数 [urlString appendString:@"version=1"]; [urlString appendString:@"&cflag=0"]; // 分享类型 - 根据是否有URL决定 if (url && url.length > 0) { [urlString appendString:@"&req_type=1"]; // 网页分享 [urlString appendFormat:@"&url=%@", [self encodeString:url]]; } else { [urlString appendString:@"&req_type=0"]; // 文本分享 } // 内容参数 if (title && title.length > 0) { [urlString appendFormat:@"&title=%@", [self encodeString:title]]; } if (description && description.length > 0) { [urlString appendFormat:@"&description=%@", [self encodeString:description]]; } // 打开URL NSLog(@"简化QQ分享URL: %@", urlString); NSURL *qqURL = [NSURL URLWithString:urlString]; if ([[UIApplication sharedApplication] canOpenURL:qqURL]) { if (@available(iOS 10.0, *)) { [[UIApplication sharedApplication] openURL:qqURL options:@{} completionHandler:^(BOOL success) { NSLog(@"简化QQ分享调用结果: %@", success ? @"成功" : @"失败"); }]; } else { [[UIApplication sharedApplication] openURL:qqURL]; } } else { if (completion) { completion(NO); } } } + (void)forceOpenQQSessionSelector:(void(^)(BOOL success))completion { NSLog(@"尝试强制弹出QQ会话选择列表..."); // 检查QQ是否安装 if (![self isQQInstalled]) { NSLog(@"QQ未安装"); if (completion) completion(NO); return; } // 尝试的URL方案,按优先级排序 NSArray *urlTemplates = @[ // 方案1: 使用好友分享接口 @"mqqopensdkfriend://share?src_type=internal&version=1&app_name=%@&title=%@", // 方案2: 直接打开聊天选择界面 @"mqq://im/chat?chat_type=select", // 方案3: 打开好友列表 @"mqq://contact/friend_list", // 方案4: 打开最近会话 @"mqq://im/recent", // 方案5: 使用分享到好友 @"mqqapi://share/to_fri?file_type=text&file_data=%@&app_name=%@", // 方案6: 简化的card接口 @"mqqapi://card/show_pslcard?src_type=internal&version=1&app_name=%@&req_type=0&title=%@" ]; NSString *appName = [self encodeString:kQQAppName]; NSString *defaultTitle = [self encodeString:@"分享内容"]; // 逐个尝试每种方案 [self tryURLSchemes:urlTemplates appName:appName title:defaultTitle index:0 completion:completion]; } + (void)tryURLSchemes:(NSArray *)urlTemplates appName:(NSString *)appName title:(NSString *)title index:(NSInteger)index completion:(void(^)(BOOL success))completion { if (index >= urlTemplates.count) { NSLog(@"所有URL方案都尝试失败"); if (completion) completion(NO); return; } NSString *template = urlTemplates[index]; NSString *urlString; // 根据模板格式构造URL if ([template containsString:@"chat_type=select"] || [template containsString:@"friend_list"] || [template containsString:@"im/recent"]) { // 不需要参数的URL urlString = template; } else if ([template containsString:@"file_data"]) { // 需要两个参数的URL urlString = [NSString stringWithFormat:template, title, appName]; } else { // 需要app_name和title的URL urlString = [NSString stringWithFormat:template, appName, title]; } NSURL *url = [NSURL URLWithString:urlString]; NSLog(@"尝试方案%ld: %@", (long)(index + 1), urlString); if ([[UIApplication sharedApplication] canOpenURL:url]) { if (@available(iOS 10.0, *)) { [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:^(BOOL success) { if (success) { NSLog(@"方案%ld 成功打开QQ", (long)(index + 1)); // 延迟检查是否真的打开了会话选择 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ if (completion) completion(YES); }); } else { NSLog(@"方案%ld 打开失败,尝试下一个", (long)(index + 1)); [self tryURLSchemes:urlTemplates appName:appName title:title index:index + 1 completion:completion]; } }]; } else { [[UIApplication sharedApplication] openURL:url]; if (completion) completion(YES); } } else { NSLog(@"方案%ld 不支持,尝试下一个", (long)(index + 1)); [self tryURLSchemes:urlTemplates appName:appName title:title index:index + 1 completion:completion]; } } + (void)tryMultipleQQShareMethods:(NSString *)title description:(NSString *)description completion:(void(^)(BOOL success))completion { NSLog(@"使用多种方式尝试QQ分享..."); // 检查QQ是否安装 if (![self isQQInstalled]) { NSLog(@"QQ未安装"); if (completion) completion(NO); return; } // 方法1: 先尝试强制弹出会话选择器 [self forceOpenQQSessionSelector:^(BOOL success) { if (success) { NSLog(@"成功弹出会话选择器"); if (completion) completion(YES); } else { NSLog(@"会话选择器失败,尝试简化分享"); // 方法2: 使用简化分享 [self simpleShareToQQFriend:title description:description url:nil completion:^(BOOL simpleSuccess) { if (simpleSuccess) { NSLog(@"简化分享成功"); if (completion) completion(YES); } else { NSLog(@"简化分享也失败,使用标准分享"); // 方法3: 使用标准分享 [self shareToQQFriend:QQShareTypeText title:title description:description thumbImage:nil url:nil image:nil completion:completion]; } }]; } }]; } + (void)shareWithSystemShare:(NSString *)title description:(NSString *)description url:(NSString *)url fromViewController:(UIViewController *)viewController completion:(void (^)(BOOL))completion { // 准备分享内容数组 NSMutableArray *shareItems = [NSMutableArray array]; // 组合完整的分享文本(不包含URL) NSMutableString *shareText = [NSMutableString string]; // 添加标题 if (title && title.length > 0) { [shareText appendString:title]; } // 添加描述 if (description && description.length > 0) { if (shareText.length > 0) { [shareText appendString:@"\n"]; // 使用单个换行符,更紧凑 } [shareText appendString:description]; } // 添加组合文本到分享项目(在组合完成后添加) if (shareText.length > 0) { [shareItems addObject:[shareText copy]]; NSLog(@"🔍 ✅ 添加组合文本: %@", shareText); } // 统一使用固定的分享URL,不管传入的URL是什么 NSString *fixedShareURL = @"http://game.hudong.com"; NSURL *shareURL = [NSURL URLWithString:fixedShareURL]; if (shareURL) { [shareItems addObject:shareURL]; NSLog(@"🔍 ✅ 添加固定分享URL: %@", fixedShareURL); } // 添加应用桌面图标作为缩略图 UIImage *appIcon = [self getAppIcon]; if (appIcon) { [shareItems addObject:appIcon]; NSLog(@"🔍 ✅ 添加应用桌面图标作为缩略图"); } // 如果没有任何分享内容,使用默认文本 if (shareItems.count == 0) { [shareItems addObject:@"来自进贤聚友棋牌的精彩内容分享"]; NSURL *defaultURL = [NSURL URLWithString:fixedShareURL]; if (defaultURL) { [shareItems addObject:defaultURL]; } } // 详细的调试日志 NSLog(@"🔍 ======== 系统分享内容详情 ========"); NSLog(@"🔍 原始标题: %@", title ?: @"(无)"); NSLog(@"🔍 原始描述: %@", description ?: @"(无)"); NSLog(@"🔍 传入URL: %@", url ?: @"(无)"); NSLog(@"🔍 实际分享URL: http://game.hudong.com (固定)"); NSLog(@"🔍 分享文本: %@", shareText.length > 0 ? shareText : @"(无)"); NSLog(@"🔍 分享项目数量: %lu", (unsigned long)shareItems.count); // 打印所有分享项目 for (NSInteger i = 0; i < shareItems.count; i++) { id item = shareItems[i]; if ([item isKindOfClass:[NSString class]]) { NSLog(@"🔍 项目%ld (文本): %@", (long)i+1, item); } else if ([item isKindOfClass:[NSURL class]]) { NSLog(@"🔍 项目%ld (URL): %@", (long)i+1, [(NSURL *)item absoluteString]); } else { NSLog(@"🔍 项目%ld (其他): %@", (long)i+1, item); } } NSLog(@"🔍 ====================================="); // 创建系统分享控制器 UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil]; // 排除一些不常用的分享选项(可选,提高QQ等主要应用的显示优先级) NSArray *excludedActivityTypes = @[ UIActivityTypePostToVimeo, UIActivityTypePostToFlickr, UIActivityTypePostToTencentWeibo, UIActivityTypeAssignToContact, UIActivityTypeSaveToCameraRoll, UIActivityTypeAddToReadingList, UIActivityTypePostToFacebook, UIActivityTypePostToTwitter ]; activityVC.excludedActivityTypes = excludedActivityTypes; // 设置完成回调 activityVC.completionWithItemsHandler = ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { NSLog(@"🔍 ======== 系统分享结果详情 ========"); NSLog(@"🔍 分享结果: %@", completed ? @"✅ 成功" : @"❌ 取消/失败"); NSLog(@"🔍 分享到应用: %@", activityType ?: @"(未知)"); if (activityError) { NSLog(@"🔍 分享错误: %@", activityError.localizedDescription); // 特别处理 RunningBoard 相关错误 if ([activityError.domain isEqualToString:@"RBSServiceErrorDomain"]) { NSLog(@"🔍 ⚠️ 检测到RunningBoard权限错误,这通常是系统级权限问题"); NSLog(@"🔍 ⚠️ 建议检查应用是否在前台,以及设备权限设置"); } } // 特别标记QQ分享 if (activityType && ([activityType containsString:@"QQ"] || [activityType containsString:@"qq"])) { NSLog(@"🔍 ✅ QQ分享完成"); } NSLog(@"🔍 ====================================="); if (completion) { completion(completed); } }; // 在iPad上需要设置popover if ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad) { activityVC.popoverPresentationController.sourceView = viewController.view; activityVC.popoverPresentationController.sourceRect = CGRectMake(viewController.view.bounds.size.width/2, viewController.view.bounds.size.height/2, 0, 0); activityVC.popoverPresentationController.permittedArrowDirections = 0; } // 确保在主线程呈现分享面板,避免RunningBoard权限错误 dispatch_async(dispatch_get_main_queue(), ^{ // 再次检查视图控制器是否有效 if (viewController && viewController.view.window) { NSLog(@"🔍 在主线程呈现系统分享面板"); [viewController presentViewController:activityVC animated:YES completion:^{ NSLog(@"🔍 ✅ 系统分享面板呈现完成"); }]; } else { NSLog(@"🔍 ❌ 视图控制器无效或不在窗口层次结构中"); if (completion) { completion(NO); } } }); } + (void)shareWithSystemShareAuto:(NSString *)title description:(NSString *)description url:(NSString *)url completion:(void (^)(BOOL))completion { // 检查是否可以安全呈现分享面板 if (![self canSafelyPresentSharePanel]) { NSLog(@"🔍 ❌ 当前条件不允许呈现分享面板,避免RunningBoard错误"); if (completion) { completion(NO); } return; } UIViewController *topViewController = [self getTopViewController]; if (!topViewController) { NSLog(@"❌ 无法获取顶层视图控制器,无法弹出系统分享面板"); if (completion) { completion(NO); } return; } [self shareWithSystemShare:title description:description url:url fromViewController:topViewController completion:completion]; } #pragma mark - Private Methods + (UIViewController *)getTopViewController { // 检查应用状态,避免在后台或非活跃状态访问UI UIApplicationState appState = [UIApplication sharedApplication].applicationState; if (appState != UIApplicationStateActive) { NSLog(@"🔍 ⚠️ 应用不在活跃状态 (状态: %ld),可能导致权限错误", (long)appState); // 仍然尝试获取,但记录警告 } UIWindow *keyWindow = nil; // iOS 13+ 获取keyWindow的方式 if (@available(iOS 13.0, *)) { NSSet *connectedScenes = [UIApplication sharedApplication].connectedScenes; NSLog(@"🔍 当前连接的Scene数量: %lu", (unsigned long)connectedScenes.count); for (UIWindowScene *windowScene in connectedScenes) { NSLog(@"🔍 Scene状态: %ld", (long)windowScene.activationState); if (windowScene.activationState == UISceneActivationStateForegroundActive) { NSLog(@"🔍 找到前台活跃的WindowScene"); for (UIWindow *window in windowScene.windows) { NSLog(@"🔍 检查Window: isKeyWindow=%d, isHidden=%d", window.isKeyWindow, window.isHidden); if (window.isKeyWindow && !window.isHidden) { keyWindow = window; NSLog(@"🔍 ✅ 找到活跃的keyWindow"); break; } } if (keyWindow) break; } } } // 兼容iOS 13以下版本 if (!keyWindow) { NSLog(@"🔍 使用传统方式获取keyWindow"); keyWindow = [UIApplication sharedApplication].keyWindow; } // 如果还是没有,尝试获取第一个可见window if (!keyWindow) { NSLog(@"🔍 尝试获取第一个可见window"); NSArray *windows = [UIApplication sharedApplication].windows; for (UIWindow *window in windows) { if (!window.isHidden && window.rootViewController) { keyWindow = window; NSLog(@"🔍 使用备选window"); break; } } } if (!keyWindow) { NSLog(@"🔍 ❌ 无法获取任何有效的window,这可能导致RunningBoard权限错误"); return nil; } // 检查window是否有rootViewController if (!keyWindow.rootViewController) { NSLog(@"🔍 ❌ keyWindow没有rootViewController"); return nil; } // 获取顶层视图控制器 UIViewController *topViewController = keyWindow.rootViewController; NSLog(@"🔍 根视图控制器: %@", NSStringFromClass([topViewController class])); // 遍历presented视图控制器 int presentedCount = 0; while (topViewController.presentedViewController && presentedCount < 10) { // 防止无限循环 topViewController = topViewController.presentedViewController; presentedCount++; NSLog(@"🔍 Presented视图控制器 %d: %@", presentedCount, NSStringFromClass([topViewController class])); } // 处理UINavigationController if ([topViewController isKindOfClass:[UINavigationController class]]) { UINavigationController *navController = (UINavigationController *)topViewController; topViewController = navController.topViewController; NSLog(@"🔍 导航控制器顶层: %@", NSStringFromClass([topViewController class])); } // 处理UITabBarController if ([topViewController isKindOfClass:[UITabBarController class]]) { UITabBarController *tabController = (UITabBarController *)topViewController; topViewController = tabController.selectedViewController; NSLog(@"🔍 标签控制器选中: %@", NSStringFromClass([topViewController class])); // 再次检查是否是NavigationController if ([topViewController isKindOfClass:[UINavigationController class]]) { UINavigationController *navController = (UINavigationController *)topViewController; topViewController = navController.topViewController; NSLog(@"🔍 标签内导航控制器顶层: %@", NSStringFromClass([topViewController class])); } } // 最终验证 if (topViewController && topViewController.view.window) { NSLog(@"🔍 ✅ 成功获取有效的顶层视图控制器: %@", NSStringFromClass([topViewController class])); } else { NSLog(@"🔍 ⚠️ 获取的视图控制器可能无效或不在window层次结构中"); } return topViewController; } + (BOOL)canSafelyPresentSharePanel { NSLog(@"🔍 ======== 检查分享面板呈现条件 ========"); // 1. 检查应用状态 UIApplicationState appState = [UIApplication sharedApplication].applicationState; NSLog(@"🔍 应用状态: %ld (0=Active, 1=Inactive, 2=Background)", (long)appState); if (appState != UIApplicationStateActive) { NSLog(@"🔍 ❌ 应用不在活跃状态,无法安全呈现分享面板"); return NO; } // 2. 检查是否有有效的视图控制器 UIViewController *topVC = [self getTopViewController]; if (!topVC) { NSLog(@"🔍 ❌ 无法获取顶层视图控制器"); return NO; } if (!topVC.view.window) { NSLog(@"🔍 ❌ 视图控制器不在window层次结构中"); return NO; } // 3. 检查是否已经有模态视图 if (topVC.presentedViewController) { NSLog(@"🔍 ❌ 已有模态视图控制器正在呈现: %@", NSStringFromClass([topVC.presentedViewController class])); return NO; } // 4. 检查是否在主线程 if (![NSThread isMainThread]) { NSLog(@"🔍 ❌ 不在主线程,可能导致RunningBoard错误"); return NO; } // 5. iOS 13+ Scene状态检查 if (@available(iOS 13.0, *)) { BOOL hasActiveScene = NO; NSSet *connectedScenes = [UIApplication sharedApplication].connectedScenes; for (UIWindowScene *windowScene in connectedScenes) { if ([windowScene isKindOfClass:[UIWindowScene class]] && windowScene.activationState == UISceneActivationStateForegroundActive) { hasActiveScene = YES; break; } } if (!hasActiveScene) { NSLog(@"🔍 ❌ 没有活跃的前台Scene"); return NO; } } NSLog(@"🔍 ✅ 所有条件满足,可以安全呈现分享面板"); NSLog(@"🔍 ====================================="); return YES; } + (NSString *)base64Encode:(NSString *)string { NSData *data = [string dataUsingEncoding:NSUTF8StringEncoding]; return [data base64EncodedStringWithOptions:0]; } + (NSString *)encodeString:(NSString *)string { if (!string) return @""; // We must encode everything that is strictly reserved or unsafe in a query parameter value // Especially + / = which are key for Base64 NSMutableCharacterSet *allowed = [[NSCharacterSet alphanumericCharacterSet] mutableCopy]; [allowed addCharactersInString:@"-._~"]; // Unreserved characters per RFC 3986 return [string stringByAddingPercentEncodingWithAllowedCharacters:allowed]; } + (NSString *)saveImageToTempDirectory:(UIImage *)image { NSData *imageData = UIImageJPEGRepresentation(image, 0.7); NSString *fileName = [NSString stringWithFormat:@"qq_share_%@.jpg", [self generateUUID]]; NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:fileName]; [imageData writeToFile:filePath atomically:YES]; return filePath; } + (NSString *)saveImageToDocumentsDirectory:(UIImage *)image { // Compress image to ensure it's not too large (similar to WeChat's 0.6 behavior) NSData *imageData = UIImageJPEGRepresentation(image, 0.6); NSString *fileName = [NSString stringWithFormat:@"qq_share_persistent_%@.jpg", [self generateUUID]]; // Use Documents directory for persistence NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES); NSString *documentsDirectory = [paths firstObject]; NSString *filePath = [documentsDirectory stringByAppendingPathComponent:fileName]; [imageData writeToFile:filePath atomically:YES]; NSLog(@"🔍 [QQShareManager] Saved image to Documents: %@", filePath); return filePath; } + (NSString *)generateUUID { CFUUIDRef uuid = CFUUIDCreate(NULL); CFStringRef uuidString = CFUUIDCreateString(NULL, uuid); NSString *result = (__bridge_transfer NSString *)uuidString; CFRelease(uuid); return result; } + (NSString *)getUrlParam:(NSString *)url paramName:(NSString *)name { NSError *error; NSString *regTags = [[NSString alloc] initWithFormat:@"(^|&|\\?)%@=([^&]*)(&|$)", name]; NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regTags options:NSRegularExpressionCaseInsensitive error:&error]; NSTextCheckingResult *match = [regex firstMatchInString:url options:0 range:NSMakeRange(0, [url length])]; if (match) { NSString *paramValue = [url substringWithRange:[match rangeAtIndex:2]]; return paramValue; } return nil; } + (void)showAppNotInstalledAlert:(NSString *)appName { dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:[NSString stringWithFormat:@"%@未安装", appName] message:[NSString stringWithFormat:@"您的设备未安装%@客户端,无法进行分享", appName] preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; [alertController addAction:okAction]; UIViewController *topVC = [self topViewController]; [topVC presentViewController:alertController animated:YES completion:nil]; }); } + (void)showAppIDConfigurationErrorAlert { dispatch_async(dispatch_get_main_queue(), ^{ UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"QQ分享配置错误" message:@"QQ AppID配置错误,请联系开发者检查配置" preferredStyle:UIAlertControllerStyleAlert]; UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"确定" style:UIAlertActionStyleDefault handler:nil]; [alertController addAction:okAction]; UIViewController *topVC = [self topViewController]; [topVC presentViewController:alertController animated:YES completion:nil]; }); } + (UIViewController *)topViewController { UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController; while (rootViewController.presentedViewController) { rootViewController = rootViewController.presentedViewController; } if ([rootViewController isKindOfClass:[UINavigationController class]]) { UINavigationController *navigationController = (UINavigationController *)rootViewController; return navigationController.visibleViewController; } if ([rootViewController isKindOfClass:[UITabBarController class]]) { UITabBarController *tabBarController = (UITabBarController *)rootViewController; UIViewController *selectedViewController = tabBarController.selectedViewController; if ([selectedViewController isKindOfClass:[UINavigationController class]]) { UINavigationController *navigationController = (UINavigationController *)selectedViewController; return navigationController.visibleViewController; } return selectedViewController; } return rootViewController; } + (UIImage *)getAppLaunchImage { // 保留此方法以备将来需要,但现在主要使用桌面图标 NSLog(@"🔍 ⚠️ 建议使用 getAppIcon 方法获取桌面图标"); return [self getAppIcon]; } + (UIImage *)getAppIcon { // 尝试获取应用桌面图标 UIImage *appIcon = nil; // 方法1.1: 优先尝试直接从 Asset Catalog 中获取 "AppIcon-1" // 很多时候 Xcode 并不会把 AppIcon 打包成松散文件,而是编译进 Assets.car // 我们发现 Info.plist 并未包含 CFBundleIcons (在某些旧项目配置中),但 Xcode 设置了 ASSETCATALOG_COMPILER_APPICON_NAME = "AppIcon-1" appIcon = [UIImage imageNamed:@"AppIcon-1"]; if (appIcon) { NSLog(@"🔍 ✅ 获取应用图标成功: AppIcon-1"); return appIcon; } appIcon = [UIImage imageNamed:@"AppIcon"]; if (appIcon) { NSLog(@"🔍 ✅ 获取应用图标成功: AppIcon"); return appIcon; } // 方法1.2: 从Info.plist获取图标信息 NSDictionary *infoPlist = [[NSBundle mainBundle] infoDictionary]; // iOS 13+ 支持的新格式 NSDictionary *icons = infoPlist[@"CFBundleIcons"]; NSDictionary *primaryIcon = icons[@"CFBundlePrimaryIcon"]; NSArray *iconFiles = primaryIcon[@"CFBundleIconFiles"]; if (iconFiles && iconFiles.count > 0) { // 按优先级尝试不同尺寸的图标(从大到小) NSArray *preferredSizes = @[@"180", @"120", @"60"]; for (NSString *size in preferredSizes) { for (NSString *iconName in iconFiles) { NSString *testName = [NSString stringWithFormat:@"%@%@", iconName, size]; appIcon = [UIImage imageNamed:testName]; if (appIcon) { NSLog(@"🔍 ✅ 获取应用图标成功: %@", testName); return appIcon; } } } // 尝试原始名称 for (NSString *iconName in iconFiles) { appIcon = [UIImage imageNamed:iconName]; if (appIcon) { NSLog(@"🔍 ✅ 获取应用图标成功: %@", iconName); return appIcon; } } } // 方法2: 尝试常见的图标名称 NSArray *commonIconNames = @[ @"Icon-180", // iPhone @3x (180x180) @"Icon180", // 常见命名 @"Icon-120", // iPhone @2x (120x120) @"Icon120", // 常见命名 @"Icon-60", // iPhone @1x (60x60) @"Icon60", // 常见命名 @"AppIcon60x60@3x", @"AppIcon60x60@2x", @"AppIcon60x60", @"AppIcon", // 通用名称 @"Icon", // 简单名称 @"icon" // 小写 ]; for (NSString *iconName in commonIconNames) { appIcon = [UIImage imageNamed:iconName]; if (appIcon) { NSLog(@"🔍 ✅ 获取应用图标成功: %@", iconName); return appIcon; } } // 方法3: 从App Bundle中搜索图标文件 NSBundle *mainBundle = [NSBundle mainBundle]; NSArray *imageExtensions = @[@"png", @"jpg", @"jpeg"]; for (NSString *ext in imageExtensions) { NSArray *iconPaths = [mainBundle pathsForResourcesOfType:ext inDirectory:nil]; for (NSString *path in iconPaths) { NSString *filename = [[path lastPathComponent] stringByDeletingPathExtension]; // 检查文件名是否包含icon相关关键词 if ([filename.lowercaseString containsString:@"icon"] || [filename.lowercaseString containsString:@"appicon"]) { appIcon = [UIImage imageWithContentsOfFile:path]; if (appIcon) { NSLog(@"🔍 ✅ 从Bundle获取应用图标: %@", filename); return appIcon; } } } } NSLog(@"🔍 ⚠️ 无法获取应用桌面图标,创建默认图标"); return [self createDefaultAppIcon]; } + (UIImage *)createDefaultAppIcon { // 创建一个简单的默认应用图标 CGSize iconSize = CGSizeMake(120, 120); UIGraphicsBeginImageContextWithOptions(iconSize, NO, [UIScreen mainScreen].scale); // 设置渐变背景色(模拟应用图标的效果) CGContextRef context = UIGraphicsGetCurrentContext(); // 创建渐变色 CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); CGFloat colors[] = { 0.2, 0.6, 1.0, 1.0, // 蓝色 0.1, 0.4, 0.8, 1.0 // 深蓝色 }; CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, colors, NULL, 2); // 绘制渐变背景 CGPoint startPoint = CGPointMake(0, 0); CGPoint endPoint = CGPointMake(iconSize.width, iconSize.height); CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, 0); // 释放资源 CGGradientRelease(gradient); CGColorSpaceRelease(colorSpace); // 添加圆角效果(模拟iOS图标圆角) UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, iconSize.width, iconSize.height) cornerRadius:iconSize.width * 0.2]; [roundedRect addClip]; // 添加文字 NSString *text = @"聚友\n棋牌"; NSDictionary *attributes = @{ NSFontAttributeName: [UIFont boldSystemFontOfSize:18], NSForegroundColorAttributeName: [UIColor whiteColor], NSParagraphStyleAttributeName: ({ NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; style.alignment = NSTextAlignmentCenter; style; }) }; CGSize textSize = [text boundingRectWithSize:iconSize options:NSStringDrawingUsesLineFragmentOrigin attributes:attributes context:nil].size; CGRect textRect = CGRectMake((iconSize.width - textSize.width) / 2, (iconSize.height - textSize.height) / 2, textSize.width, textSize.height); [text drawInRect:textRect withAttributes:attributes]; UIImage *defaultIcon = UIGraphicsGetImageFromCurrentImageContext(); UIGraphicsEndImageContext(); NSLog(@"🔍 ✅ 创建默认应用图标"); return defaultIcon; } + (NSString *)validateAndFixURL:(NSString *)url { if (!url || url.length == 0) { return nil; } // 移除首尾空格 NSString *trimmedURL = [url stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; NSLog(@"🔍 开始验证URL: %@", trimmedURL); // 检查是否是自定义scheme(如http://sharegame/...) if ([trimmedURL hasPrefix:@"http://sharegame/"] || [trimmedURL hasPrefix:@"https://sharegame/"]) { // 将自定义的sharegame域名替换为合法的域名 NSString *fixedURL = [trimmedURL stringByReplacingOccurrencesOfString:@"http://sharegame/" withString:@"https://game.example.com/"]; fixedURL = [fixedURL stringByReplacingOccurrencesOfString:@"https://sharegame/" withString:@"https://game.example.com/"]; NSLog(@"🔍 修复自定义域名URL: %@ -> %@", trimmedURL, fixedURL); return fixedURL; } // 检查是否已经有协议头 if (![trimmedURL hasPrefix:@"http://"] && ![trimmedURL hasPrefix:@"https://"] && ![trimmedURL hasPrefix:@"ftp://"]) { // 如果没有协议头,添加https:// NSString *fixedURL = [@"https://" stringByAppendingString:trimmedURL]; NSLog(@"🔍 添加协议头: %@ -> %@", trimmedURL, fixedURL); return fixedURL; } // 验证URL是否有效 NSURL *testURL = [NSURL URLWithString:trimmedURL]; if (testURL && testURL.scheme && testURL.host) { NSLog(@"🔍 ✅ URL格式有效: %@", trimmedURL); return trimmedURL; } // 如果URL无效,尝试进行URL编码 NSString *encodedURL = [trimmedURL stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; if (encodedURL) { NSURL *encodedTestURL = [NSURL URLWithString:encodedURL]; if (encodedTestURL && encodedTestURL.scheme && encodedTestURL.host) { NSLog(@"🔍 ✅ URL编码后有效: %@ -> %@", trimmedURL, encodedURL); return encodedURL; } } NSLog(@"🔍 ❌ URL无法修复,将作为文本处理: %@", trimmedURL); return nil; } + (void)shareWithSystemShareContent:(id)shareContent completion:(void (^ _Nullable)(BOOL success))completion { NSLog(@"🔍 [QQShareManager] 开始使用ShareContent对象进行系统分享"); // 主线程检查 if (![NSThread isMainThread]) { NSLog(@"⚠️ [QQShareManager] 检测到非主线程调用,切换到主线程"); dispatch_async(dispatch_get_main_queue(), ^{ [self shareWithSystemShareContent:shareContent completion:completion]; }); return; } // 权限检查 if (![self canSafelyPresentSharePanel]) { NSLog(@"❌ [QQShareManager] 无法安全呈现系统分享面板"); if (completion) { completion(NO); } return; } // 检查ShareContent对象 if (!shareContent) { NSLog(@"❌ [QQShareManager] ShareContent对象为空"); if (completion) { completion(NO); } return; } // 通过KVC从ShareContent对象中提取参数 NSString *title = nil; NSString *description = nil; NSString *url = nil; NSString *type = nil; @try { // 尝试获取标题 if ([shareContent respondsToSelector:@selector(valueForKey:)]) { title = [shareContent valueForKey:@"title"]; description = [shareContent valueForKey:@"desc"]; url = [shareContent valueForKey:@"webpageUrl"]; type = [shareContent valueForKey:@"type"]; } NSLog(@"🔍 [QQShareManager] 从ShareContent提取参数:"); NSLog(@"🔍 - 标题: %@", title ?: @"(无)"); NSLog(@"🔍 - 描述: %@", description ?: @"(无)"); NSLog(@"🔍 - 链接: %@", url ?: @"(无)"); NSLog(@"🔍 - 类型: %@", type ?: @"(无)"); } @catch (NSException *exception) { NSLog(@"❌ [QQShareManager] 解析ShareContent时发生异常: %@", exception.reason); if (completion) { completion(NO); } return; } // 检查分享类型,决定分享方式 BOOL isScreenshotShare = [type intValue] == 2; NSLog(@"🔍 [QQShareManager] 分享类型: %@", isScreenshotShare ? @"截图分享" : @"文本分享"); // 设置默认值 if (!title || title.length == 0) { title = isScreenshotShare ? @"游戏截图分享" : @"游戏分享"; } if (!description || description.length == 0) { description = isScreenshotShare ? @"精彩游戏画面分享" : @"精彩游戏内容分享"; } // 构建分享内容项 NSMutableArray *shareItems = [NSMutableArray array]; if (isScreenshotShare) { // 截图分享模式 NSLog(@"🔍 [QQShareManager] 执行截图分享"); // 获取屏幕截图 UIImage *screenImage = [FuncPublic getImageWithFullScreenshot]; if (screenImage) { [shareItems addObject:screenImage]; NSLog(@"🔍 [QQShareManager] 添加屏幕截图,尺寸: %.0fx%.0f", screenImage.size.width, screenImage.size.height); description = @"精彩游戏画面分享"; // 为截图添加文字说明 NSString *combinedText = nil; if (title && description) { combinedText = [NSString stringWithFormat:@"%@\n%@", title, description]; } else if (title) { combinedText = title; } else if (description) { combinedText = description; } if (combinedText) { [shareItems addObject:combinedText]; NSLog(@"🔍 [QQShareManager] 添加截图说明文字: %@", combinedText); } } else { NSLog(@"❌ [QQShareManager] 截图获取失败,降级为文本分享"); isScreenshotShare = NO; // 降级处理 } } if (!isScreenshotShare) { // 纯文本分享模式(真正的纯文本,不包含URL和图标) NSLog(@"🔍 [QQShareManager] 执行纯文本分享(仅文本内容)"); // 组合完整的分享文本 NSMutableString *shareText = [NSMutableString string]; // 添加标题 if (title && title.length > 0) { [shareText appendString:title]; } // 添加描述 if (description && description.length > 0) { if (shareText.length > 0) { [shareText appendString:@"\n\n"]; // 使用单个换行符 } [shareText appendString:description]; } // 仅添加组合文本到分享项目(纯文本分享) if (shareText.length > 0) { [shareItems addObject:[shareText copy]]; NSLog(@"🔍 [QQShareManager] 添加纯文本内容: %@", shareText); } else { // 如果没有任何文本,使用默认文本 [shareItems addObject:@"来自进贤聚友棋牌的精彩内容分享"]; NSLog(@"🔍 [QQShareManager] 使用默认纯文本内容"); } // 纯文本分享:不添加URL,不添加应用图标 NSLog(@"🔍 [QQShareManager] 纯文本分享模式:不包含URL和图标"); } // 检查分享内容 if (shareItems.count == 0) { NSLog(@"❌ [QQShareManager] 没有有效的分享内容"); if (completion) { completion(NO); } return; } // 详细的调试日志 NSLog(@"🔍 ======== ShareContent系统分享详情 ========"); NSLog(@"🔍 分享类型: %@", isScreenshotShare ? @"截图分享" : @"纯文本分享"); NSLog(@"🔍 原始标题: %@", title ?: @"(无)"); NSLog(@"🔍 原始描述: %@", description ?: @"(无)"); NSLog(@"🔍 传入URL: %@", url ?: @"(无)"); if (!isScreenshotShare) { NSLog(@"🔍 纯文本模式:不包含URL和图标"); } NSLog(@"🔍 type字段: %@", type ?: @"(无)"); NSLog(@"🔍 分享项目数量: %lu", (unsigned long)shareItems.count); // 打印所有分享项目(与shareWithSystemShare保持一致) for (NSInteger i = 0; i < shareItems.count; i++) { id item = shareItems[i]; if ([item isKindOfClass:[NSString class]]) { NSLog(@"🔍 项目%ld (文本): %@", (long)i+1, item); } else if ([item isKindOfClass:[NSURL class]]) { NSLog(@"🔍 项目%ld (URL): %@", (long)i+1, [(NSURL *)item absoluteString]); } else if ([item isKindOfClass:[UIImage class]]) { UIImage *image = (UIImage *)item; NSLog(@"🔍 项目%ld (图片): %.0fx%.0f", (long)i+1, image.size.width, image.size.height); } else { NSLog(@"🔍 项目%ld (其他): %@", (long)i+1, item); } } NSLog(@"🔍 ========================================="); NSLog(@"🔍 [QQShareManager] 准备分享 %lu 个项目", (unsigned long)shareItems.count); // 获取顶层视图控制器 UIViewController *topViewController = [self getTopViewController]; if (!topViewController) { NSLog(@"❌ [QQShareManager] 无法获取顶层视图控制器"); if (completion) { completion(NO); } return; } // 创建UIActivityViewController UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil]; // 排除不需要的分享选项(可选) activityVC.excludedActivityTypes = @[ UIActivityTypeAssignToContact, UIActivityTypePostToVimeo, UIActivityTypePrint, UIActivityTypeAirDrop // 排除AirDrop避免意外分享 ]; // 设置完成回调(与shareWithSystemShare保持一致的日志格式) activityVC.completionWithItemsHandler = ^(UIActivityType activityType, BOOL completed, NSArray *returnedItems, NSError *activityError) { NSLog(@"🔍 ======== ShareContent分享结果详情 ========"); NSLog(@"🔍 分享结果: %@", completed ? @"✅ 成功" : @"❌ 取消/失败"); NSLog(@"🔍 分享到应用: %@", activityType ?: @"(未知)"); if (activityError) { NSLog(@"🔍 分享错误: %@", activityError.localizedDescription); // 特别处理 RunningBoard 相关错误(与shareWithSystemShare一致) if ([activityError.domain isEqualToString:@"RBSServiceErrorDomain"]) { NSLog(@"🔍 ⚠️ 检测到RunningBoard权限错误,这通常是系统级权限问题"); NSLog(@"🔍 ⚠️ 建议检查应用是否在前台,以及设备权限设置"); } if (completion) { completion(NO); } return; } // 特别标记QQ分享(与shareWithSystemShare一致) if (activityType && ([activityType containsString:@"QQ"] || [activityType containsString:@"qq"])) { NSLog(@"🔍 ✅ QQ分享完成"); } NSLog(@"🔍 ========================================="); if (completion) { completion(completed); } }; // iPad适配 - 设置popover if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) { if (activityVC.popoverPresentationController) { activityVC.popoverPresentationController.sourceView = topViewController.view; activityVC.popoverPresentationController.sourceRect = CGRectMake( topViewController.view.bounds.size.width / 2, topViewController.view.bounds.size.height / 2, 1, 1 ); activityVC.popoverPresentationController.permittedArrowDirections = 0; } } // 呈现分享面板 [topViewController presentViewController:activityVC animated:YES completion:^{ NSLog(@"🔍 [QQShareManager] 系统分享面板已呈现"); }]; } + (void)shareWithContent:(id)shareContent completion:(void (^ _Nullable)(BOOL success))completion { NSLog(@"🔍 [QQShareManager] 开始QQ直接分享(自动拉起会话选择)"); // 检查QQ是否已安装 if (![self isQQInstalled]) { NSLog(@"❌ [QQShareManager] QQ未安装"); if (completion) { completion(NO); } [self showAppNotInstalledAlert:@"QQ"]; return; } // 提取参数 NSString *title = nil; NSString *description = nil; NSString *webpageUrl = nil; NSString *type = nil; @try { if ([shareContent respondsToSelector:@selector(valueForKey:)]) { title = [shareContent valueForKey:@"title"]; description = [shareContent valueForKey:@"desc"]; webpageUrl = [shareContent valueForKey:@"webpageUrl"]; type = [shareContent valueForKey:@"type"]; } } @catch (NSException *exception) { NSLog(@"❌ [QQShareManager] 参数提取异常: %@", exception); if (completion) completion(NO); return; } // 判断分享类型 BOOL isScreenshotShare = [type intValue] == 2; if (isScreenshotShare) { // 截图分享 (Image) UIImage *screenImage = [FuncPublic getImageWithFullScreenshot]; // 如果截屏失败或者为空,尝试检查是否有 image 字段(可能是Base64或UIImage对象) if (!screenImage) { id imageObj = nil; if ([shareContent respondsToSelector:@selector(valueForKey:)]) { @try { imageObj = [shareContent valueForKey:@"image"]; } @catch (NSException *e) {} } if (imageObj && [imageObj isKindOfClass:[UIImage class]]) { screenImage = imageObj; } else if (imageObj && [imageObj isKindOfClass:[NSString class]]) { NSData *data = [[NSData alloc] initWithBase64EncodedString:imageObj options:0]; if (data) screenImage = [UIImage imageWithData:data]; } } if (!screenImage) { NSLog(@"❌ [QQShareManager] 截图失败且未通过参数传入图片"); if (completion) completion(NO); return; } [self shareToQQFriend:QQShareTypeImage title:title description:description thumbImage:nil url:nil image:screenImage completion:completion]; } else { // 网页/链接分享 (News) // 默认使用应用图标作为缩略图 UIImage *thumbImage = [self getAppIcon]; [self shareToQQFriend:QQShareTypeNews title:title description:description thumbImage:thumbImage url:webpageUrl image:nil completion:completion]; } } @end