优化:日志输出配置
This commit is contained in:
parent
59ab3b247a
commit
66bbf38c64
@ -4,7 +4,9 @@ import android.app.ActivityManager
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
import com.facebook.FacebookSdk
|
import com.facebook.FacebookSdk
|
||||||
|
import com.facebook.LoggingBehavior
|
||||||
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
import io.flutter.embedding.engine.plugins.FlutterPlugin
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
@ -54,12 +56,25 @@ class ClientProxyFrameworkPlugin : FlutterPlugin, MethodChannel.MethodCallHandle
|
|||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
|
"setFacebookSdkDebugLogging" -> {
|
||||||
|
val ctx = applicationContext
|
||||||
|
if (ctx == null) {
|
||||||
|
result.error("NO_CONTEXT", "applicationContext null", null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val enabled = call.argument<Boolean>("enabled") ?: false
|
||||||
|
applyFacebookSdkDebugLogging(enabled)
|
||||||
|
result.success(null)
|
||||||
|
}
|
||||||
"waitForFacebookSdkInit" -> {
|
"waitForFacebookSdkInit" -> {
|
||||||
val ctx = applicationContext
|
val ctx = applicationContext
|
||||||
if (ctx == null) {
|
if (ctx == null) {
|
||||||
result.error("NO_CONTEXT", "applicationContext null", null)
|
result.error("NO_CONTEXT", "applicationContext null", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
val args = call.arguments as? Map<*, *>
|
||||||
|
val debugLogs = coerceFacebookSdkDebugLogsArg(args?.get("facebookSdkDebugLogs"))
|
||||||
|
applyFacebookSdkDebugLogging(debugLogs)
|
||||||
val mainHandler = Handler(Looper.getMainLooper())
|
val mainHandler = Handler(Looper.getMainLooper())
|
||||||
try {
|
try {
|
||||||
// SDK 18 无 addInitializedCallback;用 sdkInitialize(..., InitializeCallback) 在就绪后回调
|
// SDK 18 无 addInitializedCallback;用 sdkInitialize(..., InitializeCallback) 在就绪后回调
|
||||||
@ -71,6 +86,8 @@ class ClientProxyFrameworkPlugin : FlutterPlugin, MethodChannel.MethodCallHandle
|
|||||||
override fun onInitialized() {
|
override fun onInitialized() {
|
||||||
mainHandler.post {
|
mainHandler.post {
|
||||||
try {
|
try {
|
||||||
|
// 再应用一次:避免 FacebookInitProvider / 其他插件抢先初始化导致标志被吃掉
|
||||||
|
applyFacebookSdkDebugLogging(debugLogs)
|
||||||
channel?.invokeMethod("onFacebookSdkInitialized", null)
|
channel?.invokeMethod("onFacebookSdkInitialized", null)
|
||||||
result.success(true)
|
result.success(true)
|
||||||
} catch (e: Exception) {
|
} 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 {
|
companion object {
|
||||||
const val CHANNEL_NAME = "client_proxy_framework/facebook_sdk"
|
const val CHANNEL_NAME = "client_proxy_framework/facebook_sdk"
|
||||||
const val DEVICE_MEMORY_CHANNEL_NAME = "client_proxy_framework/device_memory"
|
const val DEVICE_MEMORY_CHANNEL_NAME = "client_proxy_framework/device_memory"
|
||||||
|
|||||||
@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
按模块整理的“功能 + 最小调用示例”请见 **[《Framework 功能与用法总览》](framework_feature_usage.md)**。
|
按模块整理的“功能 + 最小调用示例”请见 **[《Framework 功能与用法总览》](framework_feature_usage.md)**。
|
||||||
|
|
||||||
|
以首款换皮应用 **FunyMee** 为参考的 **视频首页**(`common_info` → 分类 → 按分类拉模板)数据流说明,见 **[《视频首页数据获取流程》](video_home_data_flow.md)**。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. 快速开始
|
## 2. 快速开始
|
||||||
|
|||||||
134
docs/video_home_data_flow.md
Normal file
134
docs/video_home_data_flow.md
Normal file
@ -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 首推路线一致。
|
||||||
@ -1,5 +1,7 @@
|
|||||||
import Flutter
|
import Flutter
|
||||||
import UIKit
|
import UIKit
|
||||||
|
import FBSDKCoreKit
|
||||||
|
import os.log
|
||||||
|
|
||||||
public class ClientProxyFrameworkPlugin: NSObject, FlutterPlugin {
|
public class ClientProxyFrameworkPlugin: NSObject, FlutterPlugin {
|
||||||
private var channel: FlutterMethodChannel?
|
private var channel: FlutterMethodChannel?
|
||||||
@ -14,11 +16,53 @@ public class ClientProxyFrameworkPlugin: NSObject, FlutterPlugin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
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)
|
result(true)
|
||||||
channel?.invokeMethod("onFacebookSdkInitialized", arguments: nil)
|
channel?.invokeMethod("onFacebookSdkInitialized", arguments: nil)
|
||||||
} else {
|
default:
|
||||||
result(FlutterMethodNotImplemented)
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,8 @@ Pod::Spec.new do |s|
|
|||||||
s.source = { :path => '.' }
|
s.source = { :path => '.' }
|
||||||
s.source_files = 'Classes/**/*'
|
s.source_files = 'Classes/**/*'
|
||||||
s.dependency 'Flutter'
|
s.dependency 'Flutter'
|
||||||
|
# 与 facebook_app_events 一致,便于在桥接层开启 FBSDK 调试日志
|
||||||
|
s.dependency 'FBSDKCoreKit', '~> 18.0'
|
||||||
s.platform = :ios, '13.0'
|
s.platform = :ios, '13.0'
|
||||||
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' }
|
||||||
s.swift_version = '5.0'
|
s.swift_version = '5.0'
|
||||||
|
|||||||
@ -32,6 +32,7 @@ class FacebookConfig {
|
|||||||
const FacebookConfig({
|
const FacebookConfig({
|
||||||
required this.appId,
|
required this.appId,
|
||||||
this.clientToken,
|
this.clientToken,
|
||||||
|
/// 是否打开 **原生 Facebook SDK** 详细日志(Logcat / Xcode 控制台),与 [AnalyticsConfig.debugLogs] 独立。
|
||||||
this.debugLogs = false,
|
this.debugLogs = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -351,7 +351,7 @@ class SkinConfig implements AppConfig {
|
|||||||
fbCfg = FacebookConfig(
|
fbCfg = FacebookConfig(
|
||||||
appId: id.trim(),
|
appId: id.trim(),
|
||||||
clientToken: ct.isEmpty ? null : ct,
|
clientToken: ct.isEmpty ? null : ct,
|
||||||
debugLogs: fb['debugLogs'] as bool? ?? false,
|
debugLogs: _coerceConfigBool(fb['debugLogs']),
|
||||||
);
|
);
|
||||||
} else if (id.trim().isNotEmpty || clientTok.trim().isNotEmpty) {
|
} else if (id.trim().isNotEmpty || clientTok.trim().isNotEmpty) {
|
||||||
SdkReminderLog.facebook(
|
SdkReminderLog.facebook(
|
||||||
@ -415,4 +415,16 @@ class SkinConfig implements AppConfig {
|
|||||||
}
|
}
|
||||||
return const PlatformAttributionConfig();
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -96,7 +96,14 @@ void _logLong(String text) {
|
|||||||
|
|
||||||
/// 格式化输出嵌入在字符串中的 JSON,保持缩进对齐。
|
/// 格式化输出嵌入在字符串中的 JSON,保持缩进对齐。
|
||||||
void logWithEmbeddedJson(Object? msg) {
|
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) {
|
if (msg is! String) {
|
||||||
_proxyLog.d(msg);
|
_proxyLog.d(msg);
|
||||||
@ -184,6 +191,38 @@ class AppLogger {
|
|||||||
|
|
||||||
static Logger? _logger;
|
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 {
|
static Logger get _instance {
|
||||||
_logger ??= Logger(
|
_logger ??= Logger(
|
||||||
printer: PrettyPrinter(
|
printer: PrettyPrinter(
|
||||||
@ -194,14 +233,22 @@ class AppLogger {
|
|||||||
printEmojis: true,
|
printEmojis: true,
|
||||||
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
|
dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart,
|
||||||
),
|
),
|
||||||
level: kReleaseMode ? Level.warning : Level.trace,
|
level: _resolveLogLevel(),
|
||||||
);
|
);
|
||||||
return _logger!;
|
return _logger!;
|
||||||
}
|
}
|
||||||
|
|
||||||
String _msg(Object? message) => '[$tag] $message';
|
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 i(Object? message) => _instance.i(_msg(message));
|
||||||
void w(Object? message) => _instance.w(_msg(message));
|
void w(Object? message) => _instance.w(_msg(message));
|
||||||
void e(Object? message, [Object? error, StackTrace? stackTrace]) =>
|
void e(Object? message, [Object? error, StackTrace? stackTrace]) =>
|
||||||
|
|||||||
@ -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<void> trackPurchaseSuccess(double amount) async {
|
static Future<void> trackPurchaseSuccess(double amount) async {
|
||||||
_skin.trackAdjustEvent('purchase');
|
_skin.trackAdjustEvent('purchase');
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@ -87,6 +87,10 @@ abstract final class AnalyticsEvents {
|
|||||||
final today = DateTime.now().toIso8601String().substring(0, 10);
|
final today = DateTime.now().toIso8601String().substring(0, 10);
|
||||||
if (regDate != null && regDate == today) {
|
if (regDate != null && regDate == today) {
|
||||||
_skin.trackAdjustEvent('firstPurchase');
|
_skin.trackAdjustEvent('firstPurchase');
|
||||||
|
FacebookService.logEvent(
|
||||||
|
'FirstRecharge',
|
||||||
|
parameters: <String, dynamic>{'amount': amount},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (amount > 0) {
|
if (amount > 0) {
|
||||||
AnalyticsService.trackPurchase(amount: amount, currency: 'USD');
|
AnalyticsService.trackPurchase(amount: amount, currency: 'USD');
|
||||||
|
|||||||
@ -152,6 +152,8 @@ abstract class FrameworkAuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (res == null) {
|
if (res == null) {
|
||||||
|
VideoHomeRuntime.reset();
|
||||||
|
ExtConfigRuntime.applyCommonInfoFailure();
|
||||||
completer.complete();
|
completer.complete();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -180,9 +182,13 @@ abstract class FrameworkAuthService {
|
|||||||
deviceId: deviceId,
|
deviceId: deviceId,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
VideoHomeRuntime.reset();
|
||||||
|
ExtConfigRuntime.applyCommonInfoFailure();
|
||||||
_callbacks!.onLoginFailed(res.msg);
|
_callbacks!.onLoginFailed(res.msg);
|
||||||
}
|
}
|
||||||
} catch (e, st) {
|
} catch (e, st) {
|
||||||
|
VideoHomeRuntime.reset();
|
||||||
|
ExtConfigRuntime.applyCommonInfoFailure();
|
||||||
if (kDebugMode) {
|
if (kDebugMode) {
|
||||||
debugPrint('[AuthService] start: 异常 $e\n$st');
|
debugPrint('[AuthService] start: 异常 $e\n$st');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:facebook_app_events/facebook_app_events.dart';
|
import 'package:facebook_app_events/facebook_app_events.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
import '../config/attribution_config.dart';
|
import '../config/attribution_config.dart';
|
||||||
@ -43,9 +44,22 @@ class FacebookService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await _channel!
|
await _channel!
|
||||||
.invokeMethod<void>('waitForFacebookSdkInit')
|
.invokeMethod<void>(
|
||||||
|
'waitForFacebookSdkInit',
|
||||||
|
<String, dynamic>{
|
||||||
|
'facebookSdkDebugLogs': config.debugLogs,
|
||||||
|
},
|
||||||
|
)
|
||||||
.timeout(_nativeInitTimeout);
|
.timeout(_nativeInitTimeout);
|
||||||
_log.d('waitForFacebookSdkInit finished');
|
_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) {
|
} on TimeoutException catch (e, st) {
|
||||||
SdkReminderLog.facebook(
|
SdkReminderLog.facebook(
|
||||||
'原生初始化超时 ($e),已跳过。请检查是否集成框架 Android/iOS 插件、`AndroidManifest` / Info.plist 中 Facebook 配置是否完整。\n$st',
|
'原生初始化超时 ($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<void> setFacebookSdkDebugLogging(bool enabled) async {
|
||||||
|
try {
|
||||||
|
_channel ??= MethodChannel(kFacebookSdkChannelName);
|
||||||
|
await _channel!.invokeMethod<void>(
|
||||||
|
'setFacebookSdkDebugLogging',
|
||||||
|
<String, dynamic>{'enabled': enabled},
|
||||||
|
);
|
||||||
|
} on MissingPluginException catch (_) {
|
||||||
|
SdkReminderLog.facebook(
|
||||||
|
'setFacebookSdkDebugLogging:未注册原生插件,已忽略。',
|
||||||
|
);
|
||||||
|
} catch (e, st) {
|
||||||
|
SdkReminderLog.facebook('setFacebookSdkDebugLogging 失败: $e\n$st');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 手动上报应用激活(当 `AndroidManifest` / `Info.plist` 关闭自动 App Events 时常用)。
|
/// 手动上报应用激活(当 `AndroidManifest` / `Info.plist` 关闭自动 App Events 时常用)。
|
||||||
static void activateApp() {
|
static void activateApp() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user