Files
youle_app_ios/msext/Class/Utils/QQShareManager.m
2026-02-12 21:52:27 +08:00

1922 lines
78 KiB
Objective-C
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//
// QQShareManager.m
// msext
//
// Created on 2025/06/15.
// Copyright © 2025年. All rights reserved.
//
#import "QQShareManager.h"
#import "FuncPublic.h" // 用于截图功能
#import <LinkPresentation/LinkPresentation.h> // 引入 LinkPresentation 用于自定义分享预览
// 尝试检查是否存在 TencentOpenAPI SDK
#if __has_include(<TencentOpenAPI/QQApiInterface.h>)
#import <TencentOpenAPI/QQApiInterface.h>
#import <TencentOpenAPI/TencentOAuth.h>
#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 <UIActivityItemSource>
@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 是 iconProvideriOS 默认会将其渲染为 "Icon" 样式(带白边、圆角遮罩等)。
// 如果提供的 image 是 imageProvideriOS 会将其渲染为 "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位数字当前长度%luAppID%@", (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<UIImage *> *)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<UIScene *> *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<UIScene *> *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