1922 lines
78 KiB
Objective-C
1922 lines
78 KiB
Objective-C
//
|
||
// 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 是 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<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 |