diff --git a/android/src/main/kotlin/com/funymee/client_proxy_framework/ClientProxyFrameworkPlugin.kt b/android/src/main/kotlin/com/funymee/client_proxy_framework/ClientProxyFrameworkPlugin.kt index ae4ff14..72ca5e8 100644 --- a/android/src/main/kotlin/com/funymee/client_proxy_framework/ClientProxyFrameworkPlugin.kt +++ b/android/src/main/kotlin/com/funymee/client_proxy_framework/ClientProxyFrameworkPlugin.kt @@ -4,7 +4,9 @@ import android.app.ActivityManager import android.content.Context import android.os.Handler import android.os.Looper +import android.util.Log import com.facebook.FacebookSdk +import com.facebook.LoggingBehavior import io.flutter.embedding.engine.plugins.FlutterPlugin import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodChannel @@ -54,12 +56,25 @@ class ClientProxyFrameworkPlugin : FlutterPlugin, MethodChannel.MethodCallHandle override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { when (call.method) { + "setFacebookSdkDebugLogging" -> { + val ctx = applicationContext + if (ctx == null) { + result.error("NO_CONTEXT", "applicationContext null", null) + return + } + val enabled = call.argument("enabled") ?: false + applyFacebookSdkDebugLogging(enabled) + result.success(null) + } "waitForFacebookSdkInit" -> { val ctx = applicationContext if (ctx == null) { result.error("NO_CONTEXT", "applicationContext null", null) return } + val args = call.arguments as? Map<*, *> + val debugLogs = coerceFacebookSdkDebugLogsArg(args?.get("facebookSdkDebugLogs")) + applyFacebookSdkDebugLogging(debugLogs) val mainHandler = Handler(Looper.getMainLooper()) try { // SDK 18 无 addInitializedCallback;用 sdkInitialize(..., InitializeCallback) 在就绪后回调 @@ -71,6 +86,8 @@ class ClientProxyFrameworkPlugin : FlutterPlugin, MethodChannel.MethodCallHandle override fun onInitialized() { mainHandler.post { try { + // 再应用一次:避免 FacebookInitProvider / 其他插件抢先初始化导致标志被吃掉 + applyFacebookSdkDebugLogging(debugLogs) channel?.invokeMethod("onFacebookSdkInitialized", null) result.success(true) } catch (e: Exception) { @@ -88,6 +105,41 @@ class ClientProxyFrameworkPlugin : FlutterPlugin, MethodChannel.MethodCallHandle } } + /** + * 由宿主 `skin_config` 的 `facebook.debugLogs` 或 MethodChannel `setFacebookSdkDebugLogging` 控制。 + * 关闭时恢复为 SDK 默认(仅保留 [LoggingBehavior.DEVELOPER_ERRORS])。 + */ + private fun applyFacebookSdkDebugLogging(enabled: Boolean) { + FacebookSdk.setIsDebugEnabled(enabled) + FacebookSdk.clearLoggingBehaviors() + if (enabled) { + FacebookSdk.addLoggingBehavior(LoggingBehavior.APP_EVENTS) + FacebookSdk.addLoggingBehavior(LoggingBehavior.REQUESTS) + FacebookSdk.addLoggingBehavior(LoggingBehavior.DEVELOPER_ERRORS) + FacebookSdk.addLoggingBehavior(LoggingBehavior.GRAPH_API_DEBUG_INFO) + Log.i( + "ClientProxyFB", + "Facebook SDK debug logging ON — also filter Logcat: tag FacebookSDK or Request", + ) + } else { + FacebookSdk.addLoggingBehavior(LoggingBehavior.DEVELOPER_ERRORS) + Log.i("ClientProxyFB", "Facebook SDK debug logging OFF (verbose behaviors cleared)") + } + } + + /** Flutter Map 里可能是 Bool / Int / String,避免一直落在默认 false。 */ + private fun coerceFacebookSdkDebugLogsArg(raw: Any?): Boolean { + return when (raw) { + is Boolean -> raw + is Number -> raw.toInt() != 0 + is String -> { + val s = raw.trim().lowercase() + s == "true" || s == "1" || s == "yes" + } + else -> false + } + } + companion object { const val CHANNEL_NAME = "client_proxy_framework/facebook_sdk" const val DEVICE_MEMORY_CHANNEL_NAME = "client_proxy_framework/device_memory" diff --git a/docs/README.md b/docs/README.md index 9a4219b..5a5498f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,6 +19,8 @@ 按模块整理的“功能 + 最小调用示例”请见 **[《Framework 功能与用法总览》](framework_feature_usage.md)**。 +以首款换皮应用 **FunyMee** 为参考的 **视频首页**(`common_info` → 分类 → 按分类拉模板)数据流说明,见 **[《视频首页数据获取流程》](video_home_data_flow.md)**。 + --- ## 2. 快速开始 diff --git a/docs/video_home_data_flow.md b/docs/video_home_data_flow.md new file mode 100644 index 0000000..364bf87 --- /dev/null +++ b/docs/video_home_data_flow.md @@ -0,0 +1,134 @@ +# 视频首页数据获取流程(以首款换皮应用 FunyMee 为参考) + +本文描述**框架侧**在典型换皮应用(首款为 **FunyMee**)中,从冷启动到「视频首页」可展示分类与模板列表的**数据链路与调用顺序**。FunyMee 宿主 UI 不在本仓库内;流程以 `client_proxy_framework` 中的 `FrameworkAuthService`、`ExtConfigRuntime`、`VideoHomeRuntime`、`ImageApi` 为准,与代码注释中引用的线网接口(如 `GET /v1/image/img2video/tasks`)一致。 + +--- + +## 1. 前提与配置 + +1. **`ClientBootstrap.initFromAsset`**:加载 `skin_config.json`,构建 `SkinConfig` 并 `ApiClient.init`,使后续请求走统一代理、AES 与 V2 包装(见 `lib/src/bootstrap/client_bootstrap.dart`)。 +2. **`FrameworkAuthService.init` + `start`**:宿主实现 `AuthServiceCallbacks`(设备 ID、签名等),由框架编排登录与 `common_info`(见 `lib/src/services/auth_service.dart`)。 +3. **`skin_config.json`** 中与首页相关的片段: + - **`extConfig`**:`keys.showVideoMenu` 映射到线网布尔(示例中为 `go_run` / `need_wait` 等,见 `lib/src/config/skin_config.example.json`);`items` 与 `taskItemMapping` 决定 `common_info` 下发的运营位卡片如何解析为与任务列表同形的 `ExtConfigItem`。 + - **`videoHome`**:`imagesTabLabel`、`imagesTabFirst` 控制「Images」虚拟 Tab 的文案及与接口分类的排序(见 `lib/src/config/skin_config.dart`)。 +4. **`fieldMapping`**:FunyMee 等换皮下线网字段与逻辑字段不一致时,响应体会先经映射再进入实体解析(注释示例见 `lib/src/services/image_api.dart`)。 + +--- + +## 2. 总览时序 + +```mermaid +sequenceDiagram + participant Host as 宿主 main / UI + participant Boot as ClientBootstrap + participant Auth as FrameworkAuthService + participant User as UserApi / ProxyClient + participant Ext as ExtConfigRuntime + participant VH as VideoHomeRuntime + participant Img as ImageApi + + Host->>Boot: initFromAsset(skin_config) + Host->>Auth: init(callbacks); start() + Auth->>User: POST /v1/user/fast_login + User-->>Auth: userToken, userId + Auth->>User: GET /v1/user/common_info + User-->>Auth: extConfig 等 + Auth->>Ext: applyCommonInfoSuccess + Auth->>VH: hydrateAfterCommonInfo (异步) + VH->>Img: GET /v1/image/img2video/categories + Img-->>VH: 分类列表 + VH->>VH: 合并 Tabs(接口分类 + ext items → Images) + VH->>Img: GET /v1/image/img2video/tasks?categoryId=… + Img-->>VH: 当前分类模板列表 +``` + +--- + +## 3. 分阶段说明 + +### 3.1 启动与快速登录 + +- 延迟与重试策略由 `FrameworkAuthService.start` 控制(默认启动延迟、登录重试等)。 +- **`UserApi.fastLogin`**:`POST /v1/user/fast_login`。请求头**仅**带 `pkg`,**不带** `User_token`(见 `UserApi` 文档注释)。 +- 成功后框架将返回的 `userToken` 写入 **`ApiClient.instance.setUserToken`**,此后代理请求自动附带 `pkg` 与 `User_token`(见 `ProxyClient` 行为与 `UserApi` 说明)。 + +### 3.2 归因上报(与首页数据并行准备) + +- 在拉取 `common_info` 之前,若存在 Adjust / Play 等 referrer,会依次调用 **`UserApi.referrer`**(`POST /v1/user/referrer`)上报,不阻塞后续 `common_info` 的成功与否结论,但属于同一 `_reportReferrersAndLoadCommonInfo` 流程(见 `auth_service.dart`)。 + +### 3.3 通用信息与 extConfig + +- **`UserApi.getCommonInfo`**:`GET /v1/user/common_info`,query 含 `app`(iOS/Android 后端渠道,与 `skin_config.backend` 一致)、`pkg`、`userId`、`deviceId` 等(见 `user_api.dart`)。 +- 成功时调用 **`ExtConfigRuntime.applyCommonInfoSuccess`**: + - 将 `extConfig` 与本地 `extConfig.defaults` 浅合并; + - 按 `extConfigKeySchema` 解析为 **`ExtConfigData`**(含 `showVideoMenu`、`items` 等)(见 `ext_config_runtime.dart`、`ext_config_models.dart`)。 +- **`ExtConfigRuntime.commonInfoSucceeded`**:宿主可用「登录完成且 `common_info` 成功」再展示主业务界面(见 `ext_config_runtime.dart` 注释建议)。 + +### 3.4 视频首页运行时:水合(hydrate) + +在 `common_info` **成功**后,`FrameworkAuthService` 会 **fire-and-forget** 调用 **`VideoHomeRuntime.hydrateAfterCommonInfo`**(不阻塞 `loginComplete` 的完成)(见 `auth_service.dart`)。 + +`hydrateAfterCommonInfo` 的进入条件与行为(见 `video_home_runtime.dart`): + +| 条件 | 行为 | +|------|------| +| `userId` 为空,或 `ExtConfigData.showVideoMenu != true` | **`VideoHomeRuntime.reset`**,不拉分类、无 images Tab | +| 否则 | 置 `snapshot.loading = true`,再请求分类列表 | + +水合步骤概要: + +1. **`ImageApi.getCategoryList`** → `GET /v1/image/img2video/categories`,得到服务端分类(`CategoryItem`,含 `id` / `name`)。 +2. 从 **`ExtConfigRuntime.data`** 读取 `items`,过滤 **`ExtConfigItem.isUsableOnHome`**,非空则存在 **Images** 虚拟 Tab,文案为 `AppConfig.videoHomeImagesTabLabel`(来自 `skin_config.videoHome.imagesTabLabel`)。 +3. **Tab 顺序**:由 `videoHomeImagesTabFirst` 决定是先 Images 还是先接口分类。 +4. **默认选中 Tab**:默认选中**第一个服务端分类**(非 Images);若仅有 Images Tab 则下标为 0;并发水合与用户提前切换时有保护逻辑(见 `video_home_runtime.dart` 内注释)。 +5. 水合结束后调用 **`VideoHomeRuntime.ensureTabItems(selectedTabIndex)`**,为当前 Tab 拉取模板数据(见下节)。 + +若分类接口失败且没有任何 Tab 可构建,`snapshot.error` 会携带失败信息。 + +### 3.5 按 Tab 拉取模板列表 + +- **Images Tab**:数据直接来自 **`ExtConfigData.items`**(已在水合前解析),**不再**请求 `img2video/tasks`。 +- **服务端分类 Tab**:首次选中该 Tab 时,**`VideoHomeRuntime.ensureTabItems`** 调用 **`ImageApi.getImg2VideoTasks(categoryId: id)`** → `GET /v1/image/img2video/tasks?categoryId=...`(与 FunyMee 文档一致)。结果通过 `ExtConfigItem.fromTaskItem` 写入 **`VideoHomeSnapshot.networkItemsByCategoryId`**,并按分类 id 缓存,避免重复请求。 + +--- + +## 4. 宿主 UI 对接要点 + +- **监听状态**:`VideoHomeRuntime.snapshot`、`VideoHomeRuntime.selectedTabIndex` 为 `ValueNotifier`,可用 `ValueListenableBuilder` 或类似方式驱动顶栏 Tab 与内容区。 +- **切换 Tab**:切换 `selectedTabIndex` 后应调用 **`VideoHomeRuntime.ensureTabItems(newIndex)`**(框架水合末尾已对初始 Tab 调用一次),以按需加载该分类下的模板列表。 +- **登录 / common_info 失败**:`ExtConfigRuntime.commonInfoSucceeded == false` 或 `userId` 为空时,不会进入视频首页水合;宿主应展示错误态或重试入口。 + +--- + +## 5. 相关 HTTP 接口汇总 + +| 顺序 | 方法 | 路径 | 用途 | +|------|------|------|------| +| 1 | POST | `/v1/user/fast_login` | 设备登录,取得 token | +| 2 | POST | `/v1/user/referrer` | 可选,归因上报 | +| 3 | GET | `/v1/user/common_info` | 开关与 `extConfig`(含首页运营位 `items`) | +| 4 | GET | `/v1/image/img2video/categories` | 视频首页顶栏服务端分类 | +| 5 | GET | `/v1/image/img2video/tasks` | 某分类下模板列表(query:`categoryId`) | + +所有经 `ProxyClient` 的业务请求均走 **`skin_config` 中的 `proxyPath`**,body 为加密后的代理载荷;响应经解密与 **`fieldMapping`** 还原为逻辑字段后再解析实体。 + +--- + +## 6. 代码索引(便于跳转) + +| 模块 | 路径 | +|------|------| +| 启动换皮配置 | `lib/src/bootstrap/client_bootstrap.dart` | +| 登录与 common_info 编排 | `lib/src/services/auth_service.dart` | +| 用户接口 | `lib/src/services/user_api.dart` | +| 图/视频分类与任务列表 | `lib/src/services/image_api.dart` | +| extConfig 运行时 | `lib/src/config/ext_config_runtime.dart` | +| 视频首页 Tab 与缓存 | `lib/src/config/video_home_runtime.dart` | +| extConfig / items 解析 | `lib/src/config/ext_config_models.dart` | +| JSON 配置示例 | `lib/src/config/skin_config.example.json` | + +--- + +## 7. 与《创建新换皮应用》的关系 + +从零搭建宿主工程、资产路径与 `main` 初始化顺序,仍以 **[create_new_skin_app.md](create_new_skin_app.md)** 为准;本文仅补充 **「视频首页」在框架内的数据流**,与 FunyMee 首推路线一致。 diff --git a/ios/Classes/ClientProxyFrameworkPlugin.swift b/ios/Classes/ClientProxyFrameworkPlugin.swift index 1bbe7fe..c9eee98 100644 --- a/ios/Classes/ClientProxyFrameworkPlugin.swift +++ b/ios/Classes/ClientProxyFrameworkPlugin.swift @@ -1,5 +1,7 @@ import Flutter import UIKit +import FBSDKCoreKit +import os.log public class ClientProxyFrameworkPlugin: NSObject, FlutterPlugin { private var channel: FlutterMethodChannel? @@ -14,11 +16,53 @@ public class ClientProxyFrameworkPlugin: NSObject, FlutterPlugin { } public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { - if call.method == "waitForFacebookSdkInit" { + switch call.method { + case "setFacebookSdkDebugLogging": + let args = call.arguments as? [String: Any] + let enabled = Self.coerceFacebookSdkDebugLogsArg(args?["enabled"]) + Self.applyFacebookSdkDebugLogging(enabled: enabled) + result(nil) + case "waitForFacebookSdkInit": + let args = call.arguments as? [String: Any] + let debugLogs = Self.coerceFacebookSdkDebugLogsArg(args?["facebookSdkDebugLogs"]) + Self.applyFacebookSdkDebugLogging(enabled: debugLogs) result(true) channel?.invokeMethod("onFacebookSdkInitialized", arguments: nil) - } else { + default: result(FlutterMethodNotImplemented) } } + + /// 与 Android 一致:由 `facebook.debugLogs` 或 Dart `setFacebookSdkDebugLogging` 控制。 + private static func applyFacebookSdkDebugLogging(enabled: Bool) { + if enabled { + Settings.shared.loggingBehaviors = Set([ + .appEvents, + .networkRequests, + .informational, + .developerErrors, + .graphAPIDebugInfo, + ]) + os_log("Facebook SDK debug logging ON (filter console for FBSDK / Facebook)", log: .default, type: .info) + print("ClientProxyFB: Facebook SDK debug logging ON — also search Xcode console for FBSDK / FacebookSDK") + } else { + Settings.shared.loggingBehaviors = [] + os_log("Facebook SDK debug logging OFF", log: .default, type: .info) + print("ClientProxyFB: Facebook SDK debug logging OFF") + } + } + + private static func coerceFacebookSdkDebugLogsArg(_ raw: Any?) -> Bool { + switch raw { + case let b as Bool: + return b + case let n as NSNumber: + return n.intValue != 0 + case let s as String: + let v = s.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + return v == "true" || v == "1" || v == "yes" + default: + return false + } + } } diff --git a/ios/client_proxy_framework.podspec b/ios/client_proxy_framework.podspec index 142a596..72aad33 100644 --- a/ios/client_proxy_framework.podspec +++ b/ios/client_proxy_framework.podspec @@ -11,6 +11,8 @@ Pod::Spec.new do |s| s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.dependency 'Flutter' + # 与 facebook_app_events 一致,便于在桥接层开启 FBSDK 调试日志 + s.dependency 'FBSDKCoreKit', '~> 18.0' s.platform = :ios, '13.0' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' diff --git a/lib/src/config/attribution_config.dart b/lib/src/config/attribution_config.dart index 09ac781..85234ac 100644 --- a/lib/src/config/attribution_config.dart +++ b/lib/src/config/attribution_config.dart @@ -32,6 +32,7 @@ class FacebookConfig { const FacebookConfig({ required this.appId, this.clientToken, + /// 是否打开 **原生 Facebook SDK** 详细日志(Logcat / Xcode 控制台),与 [AnalyticsConfig.debugLogs] 独立。 this.debugLogs = false, }); diff --git a/lib/src/config/skin_config.dart b/lib/src/config/skin_config.dart index 3e4d542..46cc005 100644 --- a/lib/src/config/skin_config.dart +++ b/lib/src/config/skin_config.dart @@ -351,7 +351,7 @@ class SkinConfig implements AppConfig { fbCfg = FacebookConfig( appId: id.trim(), clientToken: ct.isEmpty ? null : ct, - debugLogs: fb['debugLogs'] as bool? ?? false, + debugLogs: _coerceConfigBool(fb['debugLogs']), ); } else if (id.trim().isNotEmpty || clientTok.trim().isNotEmpty) { SdkReminderLog.facebook( @@ -415,4 +415,16 @@ class SkinConfig implements AppConfig { } return const PlatformAttributionConfig(); } + + /// JSON / 远程配置里偶发 `1`/`"true"`,避免 `as bool?` 失败或恒为 false。 + static bool _coerceConfigBool(dynamic value, {bool fallback = false}) { + if (value == true || value == 1) return true; + if (value == false || value == 0) return false; + if (value is String) { + final s = value.trim().toLowerCase(); + if (s == 'true' || s == '1' || s == 'yes') return true; + if (s == 'false' || s == '0' || s == 'no') return false; + } + return fallback; + } } diff --git a/lib/src/log/app_logger.dart b/lib/src/log/app_logger.dart index 34bf172..ad6e9ee 100644 --- a/lib/src/log/app_logger.dart +++ b/lib/src/log/app_logger.dart @@ -96,7 +96,14 @@ void _logLong(String text) { /// 格式化输出嵌入在字符串中的 JSON,保持缩进对齐。 void logWithEmbeddedJson(Object? msg) { - if (!kDebugMode) return; + if (kReleaseMode) { + final releaseLevel = + const String.fromEnvironment('APP_LOG_LEVEL').trim().toLowerCase(); + final allowVerbose = releaseLevel == 'all' || + releaseLevel == 'trace' || + releaseLevel == 'debug'; + if (!allowVerbose) return; + } if (msg is! String) { _proxyLog.d(msg); @@ -184,6 +191,38 @@ class AppLogger { static Logger? _logger; + static bool _releaseVerboseEnabled() { + if (!kReleaseMode) return false; + final raw = const String.fromEnvironment('APP_LOG_LEVEL').trim().toLowerCase(); + return raw == 'all' || raw == 'trace' || raw == 'debug'; + } + + static Level _resolveLogLevel() { + final raw = const String.fromEnvironment('APP_LOG_LEVEL').trim().toLowerCase(); + switch (raw) { + case 'all': + case 'trace': + return Level.trace; + case 'debug': + return Level.debug; + case 'info': + return Level.info; + case 'warning': + case 'warn': + return Level.warning; + case 'error': + return Level.error; + case 'fatal': + case 'wtf': + return Level.fatal; + case 'off': + case 'none': + return Level.off; + default: + return kReleaseMode ? Level.warning : Level.trace; + } + } + static Logger get _instance { _logger ??= Logger( printer: PrettyPrinter( @@ -194,14 +233,22 @@ class AppLogger { printEmojis: true, dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, ), - level: kReleaseMode ? Level.warning : Level.trace, + level: _resolveLogLevel(), ); return _logger!; } String _msg(Object? message) => '[$tag] $message'; - void d(Object? message) => _instance.d(_msg(message)); + void d(Object? message) { + final m = _msg(message); + if (_releaseVerboseEnabled()) { + // Release + trace/debug 时兜底直出,避免第三方 logger 在某些终端链路中被过滤。 + debugPrint(m); + return; + } + _instance.d(m); + } void i(Object? message) => _instance.i(_msg(message)); void w(Object? message) => _instance.w(_msg(message)); void e(Object? message, [Object? error, StackTrace? stackTrace]) => diff --git a/lib/src/services/analytics_events.dart b/lib/src/services/analytics_events.dart index 089109e..59d8b8e 100644 --- a/lib/src/services/analytics_events.dart +++ b/lib/src/services/analytics_events.dart @@ -78,7 +78,7 @@ abstract final class AnalyticsEvents { } } - /// 支付成功:Adjust `purchase`;若注册当日则 `firstPurchase`;Facebook [AnalyticsService.trackPurchase]。 + /// 支付成功:Adjust `purchase`;若注册当日则 `firstPurchase` + Facebook `FirstRecharge`(与 app_client [AdjustEvents.trackFirstPurchase] 一致);Facebook 标准购买见 [AnalyticsService.trackPurchase]。 static Future trackPurchaseSuccess(double amount) async { _skin.trackAdjustEvent('purchase'); final prefs = await SharedPreferences.getInstance(); @@ -87,6 +87,10 @@ abstract final class AnalyticsEvents { final today = DateTime.now().toIso8601String().substring(0, 10); if (regDate != null && regDate == today) { _skin.trackAdjustEvent('firstPurchase'); + FacebookService.logEvent( + 'FirstRecharge', + parameters: {'amount': amount}, + ); } if (amount > 0) { AnalyticsService.trackPurchase(amount: amount, currency: 'USD'); diff --git a/lib/src/services/auth_service.dart b/lib/src/services/auth_service.dart index dc3c8b0..1139726 100644 --- a/lib/src/services/auth_service.dart +++ b/lib/src/services/auth_service.dart @@ -152,6 +152,8 @@ abstract class FrameworkAuthService { } if (res == null) { + VideoHomeRuntime.reset(); + ExtConfigRuntime.applyCommonInfoFailure(); completer.complete(); return; } @@ -180,9 +182,13 @@ abstract class FrameworkAuthService { deviceId: deviceId, ); } else { + VideoHomeRuntime.reset(); + ExtConfigRuntime.applyCommonInfoFailure(); _callbacks!.onLoginFailed(res.msg); } } catch (e, st) { + VideoHomeRuntime.reset(); + ExtConfigRuntime.applyCommonInfoFailure(); if (kDebugMode) { debugPrint('[AuthService] start: 异常 $e\n$st'); } diff --git a/lib/src/services/facebook_service.dart b/lib/src/services/facebook_service.dart index 6907698..305b185 100644 --- a/lib/src/services/facebook_service.dart +++ b/lib/src/services/facebook_service.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:facebook_app_events/facebook_app_events.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import '../config/attribution_config.dart'; @@ -43,9 +44,22 @@ class FacebookService { }); await _channel! - .invokeMethod('waitForFacebookSdkInit') + .invokeMethod( + 'waitForFacebookSdkInit', + { + 'facebookSdkDebugLogs': config.debugLogs, + }, + ) .timeout(_nativeInitTimeout); _log.d('waitForFacebookSdkInit finished'); + if (config.debugLogs) { + debugPrint( + '[Facebook] 已请求原生打开 Facebook SDK 调试日志(skin: analytics.facebook.debugLogs)。' + ' 若看不到 FacebookSDK 相关行:请「完全重启」应用(改 asset 后热重载无效)、' + 'Android Logcat 级别选 Verbose/Debug 并搜索 FacebookSDK 或 ClientProxyFB;' + ' 关闭自动 App Events 时需有网络请求或手动 logEvent/activateApp 才会有大量 REQUESTS 日志。', + ); + } } on TimeoutException catch (e, st) { SdkReminderLog.facebook( '原生初始化超时 ($e),已跳过。请检查是否集成框架 Android/iOS 插件、`AndroidManifest` / Info.plist 中 Facebook 配置是否完整。\n$st', @@ -95,6 +109,26 @@ class FacebookService { } } + /// 宿主可随时调用,覆盖原生 Facebook SDK 日志开关(不持久化,下次冷启动仍以 [FacebookConfig.debugLogs] 为准)。 + /// + /// **注意:** [init] 内会再次按配置应用开关;若需在运行时单独打开,请在 [init] 完成后再调用本方法, + /// 或把 `skin_config` 里 `analytics.facebook.debugLogs` 设为 `true`。 + static Future setFacebookSdkDebugLogging(bool enabled) async { + try { + _channel ??= MethodChannel(kFacebookSdkChannelName); + await _channel!.invokeMethod( + 'setFacebookSdkDebugLogging', + {'enabled': enabled}, + ); + } on MissingPluginException catch (_) { + SdkReminderLog.facebook( + 'setFacebookSdkDebugLogging:未注册原生插件,已忽略。', + ); + } catch (e, st) { + SdkReminderLog.facebook('setFacebookSdkDebugLogging 失败: $e\n$st'); + } + } + /// 手动上报应用激活(当 `AndroidManifest` / `Info.plist` 关闭自动 App Events 时常用)。 static void activateApp() { try {