Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec8cf06c66 | ||
|
|
8060805c84 | ||
|
|
96f183c2c0 | ||
|
|
96d6f1b34e | ||
|
|
487ad9e074 | ||
|
|
9e29282be3 | ||
|
|
6fe0d2ec7a | ||
|
|
0312555501 | ||
|
|
bd96495482 | ||
|
|
84063edcb2 | ||
|
|
10bbfc4c82 | ||
|
|
d0949704ed | ||
|
|
1fce98f7c3 | ||
|
|
d8513d1f1d | ||
|
|
56ad003e04 | ||
|
|
f947f3d2f8 |
@@ -1 +1,3 @@
|
||||
{}
|
||||
{
|
||||
"vimMode": true
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
{}
|
||||
{
|
||||
"theme": "moonstone"
|
||||
}
|
||||
@@ -176,7 +176,7 @@
|
||||
}
|
||||
],
|
||||
"direction": "horizontal",
|
||||
"width": 300
|
||||
"width": 326.390625
|
||||
},
|
||||
"left-ribbon": {
|
||||
"hiddenItems": {
|
||||
@@ -191,6 +191,30 @@
|
||||
},
|
||||
"active": "9775ada2a6c4ad3e",
|
||||
"lastOpenFiles": [
|
||||
"CrashDiagnosticsDemo/entry/build/default/outputs/default/entry-default-signed.hap",
|
||||
"CrashDiagnosticsDemo/entry/build/default/intermediates/default/temp/signConfigCheckJson.json",
|
||||
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/AuthSession.ets-fdb57a143b7ecc425dbeed2e5ed65f533bc4b7678045d59c12cc1712bd95861e.msgpack",
|
||||
"CrashDiagnosticsDemo/entry/build/default/intermediates/loader_out/default/ets/modules.abc",
|
||||
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-StartupLogin.ets-2334e21c16054ff88652c51913810e6a1bb6e8c3b9caae78194e6fa61d22454f.msgpack",
|
||||
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-Index.ets-f90047a353dd9b221b44a8e9fb622244ad78231d625fc4cd827257cdb1f3fdd9.msgpack",
|
||||
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-AuthSession.ets-fdb57a143b7ecc425dbeed2e5ed65f533bc4b7678045d59c12cc1712bd95861e.msgpack",
|
||||
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/compiler.cache/modules/new-EntryAbility.ets-6cb6772688a3032e3da35b425e18d98ba9971eb7777604aea10e726599977f77.msgpack",
|
||||
"CrashDiagnosticsDemo/entry/build/default/intermediates/loader_out/default/ets/sourceMaps.map",
|
||||
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/session/AuthSession.ts",
|
||||
"CrashDiagnosticsDemo/entry/build/default/cache/default/default@CompileArkTS/esmodule/debug/entry/src/main/ets/session/AuthSession.protoBin",
|
||||
"CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/startIcon.png",
|
||||
"CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/foreground.png",
|
||||
"CrashDiagnosticsDemo/entry/build/default/intermediates/res/default/resources/base/media/background.png",
|
||||
"HarmonyOS 启动性能监控接入方案_20260805.md",
|
||||
"CrashDiagnosticsDemo/CLAUDE.md",
|
||||
"CrashDiagnosticsDemo/entry/src/main/resources/base/media/startIcon.png",
|
||||
"CrashDiagnosticsDemo/entry/src/main/resources/base/media/foreground.png",
|
||||
"CrashDiagnosticsDemo/entry/src/main/resources/base/media/background.png",
|
||||
"CrashDiagnosticsDemo/AppScope/resources/base/media/foreground.png",
|
||||
"CrashDiagnosticsDemo/AppScope/resources/base/media/background.png",
|
||||
"report-assets/10-home-first-frame.png",
|
||||
"report-assets/09-profiler-first-frame-time-check.jpg",
|
||||
"AGENTS.md",
|
||||
"未命名.md",
|
||||
"HarmonyOS APM分析报告_20260731.md"
|
||||
]
|
||||
|
||||
@@ -40,6 +40,14 @@ Validation is manual. Before submitting changes:
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
History is currently minimal (`first commit`), so use concise, imperative commit subjects such as `docs: clarify OOM crash accounting` or `assets: add native crash evidence`. Keep report and supporting screenshots in the same commit.
|
||||
Use Conventional Commit-style subjects in the form `<type>: <summary>`, such as `docs: clarify OOM crash accounting` or `feat: 完善启动性能监控流程`. For non-trivial changes, add a blank line followed by `-` list items describing the concrete behavior, implementation, and verification. Keep the subject concise and do not repeat it in the body. Keep related code, documentation, and supporting assets in the same commit.
|
||||
|
||||
```text
|
||||
feat: 完善启动性能监控流程
|
||||
|
||||
- 保证每个进程只提交一次首帧完成事件
|
||||
- 按启动事件唯一键去重上报
|
||||
- 在真机验证登录与网络异常场景
|
||||
```
|
||||
|
||||
Pull requests should summarize the conclusion changed, identify the data period and source, list added or replaced assets, and call out any unresolved evidence gaps. Include rendered screenshots when formatting changes are substantial and link the relevant issue when one exists.
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Repository Overview
|
||||
|
||||
Mixed-purpose repository with three distinct areas:
|
||||
|
||||
1. **Root level** — Obsidian-friendly documentation workspace for HarmonyOS APM (Application Performance Management) analysis of 同花顺期货 (THS Futures). Two main reports:
|
||||
- `HarmonyOS APM分析报告_20260731.md` — crash analysis with supporting screenshots in `report-assets/`.
|
||||
- `HarmonyOS 启动性能监控接入方案_20260805.md` — cold-start performance monitoring integration plan.
|
||||
2. **`CrashDiagnosticsDemo/`** — Standalone HarmonyOS demo app demonstrating HiAppEvent crash subscription, persistent crash records, and native crash simulation via NAPI. Full project with its own `CLAUDE.md`.
|
||||
3. **`future_entry_demo/`** — Extracted `entry` HAP module from THS Futures. NOT a full workspace — depends on sibling modules (`biz_common`, `biz_trade`, etc.) via `file:../...`. Has its own `CLAUDE.md` and `AGENTS.md`. Contains `src/main/ets/monitor/LaunchMonitor.ets` (cold-start performance monitoring implementation referenced by the startup report).
|
||||
|
||||
**When working in a subdirectory**, read that subdirectory's own `CLAUDE.md` first — it contains build commands, architecture details, and constraints specific to that project. This file covers the repo-level conventions and cross-cutting concerns. The companion `AGENTS.md` has additional detail on PR guidelines and validation checklists.
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
There is no top-level build system. Work in the appropriate subdirectory:
|
||||
|
||||
- **CrashDiagnosticsDemo**: Full HarmonyOS project — `devecocli build` from its root.
|
||||
- **future_entry_demo**: Entry module only — build from the parent workspace with `devecocli build --modules entry@default`.
|
||||
- **Root documentation**: No build step. Edit Markdown in Obsidian or any CommonMark viewer.
|
||||
|
||||
The `.claude/settings.local.json` pre-approves `devecocli build *`, `devecocli device *`, and `devecocli run *` commands.
|
||||
|
||||
Useful repo-wide checks before committing documentation changes:
|
||||
|
||||
```sh
|
||||
git status --short
|
||||
rg 'report-assets/' '*.md'
|
||||
find report-assets -type f | sort
|
||||
```
|
||||
|
||||
## Cross-Cutting Conventions
|
||||
|
||||
### HarmonyOS / ArkTS
|
||||
|
||||
All HarmonyOS projects in this repo use the **Hvigor** build system with **DevEco Studio** as the IDE. Use the **deveco-cli skill** for build, run, and device operations. ArkTS is a strict subset of TypeScript — load the **arkts-grammar-standards** skill before writing `.ets` files.
|
||||
|
||||
### Documentation (Markdown)
|
||||
|
||||
- UTF-8 Markdown with ATX headings (`#`, `##`), fenced code blocks with language tags, pipe tables with right-aligned numeric columns.
|
||||
- Name screenshots with a two-digit sequence and short lowercase description (e.g., `08-grafana-native-detail.png`). Do not reuse an existing image filename for unrelated evidence.
|
||||
- New reports use descriptive filenames with a date (`YYYYMMDD`). Place images in `report-assets/` or a report-specific subdirectory.
|
||||
- Use backticks for event types, API names, versions, and identifiers.
|
||||
|
||||
### Before Committing Documentation
|
||||
|
||||
1. Preview all headings, tables, code blocks, and image captions in a Markdown viewer.
|
||||
2. Recalculate totals and confirm date ranges, versions, and event classifications against source screenshots.
|
||||
3. Verify all referenced asset files exist and no local absolute paths or sensitive data are exposed.
|
||||
4. Ensure stray images at the repo root are moved into `report-assets/` or deleted.
|
||||
|
||||
### Git
|
||||
|
||||
- Use Conventional Commits format: `<type>: <summary>` (e.g., `docs: clarify OOM crash accounting`, `feat: 完善启动性能监控流程`, `chore: save current workspace changes`).
|
||||
- For non-trivial changes, add a blank line followed by `-` list items describing the concrete change, implementation, and verification.
|
||||
- Keep report text and supporting screenshots in the same commit.
|
||||
- Avoid committing personal Obsidian workspace-state changes (`.obsidian/`) unless intentionally shared.
|
||||
@@ -0,0 +1,12 @@
|
||||
/node_modules
|
||||
/oh_modules
|
||||
/local.properties
|
||||
/.idea
|
||||
**/build
|
||||
/.hvigor
|
||||
.cxx
|
||||
/.clangd
|
||||
/.clang-format
|
||||
/.clang-tidy
|
||||
**/.test
|
||||
/.appanalyzer
|
||||
@@ -0,0 +1,81 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
HarmonyOS (鸿蒙) crash diagnostics demo app — `com.example.crashdiagnosticsdemo`. Targets HarmonyOS 6.1.0 (API 23), Stage Mode, phone devices only. Uses the Hvigor build system with DevEco Studio as the IDE.
|
||||
|
||||
## Build & Development Commands
|
||||
|
||||
This is a HarmonyOS project; use the **deveco-cli skill** for all build, run, and device operations.
|
||||
|
||||
- **Build (debug)**: `devecocli build` or via DevEco Studio
|
||||
- **Run on device/emulator**: DevEco Studio → Run, or `devecocli run`
|
||||
- **Lint ArkTS**: `code-linter` is configured via `code-linter.json5` (triggers on `.ets` files, enforces security rules like `@security/no-unsafe-aes`, `@security/no-unsafe-hash`, etc.)
|
||||
- **CLI docs lookup**: use the `deveco-cli` skill to fetch HarmonyOS API docs and samples
|
||||
|
||||
### Testing
|
||||
|
||||
The project includes `@ohos/hypium` (test framework) and `@ohos/hamock` (mocking) as dev dependencies. Test targets are configured in `entry/build-profile.json5` under the `ohosTest` target.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Entry Point & Page Routing
|
||||
|
||||
```
|
||||
EntryAbility (entryability/EntryAbility.ets)
|
||||
└── loads pages/Index (main page)
|
||||
└── navigates to pages/CrashHistory (push route)
|
||||
```
|
||||
|
||||
- `EntryAbility` initializes `CrashDiagnostics` on `onCreate`, sets color mode, and loads `pages/Index` on `onWindowStageCreate`.
|
||||
- Page routes are declared in `entry/src/main/resources/base/profile/main_pages.json`.
|
||||
|
||||
### Core Diagnostic Layer — `CrashDiagnostics` (diagnostics/CrashDiagnostics.ets)
|
||||
|
||||
A **static utility class** (never instantiated) that is the backbone of the app. Key responsibilities:
|
||||
|
||||
1. **HiAppEvent subscription**: Registers an `hiAppEvent.Watcher` named `debugCrashWatcher` that listens for system `APP_CRASH` fault events. The watcher is created in `initialize()` — because it reuses the same name, it picks up un-consumed crash events from a previous process exit on the next launch.
|
||||
|
||||
2. **Persistent crash records**: Stores up to 10 crash record strings in `preferences` (key: `recent_hiappevent_crash_records`). New records are prepended; duplicates are filtered out.
|
||||
|
||||
3. **Dismiss-until-new-crash pattern**: Tracks which crash the user dismissed (key: `dismissed_hiappevent_crash_id`). `shouldShowLastCrashDialog()` returns `true` only when the current latest crash differs from the dismissed one.
|
||||
|
||||
4. **Listener callback**: `recordsUpdatedListener` — a single callback set by the active UI page; fires when cached records change.
|
||||
|
||||
5. **Caught error logging**: `recordCaughtError()` logs to hilog only — it does NOT mix caught exceptions into the APP_CRASH history. The history exclusively contains system-generated crash events.
|
||||
|
||||
### Pages
|
||||
|
||||
- **`pages/Index`** — Main debug screen. Displays crash record count/status, buttons to simulate a caught error (logged only) and a native CPP crash (via NAPI, kills the app), and a button to view crash history. On `aboutToAppear`, sets itself as the `recordsUpdatedListener` and optionally shows an alert dialog for the last un-dismissed crash.
|
||||
|
||||
- **`pages/CrashHistory`** — Lists crash records from `CrashDiagnostics.getRecentRecords()`, with detail view, clear-all, and export-to-ZIP (JSON inside ZIP, saved via `DocumentViewPicker`).
|
||||
|
||||
### Native Layer (NAPI)
|
||||
|
||||
```
|
||||
entry/src/main/cpp/napi_init.cpp → libentry.so
|
||||
entry/src/main/cpp/types/libentry/index.d.ts → ArkTS type declarations
|
||||
```
|
||||
|
||||
Exports a single function `simulateCppCrash()` that calls `std::abort()`. Used by `pages/Index` to demonstrate a native crash that gets picked up by HiAppEvent on the next launch. Built via CMake (`CMakeLists.txt`) targeting `arm64-v8a` only, linked against `libace_napi.z.so`.
|
||||
|
||||
### Key Data Flow
|
||||
|
||||
```
|
||||
App crashes (native/JS)
|
||||
→ HarmonyOS writes APP_CRASH event to HiAppEvent
|
||||
→ Next app launch: EntryAbility.onCreate → CrashDiagnostics.initialize()
|
||||
→ hiAppEvent.addWatcher("debugCrashWatcher") picks up events
|
||||
→ collectFromHolder() formats and caches records to preferences
|
||||
→ recordsUpdatedListener fires → Index page updates UI
|
||||
→ Optionally shows AlertDialog for un-dismissed crash
|
||||
```
|
||||
|
||||
## Important Constraints
|
||||
|
||||
- **ArkTS is NOT TypeScript**: ArkTS is a strict subset of TypeScript with its own compiler rules. Use the `arkts-grammar-standards` skill before writing `.ets` files. Key differences: no `any`/`unknown` types in certain contexts, restricted structural typing for `@Component` structs, specific decorator rules (`@State`, `@Entry`, `@Component`).
|
||||
- **Target ABI is arm64-v8a only** (no x86_64 emulator support in the build config).
|
||||
- **Obfuscation is disabled** even in release mode (`entry/build-profile.json5`).
|
||||
- **Strict mode enabled**: `caseSensitiveCheck: true`, `useNormalizedOHMUrl: true`.
|
||||
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"app": {
|
||||
"signingConfigs": [
|
||||
{
|
||||
"name": "default",
|
||||
"type": "HarmonyOS",
|
||||
"material": {
|
||||
"certpath": "/Users/clz/.ohos/config/default_CrashDiagnosticsDemo_dgC3A3ef-okflXeLnrBZ6af9o2i9T_iaO4bxUMOFv2k=.cer",
|
||||
"keyAlias": "debugKey",
|
||||
"keyPassword": "0000001AE4AAFBA570B8D487B590836743677A433F6DD14A7A6D0B26155D832D115352AC4E27D625EDE1",
|
||||
"profile": "/Users/clz/.ohos/config/default_CrashDiagnosticsDemo_dgC3A3ef-okflXeLnrBZ6af9o2i9T_iaO4bxUMOFv2k=.p7b",
|
||||
"signAlg": "SHA256withECDSA",
|
||||
"storeFile": "/Users/clz/.ohos/config/default_CrashDiagnosticsDemo_dgC3A3ef-okflXeLnrBZ6af9o2i9T_iaO4bxUMOFv2k=.p12",
|
||||
"storePassword": "0000001A5B9B9B6E48CF67A085861EF0102D3E508E9C395FA0B293A616EFBD3790576635D0A3852EA471"
|
||||
}
|
||||
}
|
||||
],
|
||||
"products": [
|
||||
{
|
||||
"name": "default",
|
||||
"signingConfig": "default",
|
||||
"targetSdkVersion": "6.1.0(23)",
|
||||
"compatibleSdkVersion": "6.1.0(23)",
|
||||
"runtimeOS": "HarmonyOS",
|
||||
"buildOption": {
|
||||
"strictMode": {
|
||||
"caseSensitiveCheck": true,
|
||||
"useNormalizedOHMUrl": true
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"buildModeSet": [
|
||||
{
|
||||
"name": "debug",
|
||||
},
|
||||
{
|
||||
"name": "release"
|
||||
}
|
||||
]
|
||||
},
|
||||
"modules": [
|
||||
{
|
||||
"name": "entry",
|
||||
"srcPath": "./entry",
|
||||
"targets": [
|
||||
{
|
||||
"name": "default",
|
||||
"applyToProducts": [
|
||||
"default"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { inspector } from '@kit.ArkUI';
|
||||
import { hiAppEvent, hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { deviceInfo } from '@kit.BasicServicesKit';
|
||||
import { preferences } from '@kit.ArkData';
|
||||
|
||||
enum DrawReportState {
|
||||
IDLE,
|
||||
SUBMITTED,
|
||||
SUCCEEDED,
|
||||
FAILED
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动性能检测演示。
|
||||
*
|
||||
* 监听系统 APP_LAUNCH 事件,并确保一个进程生命周期内只提交一次
|
||||
* reportDrawnCompleted。检测结果同时输出到 hilog 和模拟页面。
|
||||
*
|
||||
* 与 future_entry_demo 的 LaunchMonitor.ets 对应:本文件为无依赖演示实现
|
||||
* (仅 hilog + 页面展示,不做 ELK 上传),追溯字段已同步系统时间拆解字段
|
||||
* 与 deviceInfo 环境字段;app_install_status 以 preferences 自实现(三值语义,
|
||||
* over_written 无法区分);git_commit / build_mode 依赖 biz_common 与
|
||||
* BuildProfile,演示工程不适用,故未同步。
|
||||
*/
|
||||
export class LaunchDiagnostics {
|
||||
private static readonly WATCHER_NAME: string = 'startupDiagnosticsWatcher';
|
||||
private static readonly LOG_DOMAIN: number = 0xD002;
|
||||
private static readonly MAX_EVENT_KEYS: number = 20;
|
||||
/**
|
||||
* 补报事件判定容差(ms)。
|
||||
* 本次启动链中 icon_input_time(点击图标)与 watcher 注册时间间隔在毫秒~秒级;
|
||||
* 系统补投递的上次启动遗留事件,其 icon_input_time 至少早于注册时间 5 秒(事件送达延迟),
|
||||
* 因此容差取 2s 可稳定区分两者。
|
||||
*/
|
||||
private static readonly BACKFILL_TOLERANCE_MS: number = 2000;
|
||||
/**
|
||||
* watcher 注册时间戳(epoch ms),用于识别系统补投递的上次启动遗留事件。
|
||||
*/
|
||||
private static watcherRegisterTime: number = 0;
|
||||
/**
|
||||
* 安装状态(维度):preferences 无启动记录视为 new_install,随后写入标记。
|
||||
* 与生产实现(app_install_status 三值)对齐;演示工程无法区分 over_written(覆盖安装保留数据时显示 normal)。
|
||||
*/
|
||||
private static appInstallStatus: string = 'normal';
|
||||
private static watcherInitialized: boolean = false;
|
||||
private static drawReportState: DrawReportState = DrawReportState.IDLE;
|
||||
private static capturedEntryPage: string = '未捕获';
|
||||
private static activeObserverOwner: string = '';
|
||||
private static activeDisposer: (() => void) | undefined = undefined;
|
||||
private static statusListenerOwner: string = '';
|
||||
private static statusUpdatedListener: (() => void) | undefined = undefined;
|
||||
private static processedEventKeys: Array<string> = [];
|
||||
private static drawCallbackCount: number = 0;
|
||||
private static reportSubmitCount: number = 0;
|
||||
private static skippedReportCount: number = 0;
|
||||
private static launchEventCount: number = 0;
|
||||
private static duplicateEventCount: number = 0;
|
||||
private static lastAction: string = '等待登录页首次绘制';
|
||||
private static lastLaunchDetail: string = '尚未收到 APP_LAUNCH';
|
||||
|
||||
static initialize(context: common.UIAbilityContext): void {
|
||||
if (LaunchDiagnostics.watcherInitialized) {
|
||||
return;
|
||||
}
|
||||
// 安装状态:preferences 无启动记录视为 new_install,随后写入并同步落盘;异常时按 normal 处理
|
||||
try {
|
||||
const pref: preferences.Preferences = preferences.getPreferencesSync(context, { name: 'startup_demo' });
|
||||
LaunchDiagnostics.appInstallStatus = pref.getSync('has_launched', false) ? 'normal' : 'new_install';
|
||||
pref.putSync('has_launched', true);
|
||||
pref.flushSync();
|
||||
} catch (e) {
|
||||
LaunchDiagnostics.appInstallStatus = 'normal';
|
||||
}
|
||||
|
||||
const filter: hiAppEvent.AppEventFilter = {
|
||||
domain: hiAppEvent.domain.OS,
|
||||
names: [hiAppEvent.event.APP_LAUNCH]
|
||||
};
|
||||
const watcher: hiAppEvent.Watcher = {
|
||||
name: LaunchDiagnostics.WATCHER_NAME,
|
||||
appEventFilters: [filter],
|
||||
onReceive: (domain: string, appEventGroups: Array<hiAppEvent.AppEventGroup>) => {
|
||||
hilog.info(LaunchDiagnostics.LOG_DOMAIN, 'LaunchDiagnostics',
|
||||
'APP_LAUNCH onReceive domain=%{public}s, groups=%{public}d', domain, appEventGroups.length);
|
||||
LaunchDiagnostics.processAppLaunchEvents(appEventGroups);
|
||||
}
|
||||
};
|
||||
|
||||
// 记录注册时间戳,用于区分本次启动事件与系统补投递的上次启动遗留事件
|
||||
LaunchDiagnostics.watcherRegisterTime = Date.now();
|
||||
|
||||
try {
|
||||
hiAppEvent.addWatcher(watcher);
|
||||
LaunchDiagnostics.watcherInitialized = true;
|
||||
LaunchDiagnostics.lastAction = 'APP_LAUNCH 监听已注册,等待首帧';
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
} catch (err) {
|
||||
LaunchDiagnostics.lastAction = `APP_LAUNCH 监听注册失败:${LaunchDiagnostics.getErrorMessage(err)}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
static reportFirstFrameOnDraw(
|
||||
createObserver: () => inspector.ComponentObserver,
|
||||
abilityContext: common.UIAbilityContext,
|
||||
pageName: string
|
||||
): void {
|
||||
if (LaunchDiagnostics.drawReportState !== DrawReportState.IDLE) {
|
||||
LaunchDiagnostics.skippedReportCount++;
|
||||
LaunchDiagnostics.lastAction = `页面 ${pageName} 的重复首帧注册已跳过`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
LaunchDiagnostics.disposeFirstFrame(LaunchDiagnostics.activeObserverOwner);
|
||||
|
||||
try {
|
||||
const observer: inspector.ComponentObserver = createObserver();
|
||||
const onDraw = (): void => {
|
||||
observer.off('draw', onDraw);
|
||||
if (LaunchDiagnostics.activeObserverOwner === pageName) {
|
||||
LaunchDiagnostics.activeObserverOwner = '';
|
||||
LaunchDiagnostics.activeDisposer = undefined;
|
||||
}
|
||||
|
||||
if (LaunchDiagnostics.drawReportState !== DrawReportState.IDLE) {
|
||||
LaunchDiagnostics.skippedReportCount++;
|
||||
LaunchDiagnostics.lastAction = `页面 ${pageName} 的重复 draw 回调已跳过`;
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
LaunchDiagnostics.drawCallbackCount++;
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.SUBMITTED;
|
||||
LaunchDiagnostics.capturedEntryPage = pageName;
|
||||
LaunchDiagnostics.reportSubmitCount++;
|
||||
LaunchDiagnostics.lastAction = `已提交 ${pageName} 的 reportDrawnCompleted`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
|
||||
try {
|
||||
abilityContext.reportDrawnCompleted((err) => {
|
||||
if (err && err.code !== 0) {
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.FAILED;
|
||||
LaunchDiagnostics.lastAction =
|
||||
`reportDrawnCompleted 失败:code=${err.code}, message=${err.message}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.SUCCEEDED;
|
||||
LaunchDiagnostics.lastAction = `reportDrawnCompleted 成功:${pageName}`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
});
|
||||
} catch (err) {
|
||||
// 同步异常表示异步任务未成功提交,恢复 IDLE 后允许其他页面重试。
|
||||
LaunchDiagnostics.drawReportState = DrawReportState.IDLE;
|
||||
LaunchDiagnostics.lastAction =
|
||||
`reportDrawnCompleted 同步异常:${LaunchDiagnostics.getErrorMessage(err)}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
};
|
||||
|
||||
LaunchDiagnostics.activeObserverOwner = pageName;
|
||||
LaunchDiagnostics.activeDisposer = (): void => {
|
||||
observer.off('draw', onDraw);
|
||||
};
|
||||
observer.on('draw', onDraw);
|
||||
LaunchDiagnostics.lastAction = `已监听 ${pageName} 根组件 draw`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
} catch (err) {
|
||||
LaunchDiagnostics.lastAction = `创建 draw 监听失败:${LaunchDiagnostics.getErrorMessage(err)}`;
|
||||
LaunchDiagnostics.logError(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
}
|
||||
|
||||
static disposeFirstFrame(pageName: string): void {
|
||||
if (pageName === '' || LaunchDiagnostics.activeObserverOwner !== pageName) {
|
||||
return;
|
||||
}
|
||||
const disposer: (() => void) | undefined = LaunchDiagnostics.activeDisposer;
|
||||
if (disposer !== undefined) {
|
||||
disposer();
|
||||
}
|
||||
LaunchDiagnostics.activeObserverOwner = '';
|
||||
LaunchDiagnostics.activeDisposer = undefined;
|
||||
}
|
||||
|
||||
static setStatusUpdatedListener(owner: string, listener: () => void): void {
|
||||
LaunchDiagnostics.statusListenerOwner = owner;
|
||||
LaunchDiagnostics.statusUpdatedListener = listener;
|
||||
listener();
|
||||
}
|
||||
|
||||
static clearStatusUpdatedListener(owner: string): void {
|
||||
if (LaunchDiagnostics.statusListenerOwner !== owner) {
|
||||
return;
|
||||
}
|
||||
LaunchDiagnostics.statusListenerOwner = '';
|
||||
LaunchDiagnostics.statusUpdatedListener = undefined;
|
||||
}
|
||||
|
||||
static getStatusText(): string {
|
||||
return `状态:${LaunchDiagnostics.getStateName()}\n` +
|
||||
`捕获入口:${LaunchDiagnostics.capturedEntryPage}\n` +
|
||||
`draw 回调:${LaunchDiagnostics.drawCallbackCount}\n` +
|
||||
`系统提交:${LaunchDiagnostics.reportSubmitCount}\n` +
|
||||
`重复拦截:${LaunchDiagnostics.skippedReportCount}\n` +
|
||||
`APP_LAUNCH:${LaunchDiagnostics.launchEventCount}\n` +
|
||||
`事件去重:${LaunchDiagnostics.duplicateEventCount}\n` +
|
||||
`最近动作:${LaunchDiagnostics.lastAction}\n` +
|
||||
`事件详情:${LaunchDiagnostics.lastLaunchDetail}`;
|
||||
}
|
||||
|
||||
private static processAppLaunchEvents(appEventGroups: Array<hiAppEvent.AppEventGroup>): void {
|
||||
for (let groupIndex: number = 0; groupIndex < appEventGroups.length; groupIndex++) {
|
||||
const group: hiAppEvent.AppEventGroup = appEventGroups[groupIndex];
|
||||
for (let infoIndex: number = 0; infoIndex < group.appEventInfos.length; infoIndex++) {
|
||||
LaunchDiagnostics.processAppLaunchEvent(group.appEventInfos[infoIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static processAppLaunchEvent(eventInfo: hiAppEvent.AppEventInfo): void {
|
||||
const paramsJson: string = JSON.stringify(eventInfo.params);
|
||||
const params: Record<string, Object> = JSON.parse(paramsJson) as Record<string, Object>;
|
||||
const startType: number = (params['start_type'] as number) ?? -1;
|
||||
if (startType !== 0) {
|
||||
LaunchDiagnostics.logInfo(`跳过非冷启动事件:start_type=${startType}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const extendTime: number = (params['extend_time'] as number) ?? 0;
|
||||
if (extendTime <= 0) {
|
||||
LaunchDiagnostics.logInfo('跳过尚未填充 extend_time 的 APP_LAUNCH');
|
||||
return;
|
||||
}
|
||||
|
||||
const eventTime: number = (params['time'] as number) ?? 0;
|
||||
const iconInputTime: number = (params['icon_input_time'] as number) ?? 0;
|
||||
const processName: string = (params['process_name'] as string) ?? '';
|
||||
const eventKey: string = `${eventTime}|${iconInputTime}|${startType}|${processName}`;
|
||||
if (LaunchDiagnostics.processedEventKeys.indexOf(eventKey) !== -1) {
|
||||
LaunchDiagnostics.duplicateEventCount++;
|
||||
LaunchDiagnostics.lastAction = `重复 APP_LAUNCH 已拦截:${eventKey}`;
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastAction);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
return;
|
||||
}
|
||||
|
||||
LaunchDiagnostics.processedEventKeys.push(eventKey);
|
||||
if (LaunchDiagnostics.processedEventKeys.length > LaunchDiagnostics.MAX_EVENT_KEYS) {
|
||||
LaunchDiagnostics.processedEventKeys.shift();
|
||||
}
|
||||
|
||||
const bundleName: string = (params['bundle_name'] as string) ?? '';
|
||||
const bundleVersion: string = (params['bundle_version'] as string) ?? '';
|
||||
const animationFinishTime: number = (params['animation_finish_time'] as number) ?? 0;
|
||||
// 时间拆解字段(与接入方案文档第 4 节一致):
|
||||
// response_latency 需 API 22+;startability_processstart_dur / appattach_to_appforeground_dur 仅冷启动存在
|
||||
const responseLatency: number = (params['response_latency'] as number) ?? 0;
|
||||
const startabilityProcessStartDur: number = (params['startability_processstart_dur'] as number) ?? 0;
|
||||
const appattachToAppForegroundDur: number = (params['appattach_to_appforeground_dur'] as number) ?? 0;
|
||||
|
||||
// 补投递事件(上次启动提交绘制但进程在事件送达前退出)的 capturedEntryPage 是当前进程的快照,
|
||||
// 不能错误归因到本次启动的页面,因此补报事件的入口标记为未知。
|
||||
const isBackfill: boolean = LaunchDiagnostics.isBackfillEvent(iconInputTime);
|
||||
|
||||
LaunchDiagnostics.launchEventCount++;
|
||||
LaunchDiagnostics.lastLaunchDetail =
|
||||
`extend_time=${extendTime}ms, icon_input_time=${iconInputTime}, ` +
|
||||
`start_type=${startType}, process_name=${processName}\n` +
|
||||
`bundle_name=${bundleName}, bundle_version=${bundleVersion}\n` +
|
||||
`response_latency=${responseLatency}ms, animation_finish_time=${animationFinishTime}ms\n` +
|
||||
`startability_processstart_dur=${startabilityProcessStartDur}ms, ` +
|
||||
`appattach_to_appforeground_dur=${appattachToAppForegroundDur}ms\n` +
|
||||
`device_type=${LaunchDiagnostics.getDeviceType()}, ` +
|
||||
`system_version=${LaunchDiagnostics.getSystemVersion()}\n` +
|
||||
`app_install_status=${LaunchDiagnostics.appInstallStatus}\n` +
|
||||
`补报事件=${isBackfill ? '是(entry_page 不归因,按 unknown 处理)' : '否'}`;
|
||||
LaunchDiagnostics.lastAction = '已捕获有效冷启动 APP_LAUNCH';
|
||||
LaunchDiagnostics.logInfo(LaunchDiagnostics.lastLaunchDetail);
|
||||
LaunchDiagnostics.notifyStatusChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断事件是否为系统补投递的上次启动遗留事件。
|
||||
* 本次启动链中 icon_input_time 与 watcher 注册时间间隔在容差内;
|
||||
* 补报事件的 icon_input_time 早于注册时间超过容差(至少 5 秒的送达延迟)。
|
||||
*/
|
||||
private static isBackfillEvent(iconInputTime: number): boolean {
|
||||
return iconInputTime < LaunchDiagnostics.watcherRegisterTime - LaunchDiagnostics.BACKFILL_TOLERANCE_MS;
|
||||
}
|
||||
|
||||
private static getDeviceType(): string {
|
||||
return deviceInfo.deviceType;
|
||||
}
|
||||
|
||||
private static getSystemVersion(): string {
|
||||
return deviceInfo.osFullName;
|
||||
}
|
||||
|
||||
private static getStateName(): string {
|
||||
switch (LaunchDiagnostics.drawReportState) {
|
||||
case DrawReportState.IDLE:
|
||||
return 'IDLE';
|
||||
case DrawReportState.SUBMITTED:
|
||||
return 'SUBMITTED';
|
||||
case DrawReportState.SUCCEEDED:
|
||||
return 'SUCCEEDED';
|
||||
case DrawReportState.FAILED:
|
||||
return 'FAILED';
|
||||
default:
|
||||
return 'UNKNOWN';
|
||||
}
|
||||
}
|
||||
|
||||
private static notifyStatusChanged(): void {
|
||||
const listener: (() => void) | undefined = LaunchDiagnostics.statusUpdatedListener;
|
||||
if (listener !== undefined) {
|
||||
listener();
|
||||
}
|
||||
}
|
||||
|
||||
private static getErrorMessage(err: Object): string {
|
||||
return err instanceof Error ? err.message : JSON.stringify(err);
|
||||
}
|
||||
|
||||
private static logInfo(message: string): void {
|
||||
hilog.info(LaunchDiagnostics.LOG_DOMAIN, 'LaunchDiagnostics', '%{public}s', message);
|
||||
}
|
||||
|
||||
private static logError(message: string): void {
|
||||
hilog.error(LaunchDiagnostics.LOG_DOMAIN, 'LaunchDiagnostics', '%{public}s', message);
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,8 @@ import { AbilityConstant, common, ConfigurationConstant, UIAbility, Want } from
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { window } from '@kit.ArkUI';
|
||||
import { CrashDiagnostics } from '../diagnostics/CrashDiagnostics';
|
||||
import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
import { AuthSession } from '../session/AuthSession';
|
||||
|
||||
const DOMAIN = 0x0000;
|
||||
|
||||
@@ -24,6 +26,8 @@ export default class EntryAbility extends UIAbility {
|
||||
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
|
||||
try {
|
||||
CrashDiagnostics.initialize(this.context as common.UIAbilityContext);
|
||||
LaunchDiagnostics.initialize(this.context as common.UIAbilityContext);
|
||||
AuthSession.initialize(this.context as common.UIAbilityContext);
|
||||
this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);
|
||||
} catch (err) {
|
||||
if (err instanceof Error) {
|
||||
@@ -42,7 +46,8 @@ export default class EntryAbility extends UIAbility {
|
||||
// Main window is created, set main page for this ability
|
||||
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onWindowStageCreate');
|
||||
|
||||
windowStage.loadContent('pages/Index', (err) => {
|
||||
const initialPage: string = AuthSession.isLoggedIn() ? 'pages/Index' : 'pages/LaunchLogin';
|
||||
windowStage.loadContent(initialPage, (err) => {
|
||||
if (err.code) {
|
||||
hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
|
||||
return;
|
||||
|
||||
@@ -3,10 +3,14 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { CrashDiagnostics } from '../diagnostics/CrashDiagnostics';
|
||||
import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
import { AuthSession } from '../session/AuthSession';
|
||||
import crashNative from 'libentry.so';
|
||||
|
||||
const ENABLE_DEBUG_CRASH_DIAGNOSTICS: boolean = true;
|
||||
const PAGE_NAME: string = 'Index';
|
||||
let startupCrashDialogShown: boolean = false;
|
||||
|
||||
@Entry
|
||||
@@ -16,6 +20,11 @@ struct Index {
|
||||
@State recordCount: number = 0;
|
||||
|
||||
aboutToAppear(): void {
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_HOME_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
);
|
||||
CrashDiagnostics.setRecordsUpdatedListener(() => {
|
||||
this.updateRecordStatus();
|
||||
this.showLastCrashIfNeeded();
|
||||
@@ -25,9 +34,25 @@ struct Index {
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
CrashDiagnostics.clearRecordsUpdatedListener();
|
||||
}
|
||||
|
||||
private logout(): void {
|
||||
if (!AuthSession.setLoggedIn(false)) {
|
||||
this.statusText = '退出登录失败:无法保存登录状态';
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.getUIContext().getRouter().replaceUrl({ url: 'pages/LaunchLogin' }).catch((err: Error) => {
|
||||
this.statusText = `退出登录后跳转失败:${err.message}`;
|
||||
});
|
||||
} catch (err) {
|
||||
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
|
||||
this.statusText = `退出登录同步异常:${message}`;
|
||||
}
|
||||
}
|
||||
|
||||
private updateRecordStatus(): void {
|
||||
this.recordCount = CrashDiagnostics.getRecentRecords().length;
|
||||
this.statusText = this.recordCount === 0 ?
|
||||
@@ -123,8 +148,19 @@ struct Index {
|
||||
.onClick(() => {
|
||||
this.getUIContext().getRouter().pushUrl({ url: 'pages/CrashHistory' });
|
||||
})
|
||||
|
||||
Button('退出登录')
|
||||
.width('100%')
|
||||
.height(44)
|
||||
.fontSize(16)
|
||||
.borderRadius(8)
|
||||
.backgroundColor('#666666')
|
||||
.onClick(() => {
|
||||
this.logout();
|
||||
})
|
||||
}
|
||||
}
|
||||
.id('STARTUP_HOME_ROOT')
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
import { AuthSession } from '../session/AuthSession';
|
||||
|
||||
const PAGE_NAME: string = 'LaunchLogin';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct LaunchLogin {
|
||||
@State account: string = 'demo@example.com';
|
||||
@State password: string = '123456';
|
||||
@State startupStatus: string = LaunchDiagnostics.getStatusText();
|
||||
|
||||
aboutToAppear(): void {
|
||||
LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = LaunchDiagnostics.getStatusText();
|
||||
});
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_LOGIN_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
);
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
}
|
||||
|
||||
private openNetworkScenario(): void {
|
||||
this.navigateTo('pages/LaunchNetwork');
|
||||
}
|
||||
|
||||
private openPrivacyScenario(): void {
|
||||
this.navigateTo('pages/LaunchPrivacy');
|
||||
}
|
||||
|
||||
private completeLogin(): void {
|
||||
if (!AuthSession.setLoggedIn(true)) {
|
||||
this.startupStatus = `保存登录状态失败,请重试。\n${LaunchDiagnostics.getStatusText()}`;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this.getUIContext().getRouter().replaceUrl({ url: 'pages/Index' }).catch((err: Error) => {
|
||||
this.startupStatus = `进入首页失败:${err.message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
});
|
||||
} catch (err) {
|
||||
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
|
||||
this.startupStatus = `进入首页同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
}
|
||||
}
|
||||
|
||||
private navigateTo(url: string): void {
|
||||
try {
|
||||
this.getUIContext().getRouter().pushUrl({ url: url }).catch((err: Error) => {
|
||||
this.startupStatus = `页面跳转失败:${err.message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
});
|
||||
} catch (err) {
|
||||
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
|
||||
this.startupStatus = `页面跳转同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Scroll() {
|
||||
Column({ space: 16 }) {
|
||||
Text('账号登录')
|
||||
.fontSize(28)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
|
||||
Text('首次绘制会触发启动性能检测;进入其他场景后会再次尝试注册,用于验证单次保护。')
|
||||
.fontSize(14)
|
||||
.fontColor('#666666')
|
||||
|
||||
TextInput({ placeholder: '账号', text: this.account })
|
||||
.width('100%')
|
||||
.height(48)
|
||||
.onChange((value: string) => {
|
||||
this.account = value;
|
||||
})
|
||||
|
||||
TextInput({ placeholder: '密码', text: this.password })
|
||||
.type(InputType.Password)
|
||||
.width('100%')
|
||||
.height(48)
|
||||
.onChange((value: string) => {
|
||||
this.password = value;
|
||||
})
|
||||
|
||||
Button('模拟登录成功')
|
||||
.width('100%')
|
||||
.height(46)
|
||||
.onClick(() => {
|
||||
this.completeLogin();
|
||||
})
|
||||
|
||||
Row({ space: 12 }) {
|
||||
Button('模拟网络异常')
|
||||
.layoutWeight(1)
|
||||
.onClick(() => {
|
||||
this.openNetworkScenario();
|
||||
})
|
||||
|
||||
Button('模拟隐私确认')
|
||||
.layoutWeight(1)
|
||||
.onClick(() => {
|
||||
this.openPrivacyScenario();
|
||||
})
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Text('启动检测结果')
|
||||
.fontSize(18)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
.width('100%')
|
||||
|
||||
Text(this.startupStatus)
|
||||
.fontSize(13)
|
||||
.fontColor('#335F8A')
|
||||
.width('100%')
|
||||
.padding(12)
|
||||
.backgroundColor('#EEF6FF')
|
||||
.borderRadius(8)
|
||||
}
|
||||
.width('100%')
|
||||
.padding(24)
|
||||
}
|
||||
.id('STARTUP_LOGIN_ROOT')
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
|
||||
const PAGE_NAME: string = 'LaunchNetwork';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct LaunchNetwork {
|
||||
@State startupStatus: string = LaunchDiagnostics.getStatusText();
|
||||
|
||||
aboutToAppear(): void {
|
||||
LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = LaunchDiagnostics.getStatusText();
|
||||
});
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_NETWORK_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
);
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
}
|
||||
|
||||
private retryNetwork(): void {
|
||||
try {
|
||||
this.getUIContext().getRouter().replaceUrl({ url: 'pages/LaunchLogin' }).catch((err: Error) => {
|
||||
this.startupStatus = `页面跳转失败:${err.message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
});
|
||||
} catch (err) {
|
||||
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
|
||||
this.startupStatus = `页面跳转同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Column({ space: 18 }) {
|
||||
Text('网络连接异常')
|
||||
.fontSize(26)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
|
||||
Text('这是启动过程中的网络异常模拟页。点击重试后进入登录页,后续页面不应再次提交绘制完成事件。')
|
||||
.fontSize(15)
|
||||
.fontColor('#666666')
|
||||
.textAlign(TextAlign.Center)
|
||||
|
||||
Button('网络已恢复,重试')
|
||||
.width('100%')
|
||||
.height(46)
|
||||
.onClick(() => {
|
||||
this.retryNetwork();
|
||||
})
|
||||
|
||||
Text(this.startupStatus)
|
||||
.fontSize(13)
|
||||
.fontColor('#8A4B08')
|
||||
.width('100%')
|
||||
.padding(12)
|
||||
.backgroundColor('#FFF7E8')
|
||||
.borderRadius(8)
|
||||
}
|
||||
.id('STARTUP_NETWORK_ROOT')
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding(24)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { LaunchDiagnostics } from '../diagnostics/LaunchDiagnostics';
|
||||
|
||||
const PAGE_NAME: string = 'LaunchPrivacy';
|
||||
|
||||
@Entry
|
||||
@Component
|
||||
struct LaunchPrivacy {
|
||||
@State startupStatus: string = LaunchDiagnostics.getStatusText();
|
||||
|
||||
aboutToAppear(): void {
|
||||
LaunchDiagnostics.setStatusUpdatedListener(PAGE_NAME, () => {
|
||||
this.startupStatus = LaunchDiagnostics.getStatusText();
|
||||
});
|
||||
LaunchDiagnostics.reportFirstFrameOnDraw(
|
||||
() => this.getUIContext().getUIInspector().createComponentObserver('STARTUP_PRIVACY_ROOT'),
|
||||
this.getUIContext().getHostContext() as common.UIAbilityContext,
|
||||
PAGE_NAME
|
||||
);
|
||||
}
|
||||
|
||||
aboutToDisappear(): void {
|
||||
LaunchDiagnostics.disposeFirstFrame(PAGE_NAME);
|
||||
LaunchDiagnostics.clearStatusUpdatedListener(PAGE_NAME);
|
||||
}
|
||||
|
||||
private acceptPrivacy(): void {
|
||||
this.navigateTo('pages/LaunchLogin');
|
||||
}
|
||||
|
||||
private showNetworkError(): void {
|
||||
this.navigateTo('pages/LaunchNetwork');
|
||||
}
|
||||
|
||||
private navigateTo(url: string): void {
|
||||
try {
|
||||
this.getUIContext().getRouter().replaceUrl({ url: url }).catch((err: Error) => {
|
||||
this.startupStatus = `页面跳转失败:${err.message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
});
|
||||
} catch (err) {
|
||||
const message: string = err instanceof Error ? err.message : JSON.stringify(err);
|
||||
this.startupStatus = `页面跳转同步异常:${message}\n${LaunchDiagnostics.getStatusText()}`;
|
||||
}
|
||||
}
|
||||
|
||||
build() {
|
||||
Column({ space: 18 }) {
|
||||
Text('隐私政策确认')
|
||||
.fontSize(26)
|
||||
.fontWeight(FontWeight.Bold)
|
||||
|
||||
Text('这是启动过程中的隐私确认模拟页。页面跳转后,捕获入口和绘制完成时间不应被后续登录页覆盖。')
|
||||
.fontSize(15)
|
||||
.fontColor('#666666')
|
||||
|
||||
Button('同意并进入登录页')
|
||||
.width('100%')
|
||||
.height(46)
|
||||
.onClick(() => {
|
||||
this.acceptPrivacy();
|
||||
})
|
||||
|
||||
Button('模拟网络不可用')
|
||||
.width('100%')
|
||||
.height(46)
|
||||
.onClick(() => {
|
||||
this.showNetworkError();
|
||||
})
|
||||
|
||||
Text(this.startupStatus)
|
||||
.fontSize(13)
|
||||
.fontColor('#3F5D3F')
|
||||
.width('100%')
|
||||
.padding(12)
|
||||
.backgroundColor('#EFF8EF')
|
||||
.borderRadius(8)
|
||||
}
|
||||
.id('STARTUP_PRIVACY_ROOT')
|
||||
.width('100%')
|
||||
.height('100%')
|
||||
.justifyContent(FlexAlign.Center)
|
||||
.padding(24)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright (c) 2026 Huawei Device Co., Ltd.
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
*/
|
||||
|
||||
import { common } from '@kit.AbilityKit';
|
||||
import { preferences } from '@kit.ArkData';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
|
||||
export class AuthSession {
|
||||
private static readonly STORE_NAME: string = 'auth_session';
|
||||
private static readonly LOGGED_IN_KEY: string = 'is_logged_in';
|
||||
private static readonly LOG_DOMAIN: number = 0xD003;
|
||||
private static store: preferences.Preferences | undefined = undefined;
|
||||
|
||||
static initialize(context: common.UIAbilityContext): void {
|
||||
try {
|
||||
const options: preferences.Options = { name: AuthSession.STORE_NAME };
|
||||
AuthSession.store = preferences.getPreferencesSync(context, options);
|
||||
} catch (err) {
|
||||
AuthSession.logFailure('初始化登录状态失败。', err);
|
||||
}
|
||||
}
|
||||
|
||||
static isLoggedIn(): boolean {
|
||||
const store: preferences.Preferences | undefined = AuthSession.store;
|
||||
if (store === undefined) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return store.getSync(AuthSession.LOGGED_IN_KEY, false) as boolean;
|
||||
} catch (err) {
|
||||
AuthSession.logFailure('读取登录状态失败。', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static setLoggedIn(loggedIn: boolean): boolean {
|
||||
const store: preferences.Preferences | undefined = AuthSession.store;
|
||||
if (store === undefined) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
store.putSync(AuthSession.LOGGED_IN_KEY, loggedIn);
|
||||
store.flushSync();
|
||||
return true;
|
||||
} catch (err) {
|
||||
AuthSession.logFailure('保存登录状态失败。', err);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static logFailure(message: string, err: Object): void {
|
||||
const detail: string = err instanceof Error ? err.message : 'Unknown error';
|
||||
hilog.error(AuthSession.LOG_DOMAIN, 'AuthSession', '%{public}s %{public}s', message, detail);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"src": [
|
||||
"pages/LaunchLogin",
|
||||
"pages/LaunchNetwork",
|
||||
"pages/LaunchPrivacy",
|
||||
"pages/Index",
|
||||
"pages/CrashHistory"
|
||||
]
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
# HarmonyOS 启动性能监控接入方案
|
||||
|
||||
## 1. 目标
|
||||
|
||||
建立冷启动耗时的线上采集、聚合和看板能力,用于发现版本或分支引入的启动性能劣化。统计口径以“用户点击图标”至“首页首帧完成”为准,单位为毫秒。
|
||||
|
||||
## 2. 总体链路
|
||||
|
||||
`APP_LAUNCH` 系统事件提供启动类型和系统耗时;首个有效入口页完成首次绘制后,通过 `reportDrawnCompleted` 标记终点。当前客户端双通道上报:`ElkService` 异步上传结构化追溯明细(11 字段),`HXEventMonitor` 分桶指标记录原始耗时由 SDK 按桶聚合(详见 6.2),后台再按模块配置阈值、聚合和看板。若后续具备 HarmonyOS 自定义 APM SDK,可替换上传层,但保留事件采集和去重逻辑。
|
||||
|
||||
## 3. 客户端接入
|
||||
|
||||
实现位于 `future_entry_demo/src/main/ets/monitor/LaunchMonitor.ets`。`EntryAbility.onCreate` 必须尽早调用 `LaunchMonitor.init()`,先注册 watcher,再允许入口页提交首帧完成事件。`init()` 本身应保持幂等,避免同一进程重复注册 watcher。
|
||||
|
||||
监听 `APP_LAUNCH` 后读取:
|
||||
|
||||
- `start_type`:启动类型;仅 `0` 纳入冷启动统计。
|
||||
- `extend_time`:从系统记录的 `icon_input_time` 到应用报告绘制完成的耗时。
|
||||
- `time`、`icon_input_time`、`process_name`:标识启动事件并用于去重。
|
||||
- `animation_finish_time`、`bundle_version`:用于分析和问题追溯。
|
||||
|
||||
### 3.1 首帧单次提交
|
||||
|
||||
`LauncherPage`、`NetworkAnomalyPage` 和 `Index` 都应尝试监听根节点首次 `draw`,以最先有效绘制的页面作为 `entry_page`。必须遵循以下约束:
|
||||
|
||||
1. 在调用 `reportDrawnCompleted` **之前**设置进程级已提交状态;异步回调尚未执行时,其他页面也不得再次提交。
|
||||
2. observer 触发后立即注销,页面 `aboutToDisappear` 仅释放与自身 `pageName` 匹配的 observer,避免旧页面销毁新页面的监听。
|
||||
3. 仅同步调用异常表示任务未提交,此时恢复未提交状态,允许后续入口页重试;页面销毁不得重置已经提交的状态。
|
||||
|
||||
### 3.2 事件过滤与去重
|
||||
|
||||
仅处理 `start_type === 0` 且 `extend_time > 0` 的记录。上传前按以下键去重:
|
||||
|
||||
```text
|
||||
time + icon_input_time + start_type + process_name
|
||||
```
|
||||
|
||||
客户端保留最近 20 个事件键,兼容同一回调组内重复记录和系统重新投递,同时避免缓存无限增长。
|
||||
|
||||
## 4. 指标定义
|
||||
|
||||
| 项目 | 建议值 |
|
||||
| ---- | -------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| 模块 | `launch` |
|
||||
| 指标名 | `cold_start_time` |
|
||||
| 指标值 | 冷启动耗时,单位 ms |
|
||||
| 维度 | `start_type`、`entry_page`、`app_version`、`app_install_status` |
|
||||
| 追溯字段 | `response_latency`、`animation_finish_time`、`startability_processstart_dur`、`appattach_to_appforeground_dur`、`git_commit`、`is_backfill` |
|
||||
| 分桶 | `[500, 600, 700, 800, 1000, 1500, 2000, 3000]` |
|
||||
|
||||
上传负载为最小必要集:指标 1、维度 4(含 `app_install_status`)、启动生命周期各阶段时间 4(劣化分段定位)、`git_commit`(代码级追溯)、`is_backfill`(补报标识);去重为客户端行为(第 3.2 节),负载不含去重键。其余候选字段按需加回,无需改动采集逻辑。
|
||||
|
||||
模块名小写+连字符,指标名不含连字符;维度避免高基数(用户 ID、完整 URL 等)。
|
||||
|
||||
分桶指标由 `HXEventMonitor` 实现(8 桶、4 维度,与追溯一致),`record(extendTime)` 由 SDK 归桶聚合;桶边界按实测主体(600-800ms)细分、尾部留告警锚点,分桶变更需后台重建并档(历史分布不可比)。维度 `app_install_status` 取值 `normal`/`new_install`/`over_written`(安装状态:常规/全新安装/覆盖安装),与 `StartManager` 安装状态对齐。
|
||||
|
||||
### 4.1 启动时间拆解
|
||||
|
||||
`cold_start_time` 之外,追溯通道已随上传携带各分段耗时(见上表),可拆分冷启动定位劣化归属:
|
||||
|
||||
```text
|
||||
离手 ──response_latency──► 动效开始 ──(animation_finish_time − response_latency)──► 动效完成 ──(extend_time − animation_finish_time)──► 首帧绘制完成
|
||||
系统响应段 系统动效段 应用可优化段
|
||||
```
|
||||
|
||||
- `response_latency`:离手到动效开始的耗时(需 API 22+),反映系统响应快慢。
|
||||
- `startability_processstart_dur`:系统启动 Ability 到进程创建完成(仅冷启动),反映系统侧进程启动。
|
||||
- `appattach_to_appforeground_dur`:进程初始化完成到应用切前台(仅冷启动),反映系统侧应用挂载。
|
||||
- `extend_time − animation_finish_time`:动效完成到应用首帧的耗时,即**应用侧可优化的独占部分**;看板可据此判断劣化发生在系统侧还是应用侧。注意 `animation_finish_time` 在部分机型为 `0`(系统未填充),此时该段等于 `extend_time` 全量,聚合时需兼容。
|
||||
|
||||
未上传的候选字段(按需启用):环境 `build_mode`/`target_name` 区分 debug/forTest/Official 环境,避免测试包污染线上看板;`device_type`/`system_version` 排除设备差异;版本 `bundle_version`/`bundle_name`;去重键 `time`/`icon_input_time`/`process_name`(去重为客户端行为,见第 3.2 节)。`git_commit` 已上传(代码级追溯)。
|
||||
|
||||
## 5. 后台与看板配置
|
||||
|
||||
### 5.1 模块策略下发(链路 B 激活前提)
|
||||
|
||||
链路 B 是**策略驱动**的:SDK 侧 `HXEventMonitorPlugin.parseMonitorStrategy()` 读取后台策略注入 `EventStrategyBean`,若 `launch` 模块无开启策略,`create()/record()` 一律空转并打印 `Monitor.Event cold_start_time eventId has not created!`,应用侧无法绕开。因此必须在 APM 后台为 **module=`launch`** 下发如下策略:
|
||||
|
||||
```json
|
||||
{
|
||||
"module": "launch",
|
||||
"config": {
|
||||
"status": 1,
|
||||
"sampling_rate": 1.0,
|
||||
"interval": 5000,
|
||||
"aggre_time": 60000,
|
||||
"aggre_count": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明(对齐 `EventStrategyBean.fromJson`,仅这些字段生效):
|
||||
|
||||
| 字段 | 取值 | 说明 |
|
||||
|------|------|------|
|
||||
| `module` | `launch` | 必须与代码 `HXEventMonitor.getEventFactory('launch')` 一致 |
|
||||
| `status` | `1` | **必须 `1`(STATUS_OPEN)**;否则 `isOpen()` 为 false,`create/record` 全空转 |
|
||||
| `sampling_rate` | `0~1` | 抽样率,`isOpen()` 需 `status==1` 且抽样通过才生效 |
|
||||
| `interval` | ms | 采集间隔 |
|
||||
| `aggre_time` | ms | 聚合时间阈值,达到即触发一次 push |
|
||||
| `aggre_count` | 条 | 聚合数量阈值,达到即触发一次 push |
|
||||
|
||||
客户端 `record` 后由 SDK 本地聚合,达到聚合周期或条数任一触发条件后自动上报至 APM 平台(发送通道为 `HXMonitor` 注入的 `IElkService`,即 `APMElkService` 所走的 `ElkService` 通道)。
|
||||
|
||||
**判定生效**:后台下发后重启应用,冷启动日志中应消失 `Monitor.Event cold_start_time eventId has not created!`、出现 `Cold start bucket metric recorded: <ms>` 与 SDK 聚合 push 日志,看板出现 `launch` 模块 `cold_start_time` 桶分布。
|
||||
|
||||
### 5.2 策略门控机制(源码级验证,2026-08-07,`@kernel/app_monitor_event`/`app_monitor_lib` 源码)
|
||||
|
||||
策略门控发生在 **SDK 指标层**,插件无法绕过:
|
||||
|
||||
- **注入链**:后台策略 JSON → `HXEventMonitorPlugin.parseMonitorStrategy()` → `EventStrategyBean.fromJson()` → `EventMonitorStrategy.addStrategy(module, bean)`(全局 `configMap`)→ `DataMonitorModule` 构造时 `getStrategy('launch')` 取策略。
|
||||
- **两级门控**:插件级(`AbstractHXBasePlugin.isSupport()`,默认"有策略且开启",`HXLaunchMonitorPlugin` override 为 `true` 强制通过)仅控制插件生命周期;**指标级**(`DataMonitorModule.create/getEventMonitor` 检查 `_strategy?.isOpen()`)独立生效——`launch` 无策略 bean 时 `_strategy=null`,`create`/`getEventMonitor` 返回 `NullDataMonitorImp`,`record` 空转打印 `eventId has not created!`。**插件强制开启绕不过指标级门控,必须后台下发策略。**
|
||||
- **`isOpen()` 语义**:`status == 1(STATUS_OPEN)` 且抽样通过(`isOpenByRandom`,对应 `sampling_rate`)。
|
||||
- **聚合上传触发**:`onRecord → isNeedUpload`——累计条数 ≥ `aggregatorSizeThreshold`(`aggre_count`)或距上次上传 ≥ `aggregatorTimeThreshold`(`aggre_time`)即触发 `push`。
|
||||
- **重启生效机制**:`LaunchMonitor.ensureColdStartMetric()` 的幂等标记在 `create` 被策略拒绝(返回空实现)时仍置 `true`——本进程内策略下发后不会重试创建,**必须重启应用**(与上节"判定生效"一致);策略未下发时链路 A(Elk 追溯)不受影响、正常上传。
|
||||
|
||||
随后为该模块补充 ELK 索引模板,在 Grafana 建立看板面板(建议形态):
|
||||
|
||||
```json
|
||||
{
|
||||
"panels": [
|
||||
{
|
||||
"title": "冷启动 P50/P95",
|
||||
"type": "percentile",
|
||||
"metric": "cold_start_time",
|
||||
"group_by": ["app_version", "start_type"],
|
||||
"window": "1d"
|
||||
},
|
||||
{
|
||||
"title": "超 2 秒占比",
|
||||
"type": "ratio",
|
||||
"condition": "cold_start_time >= 2000",
|
||||
"group_by": ["app_version"],
|
||||
"window": "1d"
|
||||
},
|
||||
{
|
||||
"title": "分桶分布",
|
||||
"type": "bucket_distribution",
|
||||
"metric": "cold_start_time",
|
||||
"buckets": [500, 600, 700, 800, 1000, 1500, 2000, 3000],
|
||||
"group_by": ["entry_page", "app_install_status"],
|
||||
"window": "1d"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
看板按版本、启动类型和时间聚合;`app_install_status` 维度(`new_install`/`over_written`)用于排除安装后首启的偏高数据。
|
||||
|
||||
## 6. 兼容与兜底
|
||||
|
||||
链路 A(`ElkService` 追溯)上传 `cold_start_time`、4 维度和阶段时间字段,不具备分桶聚合语义,由链路 B(`HXEventMonitor` 分桶指标,策略驱动见 5.1)承担看板聚合;后台需补充 ELK 索引模板、聚合规则和看板。链路 B 的 `DataMonitorBuilder`/`record` API 形态以 `@kernel/app_monitor_event` 的 `.d.ts` 为准(已随 `HXLaunchMonitorPlugin` 编译验证),不直接复用 Android 文档中的 Java API。
|
||||
|
||||
`APP_LAUNCH` 的事件解析必须与崩溃事件分开处理:崩溃事件依赖异常和日志字段,启动事件不包含这些字段。上传应异步执行,不阻塞首页渲染;以 `time + icon_input_time + start_type + process_name` 去重,兼容系统重新投递。
|
||||
|
||||
### 6.1 启动 5 秒内退出的边界行为
|
||||
|
||||
系统 `extend_time` 的定义:手指离手到 `reportDrawnCompleted` 的耗时,若 5 秒内未调用则该值为 0。真机实测(nova 14 Pro)启动后立即退出的行为链:
|
||||
|
||||
- **退出时首帧已提交**(`reportDrawnCompleted` 已调用,真实 app 首帧通常在 1 秒内):事件正常生成、`extend_time` 有效;若进程在事件送达(约 5 秒延迟)前退出,事件被系统持久化,下次启动 watcher 收到并补报,数据有效。
|
||||
- **退出时首帧未提交**(启动极慢或立即强杀):`reportDrawnCompleted` 从未调用,5 秒窗口过期,系统未生成有效事件(实测无补投递),不产生上报。
|
||||
|
||||
补报事件的 `entry_page` 语义:`capturedEntryPage` 是进程级变量,补报事件到达新进程时该值为新进程的快照(或默认值),不能归因到新启动页面。因此客户端以 watcher 注册时间戳为锚点判定补报事件:`icon_input_time` 早于注册时间超过 2 秒容差(正常启动链间隔为毫秒~秒级,补报至少早 5 秒)即视为补报,`entry_page` 置 `'unknown'` 上报,避免错误归因。同时上报字段新增 `is_backfill` 标识(`true` 为补报事件),看板可据此过滤补报数据或单独统计:补报的耗时数据有效可纳入 P50/P95 统计,但 `entry_page` 分布分析应排除。
|
||||
|
||||
真机实测确认(2026-08-05):部署新版本后先 force-stop 再冷启动,新进程收到旧进程的遗留事件,判定 `补报事件=是`(`icon_input_time` 早于注册时间约 4 秒);随后正常冷启动收到自身事件,判定 `补报事件=否`。双向判定均正确,正常启动无误判。
|
||||
|
||||
### 6.2 双通道上报
|
||||
|
||||
启动指标同时走两条通道,职责分离:
|
||||
|
||||
- **分桶指标(`HXEventMonitor`,module=`launch`)**:`record(extendTime)` 后由 SDK 按桶聚合,负责看板聚合统计(P50/P95、超 2 秒占比、分桶分布)。**策略驱动**:无后台下发策略时 `create()/record()` 空转(见 5.1),`record` 失败/空转不影响启动流程,聚合逻辑在 SDK 侧。实现为 `HXLaunchMonitorPlugin`(仿 `HXCrashMonitorPlugin`,`startPlugin()` 中建指标),在 `APMHelper` 的 `.plugin(...)` 列表注册。
|
||||
- **追溯明细(`ElkService`,biz=`launch`)**:11 字段最小集(指标/4 维度含 `app_install_status`/启动生命周期各阶段时间/`git_commit`/`is_backfill`),负责明细追溯与后端校验;其他字段按需加回。分桶指标同步 4 个维度(`setDimension4Name('app_install_status')`),与追溯维度一致。
|
||||
|
||||
两通道独立失败互不影响;`HXEventMonitor` 的 module 参数为显式传入的 `'launch'`,与文档模块定义一致,不经过 `APMElkService` 固定的 `'apm'` 来源。
|
||||
|
||||
## 7. 验收标准
|
||||
|
||||
1. 单次冷启动中,隐私页、网络页和首页连续跳转时,`reportDrawnCompleted` 仍只提交一次。
|
||||
2. 未登录启动以登录/启动页为入口;已登录启动以 `Index` 为入口,`entry_page` 不被后续页面覆盖。
|
||||
3. 每次冷启动只产生一条可查询记录,且 `extend_time > 0`;热启动、无效耗时和重复事件不上传。
|
||||
4. 页面在系统异步回调前销毁时,不影响已经提交的上报;旧页面不会释放新页面 observer。
|
||||
5. 本地日志与后台数据量级一致;看板可查询 P50、P95、超 2 秒占比及分桶分布。
|
||||
6. 断网或上传失败不影响启动流程,网络恢复后按既有上传机制重试。
|
||||
7. 双通道上传验证:完整 workspace 编译通过(`HXEventMonitor` 的 `DataMonitorBuilder`/`getEventFactory`/`setBuckets`/`record`/`setDimension_4` 已随 `HXLaunchMonitorPlugin` 编译验证);真机冷启动 hilog 出现 `Cold start metric uploaded`(链路 A)与 `Cold start bucket metric recorded`(链路 B,需后台下发策略,见 5.1);ELK 索引可查 `biz='launch'` 的 11 字段记录,APM 平台 `launch` 模块出现分桶分布。
|
||||
8. 维度与分桶:分桶分布可按 4 个维度(`start_type`/`entry_page`/`app_version`/`app_install_status`)分组查询;分桶边界变更需后台重建或并档(历史分布不可比)。
|
||||
|
||||
## 8. 已验证结果
|
||||
|
||||
### 8.1 首次回归(2026-08-05,`CrashDiagnosticsDemo`,nova 14 Pro)
|
||||
|
||||
未登录冷启动入口为 `LaunchLogin`,`extend_time` 约 `158ms`;持久化登录后冷启动入口为 `Index`,`extend_time` 约 `155ms`。网络异常、网络恢复、隐私确认及页面返回场景均保持一次 draw 回调和一次系统提交,后续页面注册被全局保护拦截;`start_type=1` 的热启动事件被正确忽略。
|
||||
|
||||
### 8.2 追溯字段扩展后的真机验证(2026-08-05,`CrashDiagnosticsDemo`,nova 14 Pro,OpenHarmony-6.1.1.120)
|
||||
|
||||
| # | 场景 | 操作 | 结果 |
|
||||
|---|---|---|---|
|
||||
| 1 | 已登录冷启动 | `aa force-stop` 后启动 | 入口 `Index`,draw 1 次、提交 1 次、成功 1 次,`extend_time=144ms`,`start_type=0` |
|
||||
| 2 | 已登录冷启动(复测) | `aa force-stop` 后启动 | 入口 `Index`,`extend_time=137ms`,单次提交 |
|
||||
| 3 | 未登录冷启动 | `bm clean -d` 清数据后启动 | 入口 `LaunchLogin`,`extend_time=152ms`,单次提交 |
|
||||
| 4 | 多入口连续跳转 | 登录页 → 网络异常页 → 返回 → 隐私确认页 | `LaunchNetwork`、`LaunchPrivacy` 重复首帧注册均被拦截(`DrawReportState` 保护),无重复提交 |
|
||||
| 5 | 时间拆解字段捕获 | 观察三次冷启动的 APP_LAUNCH 详情 | `response_latency=19~24ms`、`startability_processstart_dur=57~83ms`、`appattach_to_appforeground_dur=28~33ms`,均为非零有效值 |
|
||||
| 6 | 热启动(进程存活) | 对运行中进程再次 `aa start` | 无新 `APP_LAUNCH` 事件产生(系统对运行中单例不视为启动),不产生上报 |
|
||||
|
||||
验证结论:新增字段在真机全部可捕获且数值合理(`response_latency` 反映系统响应,两个 `*_dur` 拆解系统侧进程启动与挂载耗时)。已登录/未登录冷启动耗时(137~152ms)与首次回归(155/158ms)量级一致,字段扩展未引入回归。
|
||||
|
||||
**发现的边界情况**:`animation_finish_time` 在真机上为 `0`(系统未填充该字段),因此计算"应用可优化段(`extend_time − animation_finish_time`)"时需处理 0 值场景——此时该段等于 `extend_time` 全量,应在看板聚合时兼容。
|
||||
|
||||
### 8.3 完整重跑确认(2026-08-05 当天,同设备)
|
||||
|
||||
全部场景重新模拟一遍,结果与 8.2 一致:
|
||||
|
||||
| # | 场景 | 结果 |
|
||||
|---|---|---|
|
||||
| 1 | 清数据后未登录冷启动 | 入口 `LaunchLogin`,`extend_time=161ms`,单次提交 |
|
||||
| 2 | 登录后同进程跳转 `Index` | `Index` 重复首帧注册被拦截,无重复提交 |
|
||||
| 3 | 已登录冷启动 | 入口 `Index`,`extend_time=138ms`,单次提交 |
|
||||
| 4 | 热启动(进程存活) | 无新 `APP_LAUNCH` 事件,不产生上报 |
|
||||
|
||||
### 8.4 连续 3 次冷启动稳定性验证(2026-08-05 当天,同设备)
|
||||
|
||||
已登录态下连续 3 次 `force-stop` → 启动,每次均为独立进程、单次 draw 回调、单次 `reportDrawnCompleted` 提交与成功回调、独立有效 `APP_LAUNCH` 事件:
|
||||
|
||||
| 次数 | 进程 | 入口 | 提交次数 | `extend_time` | `start_type` |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | 43415 | `Index` | 1 | 131ms | 0 |
|
||||
| 2 | 43785 | `Index` | 1 | 132ms | 0 |
|
||||
| 3 | 43914 | `Index` | 1 | 126ms | 0 |
|
||||
|
||||
连续启动耗时稳定在 126~138ms,无事件串扰、无重复上报。
|
||||
|
||||
### 8.5 全场景矩阵 3 组验证(2026-08-05 当天,同设备)
|
||||
|
||||
每组完整链路:清数据 → 未登录冷启动 → 网络异常页跳转(拦截)→ 返回 → 隐私确认页跳转(拦截)→ 返回 → 模拟登录 → `Index` 跳转(拦截)→ 已登录冷启动 → 热启动(无新事件)。3 组结果:
|
||||
|
||||
| 组 | 未登录冷启动(入口 `LaunchLogin`) | 网络页拦截 | 隐私页拦截 | `Index` 拦截 | 已登录冷启动(入口 `Index`) |
|
||||
|---|---|---|---|---|---|
|
||||
| 1 | `extend_time=162ms`(进程 44062) | ✓ | ✓ | ✓ | `extend_time=133ms`(进程 44644) |
|
||||
| 2 | `extend_time=157ms`(进程 44927) | ✓ | ✓ | ✓ | `extend_time=131ms`(进程 45200) |
|
||||
| 3 | `extend_time=159ms`(进程 45571) | ✓ | ✓ | ✓ | `extend_time=135ms`(进程 45862) |
|
||||
|
||||
每组 6 个场景全部按预期执行:单次冷启动只提交一次 `reportDrawnCompleted`,后续页面(网络异常页、隐私页、`Index`)的重复首帧注册全部被拦截,热启动不产生新事件。未登录冷启动耗时稳定在 157~162ms,已登录稳定在 131~135ms,组间一致,验证可复现。
|
||||
|
||||
### 8.6 启动 5 秒内退出场景验证(2026-08-05 当天,同设备)
|
||||
|
||||
系统 `extend_time` 语义:手指离手 5 秒内未调用 `reportDrawnCompleted` 则该值为 0。真机实测退出场景:
|
||||
|
||||
| # | 场景 | 操作 | 结果 |
|
||||
|---|---|---|---|
|
||||
| 1 | 启动后 1 秒内退出(首帧已提交) | `aa start` → 1 秒后 `force-stop` | `reportDrawnCompleted` 已在退出前提交,事件正常生成、`extend_time` 有效;事件送达时进程已退出,由后续进程补投递处理 |
|
||||
| 2 | 启动后立即强杀(首帧未提交) | `aa start` → 立即 `force-stop`(无间隔) | 仅有"已监听 draw"日志,`reportDrawnCompleted` 从未调用;5 秒窗口过期,系统未生成有效事件,重启后无补投递,不产生上报 |
|
||||
| 3 | 补报判定(修复验证) | 部署新版本后 `force-stop` → 冷启动 | 新进程收到旧进程遗留事件,`icon_input_time` 早于注册时间约 4 秒,判定 `补报事件=是`,`entry_page` 按 `unknown` 处理 |
|
||||
| 4 | 正常冷启动(修复验证) | 干净 `force-stop` → 启动 | 收到自身事件,判定 `补报事件=否`,无误判 |
|
||||
|
||||
结论:首帧未提交时退出由 `extend_time<=0` 过滤兜底不上报;首帧已提交后退出的补报数据有效,修复后补报事件与正常事件可稳定区分(判定逻辑见 6.1)。
|
||||
|
||||
### 8.7 全场景(含 5 秒退出)3 组验证(2026-08-05 当天,同设备)
|
||||
|
||||
每组 8 步链路:清数据 → 未登录冷启动 → 网络页拦截 → 隐私页拦截 → 登录 `Index` 拦截 → 已登录冷启动 → 热启动 → 立即强杀(首帧未提交)→ 1 秒退出(首帧已提交)→ 补报判定。3 组结果:
|
||||
|
||||
| 步骤 | 组 1 | 组 2 | 组 3 |
|
||||
|---|---|---|---|
|
||||
| 未登录冷启动(`LaunchLogin`) | 158ms / 补报=否 | 180ms / 补报=否 | 180ms / 补报=否 |
|
||||
| 网络页重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| 隐私页重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| `Index` 重复注册拦截 | ✓ | ✓ | ✓ |
|
||||
| 已登录冷启动(`Index`) | 129ms / 补报=否 | 132ms / 补报=否 | 131ms / 补报=否 |
|
||||
| 热启动(进程存活) | 无新事件 | 无新事件 | 无新事件 |
|
||||
| 立即强杀(首帧未提交)后重启 | 被强杀进程无有效事件,重启进程正常捕获 | 同左 | 同左 |
|
||||
| 1 秒退出(首帧已提交)后重启 | 先收遗留事件补报=是,再收自身事件补报=否 | 同左 | 同左 |
|
||||
|
||||
24 个断言(3 组 × 8 步)全部通过,组间一致。补报判定在每组中均双向正确:遗留事件稳定判定为补报(`entry_page` 不归因),自身事件稳定判定为非补报(`entry_page` 正常归因)。
|
||||
|
||||
### 8.8 重装 + `is_first_install` 维度全场景验证(2026-08-06,`CrashDiagnosticsDemo`,nova 14 Pro)
|
||||
|
||||
**前置改动**:demo 同步 `is_first_install` 维度(`preferences` 无启动记录视为安装后首启,`putSync` 后 `flushSync` 同步落盘——初版遗漏 `flushSync` 导致 force-stop 后数据丢失、二次启动误判首启,对比 `AuthSession` 用法定位修复);页面重命名为 `LaunchLogin`/`LaunchNetwork`/`LaunchPrivacy`,诊断类更名为 `LaunchDiagnostics`。
|
||||
|
||||
**验证方式**:`bm uninstall -n` 完全卸载重装(真实安装后首启语义),随后全场景 3 组 × 8 步(冷启动/网络页拦截/隐私页拦截/登录跳转拦截/已登录冷启动/热启动/立即强杀/1 秒退出补报)。表中数据为页面重命名前的同一逻辑验证(页面名仅为日志展示用途,不影响捕获语义);重命名后 `devecocli build` 编译通过,真机运行回归待设备恢复后补跑。
|
||||
|
||||
| 步骤 | 组 1 | 组 2 | 组 3 |
|
||||
|---|---|---|---|
|
||||
| 未登录冷启动(入口 `LaunchLogin`) | 165ms / 首启=true / 补报=否 | 165ms / 首启=true | 161ms / 首启=true |
|
||||
| 网络页/隐私页/`Index` 重复注册拦截 | ✓ ×3 | ✓ ×3 | ✓ ×3 |
|
||||
| 已登录冷启动(入口 `Index`) | 134ms / 首启=false | 129ms / 首启=false | 131ms / 首启=false |
|
||||
| 热启动(进程存活) | 无新事件 | 无新事件 | 无新事件 |
|
||||
| 立即强杀后重启 | 142ms / 补报=否 | 132ms / 补报=否 | 127ms / 补报=否 |
|
||||
| 1 秒退出后重启 | 补报=是 + 自身=否 | 同左 | 同左 |
|
||||
|
||||
24 断言全部通过。关键结论:
|
||||
|
||||
- **`is_first_install` 维度语义正确**:安装/清数据后首启 = `true`(161~165ms),非首启 = `false`(127~134ms);**首启耗时系统性偏高约 30ms**,该维度可将其从常规分布中区分,避免拉高 P50/P95。
|
||||
- 补报判定 3 组双向正确,拦截保护 3 组 × 3 页无漏防。
|
||||
- 耗时稳定(127~165ms);此前观测到的 `response_latency=5086ms` 异常值本次未复现(偶发)。
|
||||
- 注意:demo 的 `is_first_install`(boolean)基于 `preferences` 记录,`bm clean` 清数据后该标记重置(再次显示首启);生产实现为 `app_install_status`(`normal`/`new_install`/`over_written` 三值,见第 4 节),以生产为准。
|
||||
|
||||
### 8.9 真实应用验证(2026-08-06,完整 workspace,nova 14 Pro)
|
||||
|
||||
在完整 workspace 落地(`HXLaunchMonitorPlugin` 插件 + `APMHelper` 注册)后的真机数据:
|
||||
|
||||
| 场景 | 结果 |
|
||||
|------|------|
|
||||
| 全新安装首启(`new_install`) | 链路 A:`cold_start_time=1141ms`,Entry=Index,Elk 上报 ✅ |
|
||||
| 二次冷启动(`normal`) | 链路 A:`cold_start_time=1717ms`,Elk 上报 ✅ |
|
||||
| 链路 B(分桶) | 待后台下发 `launch` 策略(5.1)后复测 |
|
||||
|
||||
编译:`claude_compile.sh → BUILD_SUCCESS`(链路 B API 随插件编译验证通过)。
|
||||
|
||||
真实应用耗时(1.1~1.7s)远高于 demo(127~165ms),符合生产首页复杂度的预期;`app_install_status` 维度可区分首启与常规启动的耗时差异。
|
||||
|
After Width: | Height: | Size: 228 KiB |
|
After Width: | Height: | Size: 698 KiB |
@@ -0,0 +1,128 @@
|
||||
# 启动性能监控(冷启动耗时)接入配置
|
||||
|
||||
> 作者:cheliangzhao
|
||||
> 日期:2026-08-06
|
||||
> 涉及模块:`entry`(HAP 入口)+ `@kernel/app_monitor_event`(APM 事件 SDK)
|
||||
> 对应需求:冷启动耗时(`cold_start_time`)采集与看板聚合
|
||||
|
||||
---
|
||||
|
||||
## 1. 背景与目标
|
||||
|
||||
统计同花顺期货鸿蒙版**冷启动耗时**,覆盖三条启动入口(`Index` / `LauncherPage` / `NetworkAnomalyPage`),区分冷启动 / 补投递四种场景,实现启动性能的持续观测与劣化告警。
|
||||
|
||||
采集分两条通道:
|
||||
|
||||
- **链路 A(Elk 追溯,已上线验证)**:把冷启动指标结构化 JSON 经 `ElkService` 异步上报,字段最全,可明细定位。
|
||||
- **链路 B(HXEventMonitor 分桶,需后台策略开启后生效)**:由 SDK 按桶自动归集,负责看板聚合曲线。
|
||||
|
||||
> ⚠️ 链路 B 是**策略驱动**的:SDK 侧在 `EventMonitorStrategy` 找不到 `launch` 模块的开启策略时,`create()/record()` 一律空转并打印 `Monitor.Event cold_start_time eventId has not created!`。应用侧无法绕开,必须由 **APM 后台下发对应模块策略**才能激活。
|
||||
|
||||
---
|
||||
|
||||
## 2. 链路 A:Elk 追溯(已生效,无需后台操作)
|
||||
|
||||
上报字段(`LaunchElkBuilder`,业务来源 `launch`):
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `cold_start_time` | number | 冷启动耗时 ms = 系统事件 `extend_time` |
|
||||
| `start_type` | number | 启动类型(当前只统计 `0`=冷启动) |
|
||||
| `entry_page` | string | 入口页(`Index`/`LauncherPage`/`NetworkAnomalyPage`;补投递为 `unknown`) |
|
||||
| `app_version` | string | 应用内部版本号 |
|
||||
| `app_install_status` | string | `normal`/`new_install`/`over_written` |
|
||||
| `response_latency` | number | 离手到动效开始(需 API 22+) |
|
||||
| `animation_finish_time` | number | 动效完成耗时 |
|
||||
| `startability_processstart_dur` | number | 进程创建段(仅冷启动) |
|
||||
| `appattach_to_appforeground_dur` | number | 进程挂载段(仅冷启动) |
|
||||
| `git_commit` | string | 代码级追溯,劣化定位用 |
|
||||
| `is_backfill` | boolean | 是否系统补投递的上次启动遗留事件 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 链路 B:HXEventMonitor 分桶指标(需后台开启)
|
||||
|
||||
### 3.1 后台需下发的模块策略
|
||||
|
||||
SDK 由 `HXEventMonitorPlugin.parseMonitorStrategy()` 读取,注入到 `EventStrategyBean` 后按模块打开。请为 **module=`launch`** 下发如下配置:
|
||||
|
||||
```json
|
||||
{
|
||||
"module": "launch",
|
||||
"config": {
|
||||
"status": 1,
|
||||
"sampling_rate": 1.0,
|
||||
"interval": 5000,
|
||||
"aggre_time": 60000,
|
||||
"aggre_count": 10
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
字段说明(对齐 `EventStrategyBean.fromJson`,仅这些字段生效):
|
||||
|
||||
| 字段 | 取值 | 说明 |
|
||||
|------|------|------|
|
||||
| `module` | `launch` | 必须与代码 `HXEventMonitor.getEventFactory('launch')` 一致 |
|
||||
| `status` | `1` | **必须 `1`(STATUS_OPEN)**;否则 `isOpen()` 为 false,`create/record` 全空转 |
|
||||
| `sampling_rate` | `0~1` | 抽样率,`isOpen()` 需 `status==1 && 抽样通过` 才生效 |
|
||||
| `interval` | ms | 采集间隔 |
|
||||
| `aggre_time` | ms | 聚合时间阈值,达到即触发一次 push |
|
||||
| `aggre_count` | 条 | 聚合数量阈值,达到即触发一次 push |
|
||||
|
||||
### 3.2 指标定义
|
||||
|
||||
| 项 | 值 |
|
||||
|----|----|
|
||||
| 模块名 `module` | `launch` |
|
||||
| 指标名 `metric` | `cold_start_time` |
|
||||
| 值域 | 冷启动耗时 ms(`extend_time`) |
|
||||
| 分桶 `buckets` | `[500, 600, 700, 800, 1000, 1500, 2000, 3000]`(按实测 600–800ms 主体细分,尾部留劣化锚点) |
|
||||
| 维度1 `start_type` | 冷启动 `0` |
|
||||
| 维度2 `entry_page` | `Index`/`LauncherPage`/`NetworkAnomalyPage`/`unknown`(补投递) |
|
||||
| 维度3 `app_version` | 应用内部版本号 |
|
||||
| 维度4 `app_install_status` | `normal`/`new_install`/`over_written` |
|
||||
|
||||
### 3.3 判定生效
|
||||
|
||||
后台下发后,重启应用,冷启动日志中:
|
||||
|
||||
- ✅ 消失:`Monitor.Event cold_start_time eventId has not created!`
|
||||
- ✅ 出现:`Cold start bucket metric recorded: <ms>` 与 SDK 聚合 push 日志
|
||||
- ✅ 看板出现 `launch` 模块 `cold_start_time` 桶分布
|
||||
|
||||
---
|
||||
|
||||
## 4. 相关代码与文件
|
||||
|
||||
| 文件 | 作用 |
|
||||
|------|------|
|
||||
| `entry/src/main/ets/monitor/LaunchMonitor.ets` | APP_LAUNCH 订阅、Elk 上报、首帧 `reportDrawnCompleted`、桶指标定义与 record |
|
||||
| `entry/src/main/ets/monitor/LaunchMonitor.ets`(`ensureColdStartMetric`) | 创建 `launch/cold_start_time` 桶指标(幂等) |
|
||||
| `entry/src/main/ets/monitor/plugin/HXLaunchMonitorPlugin.ets` | 仿 `HXCrashMonitorPlugin` 接入的启动插件,在 `startPlugin()` 建指标 |
|
||||
| `entry/src/main/ets/monitor/APMHelper.ets` | 注册 `HXLaunchMonitorPlugin` 及配置类、`.plugin(...)` 列表 |
|
||||
| `entry/src/main/ets/entryability/EntryAbility.ets` | `onCreate` 早注册 `LaunchMonitor.init()` |
|
||||
| `entry/src/main/ets/pages/Index.ets` / `LauncherPage.ets` / `network/NetworkAnomalyPage.ets` | 首帧 `HOME_ROOT` 监听 + `reportFirstFrameOnDraw`/`dispose` |
|
||||
|
||||
### 打点时序
|
||||
|
||||
```
|
||||
EntryAbility.onCreate → LaunchMonitor.init()(注册 APP_LAUNCH watcher)
|
||||
入口页 aboutToAppear → reportFirstFrameOnDraw(HOME_ROOT) → 首帧 draw → reportDrawnCompleted()
|
||||
系统产生 APP_LAUNCH 事件(冷启动)→ onReceive
|
||||
├─ 链路A: ElkService 上传冷启动 JSON(字段见 §2)
|
||||
└─ 链路B: HXEventMonitor record(cold_start_time)(依赖 launch 策略,见 §3)
|
||||
补投递:icon_input_time 早于 watcher 注册 > 2000ms → 判为 backfill,entry_page=unknown
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 真机验证结论(2026-08-06,nova 14 Pro)
|
||||
|
||||
| 场景 | 结果 |
|
||||
|------|------|
|
||||
| 全新安装首启(`new_install`) | 链路 A:`cold_start_time=1141ms`,Entry=Index,Elk 上报 ✅ |
|
||||
| 二次冷启动(`normal`) | 链路 A:`cold_start_time=1717ms`,Elk 上报 ✅ |
|
||||
| 链路 B(分桶) | ❌ 待后台下发 `launch` 策略(§3.1)后复测 |
|
||||
|
||||
编译:`./claude_tool/claude_compile.sh → BUILD_SUCCESS`
|
||||
@@ -0,0 +1,6 @@
|
||||
/node_modules
|
||||
/oh_modules
|
||||
/.preview
|
||||
/build
|
||||
/.cxx
|
||||
/.test
|
||||
@@ -0,0 +1,31 @@
|
||||
# `entry` HAP Module
|
||||
|
||||
## Scope and layout
|
||||
|
||||
- This checkout is the HarmonyOS stage-model `entry` HAP module, not the complete application workspace.
|
||||
- Production ArkTS: `src/main/ets/`. Existing areas include `pages/`, `widget/`, `entryability/`, `communication/`, `initialize/`, and `register/`.
|
||||
- Resources: `src/main/resources/`.
|
||||
- Local Hypium tests: `src/test/`; device/ability Hypium tests: `src/ohosTest/`.
|
||||
- Module metadata: `src/main/module.json5`; build targets/configuration: `build-profile.json5`.
|
||||
- `hvigorfile.ts` registers the HAP tasks and HMRouter plugin.
|
||||
|
||||
## Workspace and commands
|
||||
|
||||
`oh-package.json5` uses sibling `file:../...` dependencies, and imports resolve across those siblings. Invoke builds and runs only from the complete parent workspace; its exact root is not verified in this checkout.
|
||||
|
||||
```bash
|
||||
# Parent-workspace commands
|
||||
devecocli build --modules entry@default
|
||||
devecocli build --modules entry@ohosTest
|
||||
devecocli build --modules entry@Official --build-mode release
|
||||
devecocli device list
|
||||
devecocli run --module entry
|
||||
```
|
||||
|
||||
- Configured targets are `default`, `ohosTest`, `OEM`, `Official`, and `forTest`.
|
||||
- Device/ability tests require a connected device or emulator.
|
||||
- No test-runner command is verified here.
|
||||
|
||||
## Generated files
|
||||
|
||||
- `oh-package-lock.json5` is autogenerated; do not edit it manually.
|
||||
@@ -0,0 +1,112 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project identity
|
||||
|
||||
This is the HarmonyOS stage-model `entry` HAP module for **同花顺期货 (THS Futures)**, a financial futures trading app. It is NOT a complete workspace — sibling modules (`biz_common`, `biz_trade`, `biz_quote`, etc.) live alongside this directory and are consumed via `file:../...` dependencies. Always invoke builds and runs from the parent workspace, not this directory.
|
||||
|
||||
## Build and run commands
|
||||
|
||||
```bash
|
||||
# Build specific targets (run from parent workspace)
|
||||
devecocli build --modules entry@default
|
||||
devecocli build --modules entry@ohosTest
|
||||
devecocli build --modules entry@Official --build-mode release
|
||||
|
||||
# List connected devices/emulators
|
||||
devecocli device list
|
||||
|
||||
# Run on device
|
||||
devecocli run --module entry
|
||||
```
|
||||
|
||||
Configured targets: `default`, `ohosTest`, `OEM`, `Official`, `forTest`.
|
||||
|
||||
- `oh-package-lock.json5` is autogenerated — never edit it.
|
||||
- Device/ability tests (`src/ohosTest/`) require a connected device or emulator.
|
||||
|
||||
## Architecture overview
|
||||
|
||||
### App lifecycle and initialization chain
|
||||
|
||||
`EntryAbility` (`src/main/ets/entryability/EntryAbility.ets`) is the main `UIAbility` (singleton, launch type `singleton`). Its `onCreate` runs a carefully ordered initialization sequence:
|
||||
|
||||
1. Push message listener (trade daily notifications via `pushService.receiveMessage`)
|
||||
2. Custom figure registration (`FigureRegistry.registerAll()`)
|
||||
3. Global context initialization (multiple `ContextUtils` variants for different modules, plus `GlobalContext`, `AppUtil`)
|
||||
4. Security monitor, gray test init, cloud store log init
|
||||
5. HMRouter init
|
||||
6. Analytics/ELK upload, UGC client, auth listener, event subscription
|
||||
7. RCP HTTP header configuration
|
||||
8. Future market init chain: `TabConfigManager` → `CommonIndicatorsManage` → code table → `MainContractManager` → `HQTabManager` → `FutureMarketDataManager` → `InactiveContractManager` → `DelayMarketManager` → `TradeInitializor` → `CurveStorage`
|
||||
9. Ads, AI group client, font loading, want caching
|
||||
|
||||
`onWindowStageCreate` then branches on three conditions:
|
||||
- **No network** → `loadNetworkAnomalyPage`
|
||||
- **Privacy not accepted** → `loadPrivacyPage` (LauncherPage)
|
||||
- **Normal** → `loadIndexPage` (the main Index page)
|
||||
|
||||
### Navigation and page registry
|
||||
|
||||
All pages must be registered in `src/main/resources/base/profile/main_pages.json`. The app uses **HMRouter** (`@hadss/hmrouter`) for navigation. The main UI uses a `Tabs` component in `Index.ets` with configurable tab sets (`IndexTabManager` via `TabManager`). On large screens (`lg` breakpoint), tabs render vertically on the left.
|
||||
|
||||
### Communication layer
|
||||
|
||||
`src/main/ets/communication/` manages long-lived TCP socket connections to the trading backend:
|
||||
|
||||
- **`ConnectionInitiator`** — initializes site configuration (IP/port from `Config.ets`), supports inner-network debug mode, and handles debug-tool server overrides.
|
||||
- **`ServiceHandlerImpl`** — registers two connection configurations:
|
||||
- `HqConnectionConfig` (行情): authenticates via `HXAuthManager`, pipes data through `AuthJsonStructObserver`, `RealDataStructObserver`, `SidInstanceIdReceiver`
|
||||
- `CBASConnectionConfig` (analytics): separate connection for CBAS analytics with trade ELK log observer
|
||||
- **`HXSocketMonitor`** — monitors socket health across app foreground/background transitions.
|
||||
|
||||
### Group subscription system (real-time sync)
|
||||
|
||||
`GroupSubManager` (from `base_mobilepush`) manages push-based data synchronization. Handlers in `src/main/ets/groupsub/business/` handle specific data domains:
|
||||
|
||||
- **User-scoped** (`addUserGroupHandler`): self-code lists, YYB account lists, trade settings, indicators, draw lines, AI group, SID data, function authority
|
||||
- **All-user-scoped** (`addAllGroupHandler`): ad strategies, gray test, account analysis, futures companies, stock/variety storage updates, main contract changes
|
||||
|
||||
Each handler receives push notifications and drives local state updates.
|
||||
|
||||
### Service registration pattern
|
||||
|
||||
Services in `src/main/ets/register/` follow a singleton-then-init pattern:
|
||||
- **`cbasService`** (CbasServiceRegister) — analytics/event capture with `HxCBASAgentProvider`
|
||||
- **`UgcClient`** — user-generated content client
|
||||
- **`ElkUploadRegister`** — error log upload
|
||||
- **`SecurityMonitorInit`** — crash protection
|
||||
|
||||
### Tabs and page structure
|
||||
|
||||
The main UI has four tabs (configurable, some may be hidden):
|
||||
1. **首页** (FirstPage) — `@b2c/first_page`
|
||||
2. **行情** (HQPage) — `biz_market`, renders self-codes, futures quotes, option chains
|
||||
3. **交易** (TradeBiz) — `biz_trade`, conditionally hidden via `HexinVersionControl.hideTrade()`
|
||||
4. **发现** (DiscoverPage) — `@b2c/biz_discover`
|
||||
|
||||
Each tab has its own `TabManager` sub-manager. Tab visibility is controlled by `IndexTabManager` which extends `TabManager`.
|
||||
|
||||
### Multi-device adaptation
|
||||
|
||||
The module targets phone, tablet, and 2in1. Responsive breakpoints are handled through `updateBreakpoint()` (from `biz_common`) which broadcasts breakpoint changes (`sm`, `md`, `lg`). Window size changes are monitored in `EntryAbility.onWindowStageCreate`.
|
||||
|
||||
### Key sibling dependencies
|
||||
|
||||
All resolved via `file:../...` in `oh-package.json5`:
|
||||
- `biz_common` — shared utilities, constants, tab management, preferences
|
||||
- `biz_trade` — trading functionality, account management, notifications
|
||||
- `biz_quote` — quote/chart rendering, indicators, settings
|
||||
- `biz_market` — market data, HQ tabs, contracts
|
||||
- `biz_option`, `biz_forecast`, `biz_indicator` — option/forecast/indicator features
|
||||
- `service_push`, `service_watchlist`, `service_aigroup` — push, watchlist, AI services
|
||||
- `biz_debug` — debug tools, server switching
|
||||
- `@kernel/lib_communication` — TCP socket connection library
|
||||
- `@kernel/theme_manager` — theming and color mode
|
||||
- `@hadss/hmrouter` — route management
|
||||
- `@ohos/hypium` — test framework
|
||||
|
||||
### Debug infrastructure
|
||||
|
||||
In non-release builds, a `DebugFloatingBall` overlay is created via `windowStage.createSubWindow()`. Debug pages live under `pages/debug/` and support server switching, gray test configuration, cloud storage inspection, URL modification, and memory leak monitoring.
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"apiType": "stageMode",
|
||||
"buildOption": {
|
||||
"arkOptions": {
|
||||
// "apPath": "./modules.ap" /* Profile used for profile-guided optimization (PGO), a compiler optimization technique to improve app runtime performance. */
|
||||
}
|
||||
},
|
||||
"buildOptionSet": [
|
||||
{
|
||||
"name": "release",
|
||||
"arkOptions": {
|
||||
"obfuscation": {
|
||||
"ruleOptions": {
|
||||
"enable": true,
|
||||
"files": [
|
||||
"./obfuscation-rules.txt"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
"targets": [
|
||||
{
|
||||
"name": "default"
|
||||
},
|
||||
{
|
||||
"name": "ohosTest",
|
||||
},{
|
||||
"name": "OEM",
|
||||
"output": {"artifactName": "OEM"}
|
||||
},{
|
||||
"name": "Official",
|
||||
"output": {"artifactName": "futures-Official"}
|
||||
},{
|
||||
"name": "forTest",
|
||||
"output": {"artifactName": "futures-forTest"}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
-keep-file-name
|
||||
-keep-property-name
|
||||
-keep-global-name
|
||||
@@ -0,0 +1,7 @@
|
||||
import { hapTasks } from '@ohos/hvigor-ohos-plugin';
|
||||
import { hapPlugin } from '@hadss/hmrouter-plugin';
|
||||
|
||||
export default {
|
||||
system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */
|
||||
plugins:[hapPlugin()] /* Custom plugin to extend the functionality of Hvigor. */
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# Define project specific obfuscation rules here.
|
||||
# You can include the obfuscation configuration files in the current module's build-profile.json5.
|
||||
#
|
||||
# For more details, see
|
||||
# https://gitee.com/openharmony/arkcompiler_ets_frontend/blob/master/arkguard/README.md
|
||||
|
||||
# Obfuscation options:
|
||||
# -disable-obfuscation: disable all obfuscations
|
||||
# -enable-property-obfuscation: obfuscate the property names
|
||||
# -enable-toplevel-obfuscation: obfuscate the names in the global scope
|
||||
# -compact: remove unnecessary blank spaces and all line feeds
|
||||
# -remove-log: remove all console.* statements
|
||||
# -print-namecache: print the name cache that contains the mapping from the old names to new names
|
||||
# -apply-namecache: reuse the given cache file
|
||||
|
||||
# Keep options:
|
||||
# -keep-property-name: specifies property names that you want to keep
|
||||
# -keep-global-name: specifies names that you want to keep in the global scope
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"license": "",
|
||||
"devDependencies": {},
|
||||
"author": "",
|
||||
"name": "entry",
|
||||
"description": "Please describe the basic information.",
|
||||
"main": "",
|
||||
"version": "1.0.0",
|
||||
"dynamicDependencies": {},
|
||||
"dependencies": {
|
||||
"@ohos/hypium": "1.0.18",
|
||||
"@kernel/theme_manager": "1.0.2-beta.17",
|
||||
"biz_common": "file:../biz_common",
|
||||
"biz_forecast": "file:../biz_forecast",
|
||||
"biz_option": "file:../biz_option",
|
||||
"biz_indicator": "file:../biz_indicator",
|
||||
"@b2c-f/billparsemodule": "1.0.0",
|
||||
"@b2c-f/accountanalysis": "1.3.0",
|
||||
"biz_debug": "file:../biz_debug",
|
||||
"biz_tradecondition": "file:../biz_tradecondition",
|
||||
"service_aigroup": "file:../service_aigroup",
|
||||
"@b2c-f/lib_fuhm_theme_res": "1.0.9-alpha.2",
|
||||
"@kernel/ohos_leak_watcher": "1.0.0-beta.8"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import identifier from '@ohos.identifier.oaid';
|
||||
import hilog from '@ohos.hilog';
|
||||
import { BusinessError } from '@ohos.base';
|
||||
|
||||
export class DeviceInfoHolder {
|
||||
|
||||
init() {
|
||||
try {
|
||||
identifier.getOAID().then((data) => {
|
||||
const oaid: string = data;
|
||||
hilog.info(0x0000, 'mhy', '%{public}s', `get oaid by promise success, oaid: ${oaid}`);
|
||||
}).catch((err: BusinessError) => {
|
||||
hilog.info(0x0000, 'mhy', '%{public}s', `get oaid by promise failed, code: ${err.code}, message: ${err.message}`);
|
||||
})
|
||||
|
||||
} catch(err) {
|
||||
let e: BusinessError = err as BusinessError;
|
||||
console.error('createDeviceManager errCode:' + e.code + ',errMessage:' + e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,301 @@
|
||||
/**
|
||||
* 交易日志转换管理器
|
||||
* 负责将推送数据库中的数据转换到交易日志数据库
|
||||
*/
|
||||
|
||||
import { TradeDailyPushDataManager } from './TradeDailyPushDataManager';
|
||||
import { TradeDailyPushModel } from './TradeDailyPushModel';
|
||||
import { TradeDailyDataManager, TradeDailyModel, TradeMsgUniqueIdManager } from 'biz_trade';
|
||||
import { PushType } from 'biz_trade/src/main/ets/service/push/PushType';
|
||||
import { AppUtil } from '@pura/harmony-utils';
|
||||
import { HXLog } from 'biz_common';
|
||||
import { CodeTableManager } from 'lib_codetablemanager';
|
||||
import { AbilityConstant, ApplicationStateChangeCallback, common, Context } from '@kit.AbilityKit';
|
||||
|
||||
const TAG = 'TradeDailyConvertManager';
|
||||
|
||||
/**
|
||||
* 解析结果接口
|
||||
*/
|
||||
interface ParseResult {
|
||||
dailyTitle: string;
|
||||
dailyContent: string;
|
||||
dailyTime: string;
|
||||
}
|
||||
|
||||
export class TradeDailyConvertManager {
|
||||
private static instance: TradeDailyConvertManager;
|
||||
|
||||
private applicationStateChangeCallBack: ApplicationStateChangeCallback = {
|
||||
onApplicationForeground: (): void => {
|
||||
this.convertDailyFromPush();
|
||||
},
|
||||
onApplicationBackground: (): void => {
|
||||
}
|
||||
}
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
static getInstance(): TradeDailyConvertManager {
|
||||
if (!TradeDailyConvertManager.instance) {
|
||||
TradeDailyConvertManager.instance = new TradeDailyConvertManager();
|
||||
}
|
||||
return TradeDailyConvertManager.instance;
|
||||
}
|
||||
|
||||
async registerForeground(context: Context): Promise<void> {
|
||||
context.getApplicationContext().on('applicationStateChange', this.applicationStateChangeCallBack)
|
||||
await this.convertDailyFromPush();
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用进入前台时转换推送数据
|
||||
* 将推送数据库中的数据转换到交易日志数据库
|
||||
*/
|
||||
async convertDailyFromPush(): Promise<void> {
|
||||
try {
|
||||
HXLog.i(TAG, '开始转换推送数据到交易日志');
|
||||
|
||||
// 查询所有推送日志
|
||||
const pushDailys = await TradeDailyPushDataManager.getInstance().queryDaily();
|
||||
|
||||
if (pushDailys.length === 0) {
|
||||
HXLog.i(TAG, '没有需要转换的推送数据');
|
||||
return;
|
||||
}
|
||||
|
||||
HXLog.i(TAG, `查询到 ${pushDailys.length} 条推送数据`);
|
||||
|
||||
// 转换每条推送日志
|
||||
for (const pushDaily of pushDailys) {
|
||||
// 过滤放弃行权的推送(单号不变,无法去重)
|
||||
if (this.shouldSkipPushDaily(pushDaily)) {
|
||||
HXLog.i(TAG, `跳过放弃行权推送: ${pushDaily.body}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.convertPushDailyToTradeDaily(pushDaily);
|
||||
}
|
||||
|
||||
// 删除已转换的推送日志
|
||||
await TradeDailyPushDataManager.getInstance().deleteDaily(pushDailys);
|
||||
HXLog.i(TAG, '推送数据转换完成');
|
||||
} catch (error) {
|
||||
HXLog.e(TAG, `转换推送数据失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否应该跳过该推送日志
|
||||
* @param pushDaily 推送日志
|
||||
*/
|
||||
private shouldSkipPushDaily(pushDaily: TradeDailyPushModel): boolean {
|
||||
const type = pushDaily.type
|
||||
|
||||
// 放弃行权push不记录(单号不变,无法去重)
|
||||
const abandonExercise = AppUtil.getContext().resourceManager.getStringSync($r('app.string.trade_daily_abandon_exercise'));
|
||||
if ((type == PushType.TY_OPTION_EXEC_WEITUO ||
|
||||
type == PushType.TY_OPTION_EXEC_CHEDAN) &&
|
||||
pushDaily.body.includes(abandonExercise)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将推送日志转换为交易日志
|
||||
* @param pushDaily 推送日志
|
||||
*/
|
||||
private async convertPushDailyToTradeDaily(pushDaily: TradeDailyPushModel): Promise<void> {
|
||||
try {
|
||||
const isSame = await TradeMsgUniqueIdManager.getInstance().isSameIdentify(
|
||||
pushDaily.time,
|
||||
pushDaily.iconType,
|
||||
pushDaily.type,
|
||||
pushDaily.orderNo
|
||||
);
|
||||
|
||||
if (isSame) {
|
||||
HXLog.i(TAG, `推送已存在,跳过: ${pushDaily.tradeDate} ${pushDaily.time}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const body = pushDaily.body;
|
||||
const title = pushDaily.title;
|
||||
|
||||
// 解析推送内容
|
||||
const parseResult = await this.parsePushContent(body, title, pushDaily);
|
||||
const dailyTitle = parseResult.dailyTitle;
|
||||
const dailyContent = parseResult.dailyContent;
|
||||
const dailyTime = parseResult.dailyTime;
|
||||
|
||||
// 创建交易日志模型
|
||||
const dailyModel = TradeDailyModel.createWithDateTime(
|
||||
pushDaily.tradeDate,
|
||||
dailyTime,
|
||||
dailyTitle,
|
||||
dailyContent
|
||||
);
|
||||
|
||||
// 插入到所有匹配的账户表中
|
||||
await this.insertToMatchingAccounts(dailyModel, pushDaily.tradeNo, pushDaily.qsid);
|
||||
|
||||
HXLog.i(TAG, `转换推送日志成功: ${pushDaily.tradeDate} ${dailyTime} ${dailyTitle}`);
|
||||
} catch (error) {
|
||||
HXLog.e(TAG, `转换推送日志失败: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析推送内容
|
||||
* @param body 推送内容
|
||||
* @param title 推送标题
|
||||
* @param pushDaily 推送日志
|
||||
*/
|
||||
private async parsePushContent(body: string, title: string, pushDaily: TradeDailyPushModel): Promise<ParseResult> {
|
||||
let dailyTitle = '';
|
||||
let dailyContent = '';
|
||||
let dailyTime = pushDaily.tradeTime;
|
||||
|
||||
const iconType = pushDaily.iconType;
|
||||
const orderNo = this.getOrderNo(pushDaily);
|
||||
const orderNoPrefix = AppUtil.getContext().resourceManager.getStringSync($r('app.string.trade_daily_order_no_prefix_with_comma'));
|
||||
const transactionReport = AppUtil.getContext().resourceManager.getStringSync($r('app.string.trade_daily_transaction_report'));
|
||||
|
||||
// 错误类型处理(iconType == 3 表示错误)
|
||||
if (iconType == '3') {
|
||||
dailyContent = body;
|
||||
dailyTime = pushDaily.tradeTime;
|
||||
|
||||
// 提取时间 [15:01:30]
|
||||
const firstIndex = body.indexOf('[');
|
||||
const lastIndex = body.indexOf(']');
|
||||
if (firstIndex > -1 && lastIndex > -1) {
|
||||
const timeStr = body.substring(firstIndex + 1, lastIndex);
|
||||
dailyTime = timeStr.replace(/:/g, '');
|
||||
// 从 body 末��减去时间部分(包括括号)
|
||||
dailyContent = body.substring(0, body.length - timeStr.length - 4);
|
||||
}
|
||||
|
||||
if (orderNo.length > 0) {
|
||||
dailyContent += orderNoPrefix + orderNo;
|
||||
}
|
||||
} else {
|
||||
// 正常类型处理
|
||||
const firstIndex = body.indexOf('(');
|
||||
const lastIndex = body.indexOf(')');
|
||||
|
||||
if (firstIndex > -1 && lastIndex > -1) {
|
||||
// 标题处理
|
||||
if (!title.includes(transactionReport)) {
|
||||
dailyTitle += title;
|
||||
}
|
||||
dailyTitle += body.substring(0, firstIndex);
|
||||
|
||||
// 内容处理
|
||||
const alertContent = body.substring(firstIndex + 1, lastIndex);
|
||||
const alertContentArr = alertContent.split(',');
|
||||
|
||||
if (alertContentArr.length > 0) {
|
||||
const tradeCode = alertContentArr[0];
|
||||
const stockInfo = await CodeTableManager.get().getStockInfoByTradeCode(tradeCode);
|
||||
const stockName = stockInfo?.stockName;
|
||||
|
||||
if (stockName && stockName.length > 0) {
|
||||
// 将合约代码替换为合约名
|
||||
dailyContent += stockName;
|
||||
for (let i = 1; i < alertContentArr.length; i++) {
|
||||
dailyContent += ',' + alertContentArr[i];
|
||||
}
|
||||
} else {
|
||||
dailyContent = alertContent;
|
||||
}
|
||||
} else {
|
||||
dailyContent = alertContent;
|
||||
}
|
||||
|
||||
if (orderNo.length > 0) {
|
||||
dailyContent += orderNoPrefix + orderNo;
|
||||
}
|
||||
|
||||
// 时间处理
|
||||
// iOS: timeFromBody = body.substring(loc: last + 2, len: body.count - last - 3)
|
||||
dailyTime = pushDaily.tradeTime;
|
||||
if (dailyTime.length === 0 && lastIndex + 2 < body.length && body.length - lastIndex - 3 > 0) {
|
||||
const startPos = lastIndex + 2;
|
||||
const length = body.length - lastIndex - 3;
|
||||
dailyTime = body.substring(startPos, startPos + length);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移除时间中的冒号
|
||||
dailyTime = dailyTime.replace(/:/g, '');
|
||||
|
||||
return { dailyTitle, dailyContent, dailyTime };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单号
|
||||
* @param pushDaily 推送日志
|
||||
*/
|
||||
private getOrderNo(pushDaily: TradeDailyPushModel): string {
|
||||
const type = pushDaily.type;
|
||||
let orderNo = '';
|
||||
|
||||
// 期货成交2,期权成交8,需要用orderSysId
|
||||
if (type == PushType.TY_JIAOYI_CHENGGONG || type == PushType.TY_OPTION_CHEGNJIAO) {
|
||||
orderNo = pushDaily.orderSysId || pushDaily.orderNo;
|
||||
}
|
||||
// 条件单触发,preOrderNo有值则是附加条件单,用母单编号
|
||||
else if (type == PushType.TY_CONDITION && pushDaily.preOrderNo) {
|
||||
orderNo = pushDaily.preOrderNo;
|
||||
}
|
||||
else {
|
||||
orderNo = pushDaily.orderNo;
|
||||
}
|
||||
|
||||
// 移除空格
|
||||
return orderNo.replace(/\s/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入到所有匹配的账户表中
|
||||
* @param model 交易日志模型
|
||||
* @param accountId 账户ID
|
||||
* @param qsId 券商ID
|
||||
*/
|
||||
private async insertToMatchingAccounts(
|
||||
model: TradeDailyModel,
|
||||
accountId: string,
|
||||
qsId: string
|
||||
): Promise<void> {
|
||||
try {
|
||||
// 获取所有表名
|
||||
const tableNames = await TradeDailyDataManager.getInstance().getAllTableNames();
|
||||
|
||||
// 匹配 trade_daily_{accountId}_{qsId}_{wtId} 格式
|
||||
const prefix = `trade_daily_${accountId}_${qsId}_`;
|
||||
|
||||
for (const tableName of tableNames) {
|
||||
if (tableName.startsWith(prefix)) {
|
||||
const wtId = tableName.substring(prefix.length);
|
||||
await TradeDailyDataManager.getInstance().insertDailyToAccount(
|
||||
model,
|
||||
accountId,
|
||||
qsId,
|
||||
wtId
|
||||
);
|
||||
HXLog.i(TAG, `插入到账户表: ${tableName}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
HXLog.e(TAG, `插入到匹配账户失败: ${error}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,282 @@
|
||||
/**
|
||||
* 交易推送日志数据管理器
|
||||
*/
|
||||
|
||||
import relationalStore from '@ohos.data.relationalStore';
|
||||
import { TradeDailyPushModel } from './TradeDailyPushModel';
|
||||
import { BusinessError } from '@ohos.base';
|
||||
import hilog from '@ohos.hilog';
|
||||
|
||||
const DB_NAME = 'trade_push_daily.db';
|
||||
const TAG = 'TradeDailyPushDataManager'
|
||||
const DOMAIN = 0xFF01
|
||||
const FORMAT = '%{public}s'
|
||||
|
||||
export class TradeDailyPushDataManager {
|
||||
private static instance: TradeDailyPushDataManager;
|
||||
private rdbStore: relationalStore.RdbStore | null = null;
|
||||
private isInitialized: boolean = false;
|
||||
|
||||
private constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单例实例
|
||||
*/
|
||||
static getInstance(): TradeDailyPushDataManager {
|
||||
if (!TradeDailyPushDataManager.instance) {
|
||||
TradeDailyPushDataManager.instance = new TradeDailyPushDataManager();
|
||||
}
|
||||
return TradeDailyPushDataManager.instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化数据库
|
||||
*/
|
||||
async initDB(context: ESObject): Promise<void> {
|
||||
if (this.isInitialized && this.rdbStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const config: relationalStore.StoreConfig = {
|
||||
name: DB_NAME,
|
||||
securityLevel: relationalStore.SecurityLevel.S1,
|
||||
customDir: 'trade'
|
||||
};
|
||||
|
||||
this.rdbStore = await relationalStore.getRdbStore(context, config);
|
||||
await this.createTable();
|
||||
this.isInitialized = true;
|
||||
} catch (err) {
|
||||
const error = err as BusinessError;
|
||||
hilog.error(DOMAIN, TAG, FORMAT, `初始化数据库失败: ${error.code}, ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建表
|
||||
*/
|
||||
private async createTable(): Promise<void> {
|
||||
if (!this.rdbStore) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const sql = TradeDailyPushModel.getTableSQL();
|
||||
await this.rdbStore.executeSql(sql);
|
||||
} catch (err) {
|
||||
const error = err as BusinessError;
|
||||
hilog.error(DOMAIN, TAG, FORMAT, `创建表失败: ${error.code}, ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入数据
|
||||
*/
|
||||
async insertDaily(daily: TradeDailyPushModel): Promise<void> {
|
||||
if (!this.rdbStore) {
|
||||
hilog.error(DOMAIN, TAG, FORMAT, '数据库未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const valueBucket = daily.toDbRecord();
|
||||
await this.rdbStore.insert(TradeDailyPushModel.getTableName(), valueBucket);
|
||||
hilog.info(DOMAIN, TAG, FORMAT, '插入数据成功')
|
||||
} catch (err) {
|
||||
const error = err as BusinessError;
|
||||
hilog.error(DOMAIN, TAG, FORMAT, `插入数据失败: ${error.code}, ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除数据
|
||||
*/
|
||||
async deleteDaily(models: TradeDailyPushModel[]): Promise<void> {
|
||||
if (!this.rdbStore) {
|
||||
hilog.error(DOMAIN, TAG, FORMAT, '数据库未初始化');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const tableName = TradeDailyPushModel.getTableName();
|
||||
|
||||
for (const model of models) {
|
||||
if (!model.uniqueId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const predicates = new relationalStore.RdbPredicates(tableName);
|
||||
predicates.equalTo('uniqueId', model.uniqueId);
|
||||
await this.rdbStore.delete(predicates);
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as BusinessError;
|
||||
hilog.error(DOMAIN, TAG, FORMAT, `删除数据失败: ${error.code}, ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询所有数据
|
||||
*/
|
||||
async queryDaily(): Promise<TradeDailyPushModel[]> {
|
||||
if (!this.rdbStore) {
|
||||
hilog.error(DOMAIN, TAG, FORMAT, '数据库未初始化');
|
||||
return [];
|
||||
}
|
||||
|
||||
let resultSet: relationalStore.ResultSet | null = null;
|
||||
try {
|
||||
const tableName = TradeDailyPushModel.getTableName();
|
||||
const predicates = new relationalStore.RdbPredicates(tableName);
|
||||
resultSet = await this.rdbStore.query(predicates, TradeDailyPushModel.getColumns());
|
||||
|
||||
const models: TradeDailyPushModel[] = [];
|
||||
|
||||
if (resultSet.rowCount === 0) {
|
||||
return models;
|
||||
}
|
||||
|
||||
resultSet.goToFirstRow();
|
||||
do {
|
||||
const record: relationalStore.ValuesBucket = {};
|
||||
const columns = TradeDailyPushModel.getColumns();
|
||||
|
||||
for (const column of columns) {
|
||||
const columnIndex = resultSet.getColumnIndex(column);
|
||||
if (columnIndex >= 0) {
|
||||
record[column] = resultSet.getString(columnIndex);
|
||||
}
|
||||
}
|
||||
|
||||
const model = TradeDailyPushModel.fromDbRecord(record);
|
||||
models.push(model);
|
||||
} while (resultSet.goToNextRow());
|
||||
|
||||
return models;
|
||||
} catch (err) {
|
||||
const error = err as BusinessError;
|
||||
hilog.error(DOMAIN, TAG, FORMAT, `查询数据失败: ${error.code}, ${error.message}`);
|
||||
return [];
|
||||
} finally {
|
||||
resultSet?.close();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 pushInfo 转换为 TradeDailyPushModel
|
||||
* @param pushInfo 推送信息
|
||||
* @returns 转换后的模型,若转换失败则返回 null
|
||||
*/
|
||||
convertToTradeDailyPushModel(pushInfo: ESObject): TradeDailyPushModel | null {
|
||||
const body = pushInfo['DESC'] as string;
|
||||
const msg = pushInfo['APPMSG'] as Map<string, object>;
|
||||
|
||||
if (!msg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const tradeNo = msg['tradeno'] as string;
|
||||
const qsid = msg['qsid'] as string;
|
||||
|
||||
if (!tradeNo || !qsid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const model = new TradeDailyPushModel();
|
||||
model.body = body ?? '';
|
||||
model.tradeNo = tradeNo;
|
||||
model.qsid = qsid;
|
||||
|
||||
const orderNo = msg['orderNumber'] as string;
|
||||
if (orderNo) {
|
||||
model.orderNo = orderNo;
|
||||
}
|
||||
|
||||
// 期货成交2,期权成交8,需要用orderSysId
|
||||
const orderSysId = msg['orderSysId'] as string;
|
||||
if (orderSysId) {
|
||||
model.orderSysId = orderSysId;
|
||||
}
|
||||
|
||||
// 条件单触发, preOrderNo有值则是附加条件单,用母单编号
|
||||
const preOrderNo = msg['preOrderNumber'] as string;
|
||||
if (preOrderNo) {
|
||||
model.preOrderNo = preOrderNo;
|
||||
}
|
||||
|
||||
const uniqueid = msg['uniqueid'] as string;
|
||||
if (uniqueid) {
|
||||
model.uniqueId = uniqueid;
|
||||
}
|
||||
|
||||
const tradeTime = msg['ST2'] as string;
|
||||
if (tradeTime) {
|
||||
model.tradeTime = tradeTime;
|
||||
model.time = tradeTime;
|
||||
}
|
||||
|
||||
// 日期用收到通知的日期
|
||||
const now = new Date();
|
||||
model.tradeDate = this.formatDate(now, 'yyyyMMdd');
|
||||
|
||||
const iconType = msg['IC'] as string;
|
||||
if (iconType) {
|
||||
model.iconType = iconType;
|
||||
}
|
||||
|
||||
const type = msg['TY'] as string;
|
||||
if (type) {
|
||||
model.type = type;
|
||||
// 期货撤单3、期权撤单9、期权行权撤单52,这几种后端给的委托时间,不对,需要用收到回报的时间
|
||||
const typeNum = parseInt(type);
|
||||
if (typeNum === 3 || typeNum === 9 || typeNum === 52) {
|
||||
model.tradeTime = this.formatDate(now, 'HH:mm:ss');
|
||||
// 推送中的已撤单给过滤了,和标题重复,产品不想要
|
||||
model.body = body.replace(',已撤单', '');
|
||||
}
|
||||
}
|
||||
|
||||
const title = msg['ST'] as string;
|
||||
if (title) {
|
||||
model.title = title;
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录推送日志
|
||||
*/
|
||||
async recordPushDaily(model: TradeDailyPushModel): Promise<void> {
|
||||
hilog.info(DOMAIN, TAG, FORMAT, `[daily] notification-extension push daily: ${model.tradeDate} ${model.time} ${model.title} ${model.body}`);
|
||||
|
||||
const type = parseInt(model.type);
|
||||
// 过滤条件单(5)和盈亏(6)类型
|
||||
if (type === 5 || type === 6) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.insertDaily(model);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
private formatDate(date: Date, format: string): string {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
|
||||
if (format === 'yyyyMMdd') {
|
||||
return `${year}${month}${day}`;
|
||||
} else if (format === 'HH:mm:ss') {
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* 交易推送日报数据模型
|
||||
* Created by wenzhe on 2022/6/6.
|
||||
* Copyright © 2022 hexin. All rights reserved.
|
||||
*/
|
||||
|
||||
import relationalStore from '@ohos.data.relationalStore';
|
||||
|
||||
export class TradeDailyPushModel {
|
||||
/** 交易账号 */
|
||||
tradeNo: string = '';
|
||||
/** qsId */
|
||||
qsid: string = '';
|
||||
/** 订单号 */
|
||||
orderNo: string = '';
|
||||
/** 订单号(已成交) */
|
||||
orderSysId: string = '';
|
||||
/** 母单号(附加条件单) */
|
||||
preOrderNo: string = '';
|
||||
/** 消息唯一号 */
|
||||
uniqueId: string = '';
|
||||
/** 交易类型 */
|
||||
type: string = '';
|
||||
/** 交易时间(app显示) */
|
||||
tradeTime: string = '';
|
||||
/** 交易时间(后端推的时间) */
|
||||
time: string = '';
|
||||
/** 交易日期 */
|
||||
tradeDate: string = '';
|
||||
/** 标题 */
|
||||
title: string = '';
|
||||
/** 消息内容 */
|
||||
body: string = '';
|
||||
/** icon类型 */
|
||||
iconType: string = '';
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表名
|
||||
*/
|
||||
static getTableName(): string {
|
||||
return 'trade_push_daily';
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取表结构SQL
|
||||
*/
|
||||
static getTableSQL(): string {
|
||||
return `CREATE TABLE IF NOT EXISTS ${TradeDailyPushModel.getTableName()} (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
body TEXT,
|
||||
tradeNo TEXT,
|
||||
qsid TEXT,
|
||||
orderNo TEXT,
|
||||
orderSysId TEXT,
|
||||
preOrderNo TEXT,
|
||||
uniqueId TEXT,
|
||||
type TEXT,
|
||||
tradeTime TEXT,
|
||||
time TEXT,
|
||||
tradeDate TEXT,
|
||||
title TEXT,
|
||||
iconType TEXT
|
||||
)`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有列名
|
||||
*/
|
||||
static getColumns(): string[] {
|
||||
return [
|
||||
'id',
|
||||
'body',
|
||||
'tradeNo',
|
||||
'qsid',
|
||||
'orderNo',
|
||||
'orderSysId',
|
||||
'preOrderNo',
|
||||
'uniqueId',
|
||||
'type',
|
||||
'tradeTime',
|
||||
'time',
|
||||
'tradeDate',
|
||||
'title',
|
||||
'iconType'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数据库记录转换为模型对象
|
||||
*/
|
||||
static fromDbRecord(record: relationalStore.ValuesBucket): TradeDailyPushModel {
|
||||
const model = new TradeDailyPushModel();
|
||||
model.body = (record['body'] as string) ?? '';
|
||||
model.tradeNo = (record['tradeNo'] as string) ?? '';
|
||||
model.qsid = (record['qsid'] as string) ?? '';
|
||||
model.orderNo = (record['orderNo'] as string) ?? '';
|
||||
model.orderSysId = (record['orderSysId'] as string) ?? '';
|
||||
model.preOrderNo = (record['preOrderNo'] as string) ?? '';
|
||||
model.uniqueId = (record['uniqueId'] as string) ?? '';
|
||||
model.type = (record['type'] as string) ?? '';
|
||||
model.tradeTime = (record['tradeTime'] as string) ?? '';
|
||||
model.time = (record['time'] as string) ?? '';
|
||||
model.tradeDate = (record['tradeDate'] as string) ?? '';
|
||||
model.title = (record['title'] as string) ?? '';
|
||||
model.iconType = (record['iconType'] as string) ?? '';
|
||||
return model;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数据库记录
|
||||
*/
|
||||
toDbRecord(): relationalStore.ValuesBucket {
|
||||
const record: relationalStore.ValuesBucket = {};
|
||||
record['body'] = this.body;
|
||||
record['tradeNo'] = this.tradeNo;
|
||||
record['qsid'] = this.qsid;
|
||||
record['orderNo'] = this.orderNo;
|
||||
record['orderSysId'] = this.orderSysId;
|
||||
record['preOrderNo'] = this.preOrderNo;
|
||||
record['uniqueId'] = this.uniqueId;
|
||||
record['type'] = this.type;
|
||||
record['tradeTime'] = this.tradeTime;
|
||||
record['time'] = this.time;
|
||||
record['tradeDate'] = this.tradeDate;
|
||||
record['title'] = this.title;
|
||||
record['iconType'] = this.iconType;
|
||||
return record;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { HXLog, PreferenceService } from 'biz_common';
|
||||
import { JSBaseEvent } from 'biz_webview';
|
||||
import { common } from '@kit.AbilityKit';
|
||||
|
||||
/**
|
||||
* 撤回同意隐私政策协议
|
||||
*/
|
||||
export class PrivacyWithDrawEvent extends JSBaseEvent {
|
||||
|
||||
getName(): string {
|
||||
return "privacyWithdraw";
|
||||
}
|
||||
|
||||
getHandler(): Function {
|
||||
return (data: object, handler: Function) => {
|
||||
//TODO这里提了ir单确认能否改成自定义弹窗,否则UI效果不统一
|
||||
AlertDialog.show(
|
||||
{
|
||||
title: '撤回同意将退出同花顺,我们无法再为您提供服务',
|
||||
//subtitle: 'subtitle',
|
||||
message: '',
|
||||
autoCancel: true,
|
||||
alignment: DialogAlignment.Center,
|
||||
gridCount: 4,
|
||||
showInSubWindow: true,
|
||||
isModal: true,
|
||||
offset: { dx: 0, dy: -20 },
|
||||
buttonDirection: DialogButtonDirection.HORIZONTAL,
|
||||
buttons: [
|
||||
{
|
||||
value: '撤回并退出',
|
||||
action: () => {
|
||||
PreferenceService.putVal('Privacy', 'isUserAgree', false, true);
|
||||
(getContext(this) as common.UIAbilityContext).terminateSelf(() => {});
|
||||
}
|
||||
},
|
||||
{
|
||||
value: '继续使用',
|
||||
action: () => {
|
||||
|
||||
}
|
||||
},
|
||||
],
|
||||
cancel: () => {
|
||||
console.info('Closed callbacks')
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dialogController: CustomDialogController = new CustomDialogController({
|
||||
builder: CustomDialogExample({
|
||||
cancel: ()=> { this.onCancel() },
|
||||
confirm: ()=> { this.onAccept() },
|
||||
}),
|
||||
})
|
||||
|
||||
onCancel() {
|
||||
console.info('Callback when the first button is clicked')
|
||||
}
|
||||
|
||||
onAccept() {
|
||||
console.info('Callback when the second button is clicked')
|
||||
}
|
||||
}
|
||||
|
||||
@CustomDialog
|
||||
struct CustomDialogExample {
|
||||
cancel?: () => void
|
||||
confirm?: () => void
|
||||
controller: CustomDialogController
|
||||
|
||||
build() {
|
||||
Column() {
|
||||
Text('我是内容').fontSize(20).margin({ top: 10, bottom: 10 })
|
||||
Flex({ justifyContent: FlexAlign.SpaceAround }) {
|
||||
Button('cancel')
|
||||
.onClick(() => {
|
||||
this.controller.close()
|
||||
if (this.cancel) {
|
||||
this.cancel()
|
||||
}
|
||||
}).backgroundColor(0xffffff).fontColor(Color.Black)
|
||||
Button('confirm')
|
||||
.onClick(() => {
|
||||
this.controller.close()
|
||||
if (this.confirm) {
|
||||
this.confirm()
|
||||
}
|
||||
}).backgroundColor(0xffffff).fontColor(Color.Red)
|
||||
}.margin({ bottom: 10 })
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import BackupExtensionAbility, {BundleVersion} from '@ohos.application.BackupExtensionAbility';
|
||||
import MmkvTransferManager from './MMKVManager'
|
||||
|
||||
const TAG = `FileBackupExtensionAbility`;
|
||||
export default class BackupExtension extends BackupExtensionAbility {
|
||||
async onBackup () {
|
||||
console.info(TAG, `onBackup ok`);
|
||||
}
|
||||
|
||||
async onRestore (bundleVersion : BundleVersion) {
|
||||
console.info(TAG, `onRestore ok ${JSON.stringify(bundleVersion)}`);
|
||||
console.info(TAG, `onRestore end`);
|
||||
new MmkvTransferManager().transfer(this.context)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
import common from '@ohos.app.ability.common';
|
||||
import fs from '@ohos.file.fs';
|
||||
import { MMKV} from '@ohos/mmkv'
|
||||
import dataPreferences from '@ohos.data.preferences';
|
||||
|
||||
// mmkv值类型定义
|
||||
|
||||
type MmkvValueType = number | Set<string> | string | boolean;
|
||||
|
||||
/**
|
||||
* 双->单 mmkv文件迁移转换管理
|
||||
*
|
||||
* @since 2024-04-20
|
||||
*/
|
||||
export default class MmkvTransferManager {
|
||||
/**
|
||||
* 双->单 mmkv文件转换
|
||||
*当前例子中:
|
||||
*
|
||||
XXAJmmkvX177): source_mmkv source_
|
||||
mmkv. crc
|
||||
* 单的mmkv文件名为:target_
|
||||
mmkv
|
||||
*
|
||||
* @param context context上下文
|
||||
*/
|
||||
public async transfer(context: common.Context): Promise<void> {
|
||||
//第1步,move双框架mmkv文件到单框架指定目录
|
||||
let result = await this.moveMmkvFile(context);
|
||||
console.log("FileBackupExtensionAbility", "startMove")
|
||||
if (!result) {
|
||||
console.log("FileBackupExtensionAbility", "moveFailed")
|
||||
return;
|
||||
}
|
||||
// 第2步,解析双的mmkv的内容
|
||||
let dataMap = this.parseSourceMmkvInfo(context);
|
||||
console.log("FileBackupExtensionAbility", "dataMap" + JSON.stringify(dataMap.get("stringData")));
|
||||
// 第3步,将解析的双的mmkv中的内容保存到单的mmkv中
|
||||
if (!this.insertToTargetMmkv(context, dataMap)) {
|
||||
console.log("FileBackupExtensionAbility", "failed to insert target")
|
||||
// 数据插入失败的处理逻辑,自行实现
|
||||
return;
|
||||
}
|
||||
// 第4步,从单的mmkv中读取数据
|
||||
let resultMap = this.getDataFromTarget(context);
|
||||
let mmkvData = resultMap.get("legacyUserData")
|
||||
if (typeof mmkvData === 'string') {
|
||||
|
||||
// // 新建并打开文件
|
||||
// let file = fs.openSync(context.filesDir + '/userinfo.txt', fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE);
|
||||
// // 写入一段内容至文件
|
||||
// fs.writeSync(file.fd, mmkvData as string);
|
||||
|
||||
let options: dataPreferences.Options = { name: 'backupUserInfo' };
|
||||
let preferences = dataPreferences.getPreferencesSync(context.getApplicationContext(), options);
|
||||
preferences.putSync('data', mmkvData as string);
|
||||
preferences.flush()
|
||||
}
|
||||
// 处理查询结果,根据情况自行实现
|
||||
console.log("FileBackupExtensionAbility", "result" + JSON.stringify(resultMap.get("stringData")))
|
||||
}
|
||||
|
||||
//双mmkv文件搬迁
|
||||
/**
|
||||
* 移动双框架mmkv文件到单框架指定目录
|
||||
* 文件参考@link FileTransferManager.moveFiles
|
||||
*
|
||||
* @param context context上下文
|
||||
* @return 成功返回true, 否则false
|
||||
*/
|
||||
private async moveMmkvFile(context: common.Context): Promise<boolean> {
|
||||
try {
|
||||
if (fs.accessSync("/data/storage/el2/backup/restore/com.hexin.plat.android/ce/files/mmkv_migratable")) {
|
||||
fs.listFileSync("/data/storage/el2/backup/restore/com.hexin.plat.android/ce/files/mmkv_migratable", {
|
||||
recursion: true
|
||||
}).forEach(item => {
|
||||
console.log("FileBackupExtensionAbility", "name = " + item);
|
||||
})
|
||||
}
|
||||
// 双升单数据迁移后,mmkv默认在files/mmkv目录下
|
||||
let sourceMmkvPath: string = '/data/storage/el2/backup/restore/com.hexin.plat.android/ce/files/mmkv_migratable';
|
||||
if (!fs.accessSync(sourceMmkvPath)) {
|
||||
console.log("FileBackupExtensionAbility", "fsaccess failed")
|
||||
return false;
|
||||
}
|
||||
// 得到源路径下面所有的文件和目录
|
||||
let listFileNames: string[] = fs.listFileSync(sourceMmkvPath);
|
||||
if (!listFileNames || listFileNames.length === 0) {
|
||||
console.log("FileBackupExtensionAbility", "listFile empty")
|
||||
return false;
|
||||
}
|
||||
|
||||
// 单mmkv路径, 根据实际路径指定
|
||||
let targetPath: string = context.filesDir + '/mmkv';
|
||||
// 目标目录不存在,则需要创建
|
||||
if (!fs.accessSync(targetPath)) {
|
||||
fs.mkdirSync(targetPath);
|
||||
}
|
||||
|
||||
// 遍历源路径下面的文件或目录,进行移动
|
||||
listFileNames.forEach((fileName: string) => {
|
||||
let srcPath: string = `${sourceMmkvPath}/${fileName}`;
|
||||
let destPath: string = `${targetPath}/${fileName}`;
|
||||
if (fs.statSync(srcPath).isFile()) {
|
||||
fs.moveFileSync(srcPath, destPath);
|
||||
} else {
|
||||
fs.moveDirSync(srcPath, targetPath);
|
||||
}
|
||||
});
|
||||
return true;
|
||||
} catch (error) {
|
||||
// 异常处理逻辑,自行实现
|
||||
console.log("FileBackupExtensionAbility", "catch error")
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析双框架mmkv文件
|
||||
* 当前例子中:
|
||||
* 双的mmkv文件名为:source_mmkv, source_mmkv.crc
|
||||
* 其中设置的属性有:intData 整型, boolData bool型, stringData 字符串型,floatData 浮点型,doubleData 双精度浮点型, setData Set<string>类型
|
||||
* 单的mmkv文件名为:target_mmkv
|
||||
*
|
||||
* @param context context上下文
|
||||
* @return map 包含了双mmkv中的数据
|
||||
*/
|
||||
private parseSourceMmkvInfo(context: common.Context): Map<string, MmkvValueType> {
|
||||
let dataMap = new Map<string, MmkvValueType>();
|
||||
let mmkv: MMKV | null = null;
|
||||
try {
|
||||
// 初始化双的mmkv, mmkv文件移动到context.filesDir + '/mmkv' 目录下了
|
||||
MMKV.initialize(context.filesDir + '/mmkv', context.cacheDir + '/mmkv');
|
||||
// mmkv文件名
|
||||
let files = this.findDeseriredFile(context);
|
||||
let mmapID: string = files[0];
|
||||
console.log("FileBackupExtensionAbility", "mmapId = " + mmapID);
|
||||
// 秘钥,根据实际情况指定秘钥, 当前未设置秘钥
|
||||
let crpKey: string = '';
|
||||
mmkv = MMKV.getBackedUpMMKVWithID(mmapID, MMKV.SINGLE_PROCESS_MODE, crpKey, '');
|
||||
// 注:当前mmkv对于双框架mmkv设置的int long float的类型暂不支持,待后续更新
|
||||
dataMap.set('stringData', mmkv.decodeString('key_history_user_info', ''));
|
||||
dataMap.forEach((value, key, map) => {
|
||||
console.log("FileBackupExtensionAbility", "datamap key = " + key + "value = " + value.toString());
|
||||
})
|
||||
} catch (error) {
|
||||
// 异常处理逻辑,自行实现
|
||||
} finally {
|
||||
mmkv?.close();
|
||||
}
|
||||
return dataMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从mmkv_migratable目录中找到所有的文件名
|
||||
*/
|
||||
private findDeseriredFile(context: common.Context) : string[] {
|
||||
let result : string[] = [];
|
||||
let mmkvFilePath = context.filesDir + '/mmkv';
|
||||
let listFileNames: string[] = fs.listFileSync(mmkvFilePath);
|
||||
for (let index = 0; index < listFileNames.length; index++) {
|
||||
const element = listFileNames[index];
|
||||
console.log("FileBackupExtensionAbility", "mmkv files are = " + element)
|
||||
if (element.endsWith(".crc")) {
|
||||
const filePrefix = element.split(".crc")[0];
|
||||
result.push(filePrefix);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入数据到单的mmkv中
|
||||
*
|
||||
* @param context context上下文
|
||||
* @param dataMap 待插入的数据
|
||||
* @return 成功返回true, 否则false
|
||||
*/
|
||||
private insertToTargetMmkv(context: Context, dataMap: Map<string, MmkvValueType>): boolean {
|
||||
let mmkv: MMKV | null = null;
|
||||
try {
|
||||
// 初始化单框架mmkv, 获取mmkv的实例可以根据实际情况自行封装成工具类调用
|
||||
MMKV.initialize(context.filesDir + '/mmkv', context.cacheDir + '/mmkv');
|
||||
// mmkv文件名
|
||||
let mmapID: string = 'history_user_info';
|
||||
// 秘钥,根据实际情况指定秘钥, 当前未设置秘钥
|
||||
let crpKey: string = '';
|
||||
mmkv = MMKV.getMMKVWithMMapID(mmapID, MMKV.SINGLE_PROCESS_MODE, crpKey, '');
|
||||
// // 设置请求参数中的值
|
||||
mmkv.encodeString('history_user_info', dataMap.get('stringData') as string);
|
||||
} catch (error) {
|
||||
console.log("FileBackupExtensionAbility", "insertError" + JSON.stringify(error))
|
||||
// 异常处理逻辑,自行实现
|
||||
return false;
|
||||
} finally {
|
||||
mmkv?.close();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取单框架中mmkv中的数据
|
||||
*
|
||||
* @param context context上下文
|
||||
* @return map 包含单框架mmkv中的数据
|
||||
*/
|
||||
private getDataFromTarget(context: common.Context): Map<string, MmkvValueType> {
|
||||
let dataMap = new Map<string, MmkvValueType>();
|
||||
let mmkv: MMKV | null = null;
|
||||
try {
|
||||
// 初始化单框架mmkv
|
||||
MMKV.initialize(context.filesDir + '/mmkv', context.cacheDir + '/mmkv');
|
||||
// mmkv文件名
|
||||
let files = this.findDeseriredFile(context);
|
||||
let mmapID: string = "history_user_info";
|
||||
console.log("FileBackupExtensionAbility", "mmapId = " + mmapID);
|
||||
// 秘钥,根据实际情况指定秘钥, 当前未设置秘钥
|
||||
let crpKey: string = '';
|
||||
mmkv = MMKV.getMMKVWithMMapID(mmapID, MMKV.SINGLE_PROCESS_MODE, crpKey, '');
|
||||
|
||||
// 取值,根据mmkv中实际存在的key来取值
|
||||
dataMap.set('legacyUserData', mmkv.decodeString('history_user_info', ''));
|
||||
} catch (error) {
|
||||
// 异常处理逻辑,自行实现
|
||||
} finally {
|
||||
mmkv?.close();
|
||||
}
|
||||
return dataMap;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
|
||||
export const DEFAULT_CONNECT_SITE = '116.63.213.206';
|
||||
export const DEFAULT_CONNECT_SITE_PORT = 9528;
|
||||
|
||||
export const TEST_ENV_CONNECT_SITE = '121.52.252.9';
|
||||
export const TEST_ENV_CONNECT_SITE_PORT = 9628;
|
||||
|
||||
export const TEST_ENV_SITE = '101.71.41.211';
|
||||
export const TEST_ENV_PORT = 9628;
|
||||
@@ -0,0 +1,63 @@
|
||||
import { ConnectionServiceHelper, SiteEntity, SiteManager } from "@kernel/lib_communication";
|
||||
import { AppInfoConfig, PreferenceService } from "biz_common";
|
||||
import { DebugToolUtil } from "biz_debug";
|
||||
import { DEBUG_TOOL, ENABLE_FIX_SERVER,
|
||||
SERVER_FIX_ADDRESS,
|
||||
SERVER_FIX_PORT} from "biz_debug/src/main/ets/debug/servertool/constant";
|
||||
import WTInit from "biz_trade/src/main/ets/utils/WTInit";
|
||||
import BuildProfile from "BuildProfile";
|
||||
import { ConfigInitializer } from "../initialize/ConfigInitializor";
|
||||
import { TEST_ENV_PORT, TEST_ENV_SITE } from "./Config";
|
||||
import { ServiceHandlerImpl } from "./impl/ServiceHandlerImpl";
|
||||
import deviceInfo from "@ohos.deviceInfo";
|
||||
|
||||
export class ConnectionInitiator {
|
||||
|
||||
/**
|
||||
* 初始化连接库相关配置
|
||||
*/
|
||||
static initConnectionService(context : Context) {
|
||||
ConfigInitializer.init();
|
||||
|
||||
// SiteManager.getInstance().init(TEST_ENV_CONNECT_SITE, TEST_ENV_CONNECT_SITE_PORT)
|
||||
//SiteManager.getInstance().init(DEFAULT_CONNECT_SITE, DEFAULT_CONNECT_SITE_PORT)
|
||||
//SiteManager.getInstance().setDomainHost(AppInfoConfig.FUTURES_DOMAIN_HOST);
|
||||
|
||||
const initEntity = SiteManager.getInstance().getHQSiteEntity()
|
||||
initEntity?.setDomainHost(AppInfoConfig.FUTURES_DOMAIN_HOST)
|
||||
initEntity?.setSitePort(AppInfoConfig.FUTURES_HOST_PORT)
|
||||
if (DebugToolUtil.isInnerNetSimulateMode()) {
|
||||
// debug模拟器自动切内网环境
|
||||
ConnectionInitiator.updateServerOnDebugMode();
|
||||
}
|
||||
ConnectionInitiator.updateServerByDebugTool();
|
||||
ConnectionServiceHelper.getInstance().init(new ServiceHandlerImpl());
|
||||
WTInit.init(context.getApplicationContext());
|
||||
|
||||
}
|
||||
|
||||
private static updateServerOnDebugMode() {
|
||||
if (isDebugMode()) {
|
||||
let siteEntity = new SiteEntity();
|
||||
siteEntity.init(TEST_ENV_SITE, TEST_ENV_PORT);
|
||||
SiteManager.getInstance().updateHQSiteEntity(siteEntity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据调试工具保存的站点信息初始化行情站点
|
||||
*/
|
||||
private static updateServerByDebugTool() {
|
||||
if (PreferenceService.getBooleanValueSync(DEBUG_TOOL, ENABLE_FIX_SERVER, false)) {
|
||||
let siteEntity = new SiteEntity();
|
||||
const siteIp = PreferenceService.getStringValueSync(DEBUG_TOOL, SERVER_FIX_ADDRESS, TEST_ENV_SITE)
|
||||
const sitePort = PreferenceService.getNumberValueSync(DEBUG_TOOL, SERVER_FIX_PORT, TEST_ENV_PORT);
|
||||
siteEntity.init(siteIp, sitePort);
|
||||
SiteManager.getInstance().updateHQSiteEntity(siteEntity);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function isDebugMode() {
|
||||
return BuildProfile.BUILD_MODE_NAME.toString() === 'debug'
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import {
|
||||
CBASConnectionConfig,
|
||||
HqConnectionConfig,
|
||||
ITcpApmMonitorProxy,
|
||||
SocketConnection,
|
||||
TcpApmMonitorProxyHolder,
|
||||
TcpLogConnectionInfo
|
||||
} from '@kernel/lib_communication';
|
||||
import {
|
||||
IConnectionService,
|
||||
IServiceHandler,
|
||||
ServiceConfig
|
||||
} from '@kernel/lib_communication/src/main/ets/connection/IConnectionService';
|
||||
import hilog from '@ohos.hilog';
|
||||
// import TradeJsonStructObserver from 'biz_trade/src/main/ets/features/wtmodule/TradeJsonStructObserver'
|
||||
import { AuthJsonStructObserver } from '../interceptors/AuthJsonStructObserver';
|
||||
import { HXAuthManager } from 'biz_auth';
|
||||
import { RealDataStructObserver } from '../../groupsub/RealDataStructObserver';
|
||||
import { SelfCodeChangeObserver } from '../interceptors/SelfCodeChangeObserver';
|
||||
import { SidInstanceIdReceiver } from '../interceptors/SidInstanceIdReceiver';
|
||||
import { BaseTcpLogInfo, getTcpService, TCPConnectLogInfo } from '@kernel/app_monitor_tcp';
|
||||
import { DEFAULT_SITE } from '@kernel/lib_communication/src/main/ets/site/SiteConstants';
|
||||
import connection from '@ohos.net.connection';
|
||||
import { TradeELKLogStructObserver } from 'biz_trade';
|
||||
import { HXEventMonitor } from '@kernel/app_monitor_event';
|
||||
import { Action, HxCBASAgentProvider, logMapValue } from '@b2c-spi/hx_cbas_api';
|
||||
import { HXLog } from 'biz_common';
|
||||
|
||||
|
||||
export class ServiceHandlerImpl implements IServiceHandler {
|
||||
onConfig(serviceConfig: ServiceConfig): void {
|
||||
let hqConnectionConfig = new HqConnectionConfig();
|
||||
hqConnectionConfig.setAuth(new HXAuthManager());
|
||||
hqConnectionConfig.addStructReceiver(new SidInstanceIdReceiver());
|
||||
hqConnectionConfig.addStructReceiver(new AuthJsonStructObserver());
|
||||
// hqConnectionConfig.addStructReceiver(new TradeJsonStructObserver());
|
||||
hqConnectionConfig.addStructReceiver(new RealDataStructObserver());
|
||||
//hqConnectionConfig.addStructReceiver(new SelfCodeChangeObserver());
|
||||
serviceConfig.registerConnection(hqConnectionConfig);
|
||||
|
||||
let cbasConnectionConfig = new CBASConnectionConfig();
|
||||
cbasConnectionConfig.setAuth(new HXAuthManager());
|
||||
cbasConnectionConfig.addStructReceiver(new TradeELKLogStructObserver());
|
||||
serviceConfig.registerConnection(cbasConnectionConfig);
|
||||
|
||||
this.registerTcpApmProxy()
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册tcp的APM监控代理
|
||||
*/
|
||||
registerTcpApmProxy() {
|
||||
TcpApmMonitorProxyHolder.get().tcpLogMonitorProxy = new HXTcpApmMonitorProxy()
|
||||
}
|
||||
|
||||
onPreInit(connService: IConnectionService): void {
|
||||
initCommonConfig()
|
||||
}
|
||||
|
||||
onInitEnd(connService: IConnectionService): void {
|
||||
hilog.info(0xff11, "ServiceHandlerImpl", "onInitEnd")
|
||||
}
|
||||
|
||||
onDestroy(connService: IConnectionService): void {
|
||||
hilog.info(0xff11, "ServiceHandlerImpl", "onDestroy")
|
||||
}
|
||||
}
|
||||
|
||||
function initCommonConfig() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* tcp APM性能监控代理类
|
||||
*/
|
||||
class HXTcpApmMonitorProxy implements ITcpApmMonitorProxy {
|
||||
private TCP_LOG_MONITOR_MODULE = 'tcp-log';
|
||||
|
||||
/**
|
||||
* 开始解析dns
|
||||
* @param domainName
|
||||
*/
|
||||
dnsStart(domainName: string): void {
|
||||
const logMap: Map<string, logMapValue> = new Map()
|
||||
logMap.set('result', domainName)
|
||||
HxCBASAgentProvider.getInstance().doEvent("mob_systemgeneral_dnsresolve_start", Action.SHOW, logMap)
|
||||
HXLog.d("HXDNS", `dnsStart,domainName=${domainName}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* dns解析结束
|
||||
*/
|
||||
dnsEnd(isSuccess: boolean, domainName: string, ipList: connection.NetAddress[] | null, cost: number,
|
||||
errorMsg: string): void {
|
||||
const logMap: Map<string, logMapValue> = new Map()
|
||||
logMap.set('type', isSuccess ? "1" : "0")
|
||||
//dns解析结果
|
||||
const result = `domainName=${domainName}^ipList=${this.convertNetAddressToIpListString(ipList)}^cost=${cost}^errorMsg=${errorMsg}`
|
||||
logMap.set('result', result)
|
||||
HxCBASAgentProvider.getInstance().doEvent("mob_systemgeneral_dnsresolve_result", Action.SHOW, logMap)
|
||||
HXLog.d("HXDNS", `dnsEnd,success=${isSuccess},result=${result}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换ip列表字符串格式
|
||||
*/
|
||||
private convertNetAddressToIpListString(ipList: connection.NetAddress[] | null): string {
|
||||
if (ipList == null || ipList.length == 0) {
|
||||
return ""
|
||||
}
|
||||
|
||||
const ipArray = ipList.map((addr) => addr.address)
|
||||
//去重
|
||||
const uniqueIps = Array.from(new Set(ipArray))
|
||||
return uniqueIps.join(",")
|
||||
}
|
||||
|
||||
setSas(sas: string | null): void {
|
||||
getTcpService()?.setSas(sas)
|
||||
}
|
||||
|
||||
sendTcpConnectLog(info: TcpLogConnectionInfo): void {
|
||||
let cost = info.finishTime - info.startTime
|
||||
if (cost < 0) {
|
||||
cost = 0
|
||||
}
|
||||
//这里需要转换一下
|
||||
const logInfo = new TCPConnectLogInfo(
|
||||
cost, //连接耗时
|
||||
this.getTcpConnectResult(info.state, cost), //统一result字段
|
||||
this.getTcpConnectErrorCode(info.state, cost), //统一errorCode字段
|
||||
info.errorCode.toString(), //errorMsg
|
||||
0, //连接类型 目前没有竞速,默认0
|
||||
info.siteEntity.getSiteAddress(), //连接的服务器地址
|
||||
0, //网络栈,目前没有判断
|
||||
false, //是否ipv6
|
||||
false, //是否v6优先连接
|
||||
info.connectSeq, //站点类型
|
||||
0,
|
||||
info.siteEntity.getSiteAddress() === DEFAULT_SITE ? 1 : 0// 是否为默认站点
|
||||
)
|
||||
getTcpService()?.getTcpConnectLogMonitor()?.addUploadInfo(logInfo)
|
||||
}
|
||||
|
||||
getTcpConnectErrorCode(state: SocketConnection, cost: number): number | undefined {
|
||||
switch (state) {
|
||||
case SocketConnection.CONNECTED:
|
||||
return 4
|
||||
case SocketConnection.ERROR:
|
||||
if (cost > 10000) {
|
||||
return 10
|
||||
} else {
|
||||
return 8
|
||||
}
|
||||
default:
|
||||
return -1
|
||||
}
|
||||
}
|
||||
|
||||
getTcpConnectResult(state: SocketConnection, cost: number): string | null | undefined {
|
||||
switch (state) {
|
||||
case SocketConnection.CONNECTED:
|
||||
return BaseTcpLogInfo.SUCCESS
|
||||
case SocketConnection.ERROR:
|
||||
if (cost > 10000) {
|
||||
return BaseTcpLogInfo.TIMEOUT
|
||||
} else {
|
||||
return BaseTcpLogInfo.FAIL
|
||||
}
|
||||
default:
|
||||
return BaseTcpLogInfo.FAIL
|
||||
}
|
||||
}
|
||||
|
||||
buildRequest(frameId: number, packageId: number, traceId: string | null,
|
||||
pageIdMappingInstanceId: Map<number, number> | null): void {
|
||||
getTcpService()?.getTcpReqMonitor()?.buildRequest(frameId, packageId, traceId, pageIdMappingInstanceId)
|
||||
}
|
||||
|
||||
addRequestToQueue(packageId: number): void {
|
||||
getTcpService()?.getTcpReqMonitor()?.addRequestToQueue(packageId)
|
||||
}
|
||||
|
||||
sendBegin(packageId: number): void {
|
||||
getTcpService()?.getTcpReqMonitor()?.sendBegin(packageId)
|
||||
}
|
||||
|
||||
sendEnd(packageId: number): void {
|
||||
getTcpService()?.getTcpReqMonitor()?.sendEnd(packageId)
|
||||
}
|
||||
|
||||
receiveResponse(packageId: number, instanceId: number, rst: number): void {
|
||||
getTcpService()?.getTcpReqMonitor()?.receiveResponse(packageId, instanceId, rst)
|
||||
}
|
||||
|
||||
addRspToQueue(packageId: number, instanceId: number): void {
|
||||
getTcpService()?.getTcpReqMonitor()?.addRspToQueue(packageId, instanceId)
|
||||
}
|
||||
|
||||
processReceiveData(packageId: number, instanceId: number): void {
|
||||
getTcpService()?.getTcpReqMonitor()?.processReceiveData(packageId, instanceId)
|
||||
}
|
||||
|
||||
sendErrorLog(tag: string, msg: string): void {
|
||||
//在这里将tcp的错误日志发送到apm
|
||||
HXEventMonitor.onEvent(this.TCP_LOG_MONITOR_MODULE, tag, Object({
|
||||
errMsg: msg
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { getConnectionManager, HXLog } from 'biz_common';
|
||||
import {
|
||||
IStructReceiver,
|
||||
ProtocolDef,
|
||||
StuffBaseStruct,
|
||||
StuffResourceStruct,
|
||||
ConnectionServiceHelper
|
||||
} from '@kernel/lib_communication';
|
||||
import { StringUtils } from 'hxutil';
|
||||
import { ConnectionSeq } from '@kernel/lib_communication';
|
||||
import { ServerResourceManager, ServerTimeProvider } from 'biz_hxservice';
|
||||
import { getTcpService } from '@kernel/app_monitor_tcp';
|
||||
import { APMHelper } from '../../monitor/APMHelper';
|
||||
|
||||
/**
|
||||
* 认证包拦截器
|
||||
* 这个拦截器用于接受认证后的session数据包以及事件包
|
||||
* @author menghuiyuan@myhexin.com
|
||||
*/
|
||||
export class AuthJsonStructObserver implements IStructReceiver {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
onIntercept(stuffBaseStruct: StuffBaseStruct): boolean {
|
||||
if (stuffBaseStruct == null) {
|
||||
return false;
|
||||
}
|
||||
const headId = stuffBaseStruct.packageId;
|
||||
if (headId == ProtocolDef.DATAID_NEW_REALTIME) {
|
||||
const dataType = stuffBaseStruct.headType & ProtocolDef.DATA_TYPE_MASK;
|
||||
if (dataType === ProtocolDef.DATA_TYPE_JSON) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
handleStruct(stuffBaseStruct: StuffBaseStruct): boolean {
|
||||
if (!(stuffBaseStruct instanceof StuffResourceStruct)) {
|
||||
return false;
|
||||
}
|
||||
let tmpBuffer = stuffBaseStruct.buffer;
|
||||
const decodedStr = StringUtils.decode(tmpBuffer, "GBK")
|
||||
|
||||
try {
|
||||
const parsedObj: AuthStruct = JSON.parse(decodedStr)
|
||||
ServerResourceManager.getInstance().parseResource(decodedStr);
|
||||
if (parsedObj.srvTime && (typeof parsedObj.srvTime === 'number')) {
|
||||
ServerTimeProvider.getInstance().initServiceTime(parseInt(parsedObj.srvTime) * 1000)
|
||||
}
|
||||
const hasSessionId = StringUtils.isNotEmpty(parsedObj.sessionId);
|
||||
//处理sessionId
|
||||
if (hasSessionId) {
|
||||
ConnectionServiceHelper.getInstance()
|
||||
.getConnectionService()
|
||||
.setSessionId(ConnectionSeq.HQ, parsedObj.sessionId)
|
||||
|
||||
//设置tcp监控的sas
|
||||
getTcpService()?.setSas(`${parsedObj.sessionId}_${getConnectionManager()?.getConnectedIp()}`)
|
||||
}
|
||||
const hasClientIP = StringUtils.isNotEmpty(parsedObj.clientIp);
|
||||
if (hasClientIP) {
|
||||
APMHelper.clientIp = parsedObj.clientIp
|
||||
AppStorage.setOrCreate('clientIp', parsedObj.clientIp)
|
||||
}
|
||||
if (hasSessionId || hasClientIP) {
|
||||
//WTLoginAdapterService.receiveSessionIdHandle()
|
||||
}
|
||||
//todo 处理账户事件,如强制下线等
|
||||
if (StringUtils.isNotEmpty(parsedObj.events)) {
|
||||
HXLog.i("AuthJSONStruct", "error parsing")
|
||||
}
|
||||
} catch (e) {
|
||||
HXLog.e("AuthJSONStruct", "Error parsing " + JSON.stringify(e))
|
||||
}
|
||||
|
||||
return false;
|
||||
|
||||
}
|
||||
|
||||
getReceiverId(): string {
|
||||
return "AuthJsonStructObserver"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证包结构
|
||||
*/
|
||||
export class AuthStruct {
|
||||
srvTime: string = "";
|
||||
clientIp: string = "";
|
||||
clientPort: string = "";
|
||||
serverIp: string = "";
|
||||
releaseName: string = "";
|
||||
AuthCountryCodeMd5: string = "";
|
||||
events: string = "";
|
||||
sessionId: string = "";
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { StringUtils } from 'hxutil';
|
||||
import { IStructReceiver, ITCPClient, ProtocolDef, StuffBaseStruct, StuffResourceStruct } from '@kernel/lib_communication';
|
||||
import { SelfCodeEvent } from '../../selfcode/SelfCodeEvents';
|
||||
import { UgcClient } from '../../register/ugcregister/UgcClient';
|
||||
|
||||
/**
|
||||
* 自选股更新订阅拦截
|
||||
* @author menghuiyuan@myhexin.com
|
||||
*/
|
||||
export class SelfCodeChangeObserver implements IStructReceiver {
|
||||
|
||||
constructor() { }
|
||||
|
||||
onIntercept(stuffBaseStruct: StuffBaseStruct): boolean {
|
||||
if (stuffBaseStruct == null) {
|
||||
return false;
|
||||
}
|
||||
const headId = stuffBaseStruct.packageId;
|
||||
if (headId == ProtocolDef.DATAID_NEW_REALTIME) {
|
||||
const dataType = stuffBaseStruct.headType & ProtocolDef.DATA_TYPE_MASK;
|
||||
if (dataType === ProtocolDef.DATA_TYPE_JSON) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//自选股版本变更推送id
|
||||
private readonly UPDATE_SIG_VERSION = '00000009';
|
||||
//自选股更新强制推送id
|
||||
private readonly UPDATE_SIG_FORCE = '00000010';
|
||||
|
||||
handleStruct(stuffBaseStruct: StuffBaseStruct): boolean {
|
||||
if (!(stuffBaseStruct instanceof StuffResourceStruct)) {
|
||||
return false;
|
||||
}
|
||||
const resStruct = stuffBaseStruct as StuffResourceStruct;
|
||||
const tmpBuffer = resStruct.buffer;
|
||||
const decodedStr = StringUtils.decode(tmpBuffer, "GBK")
|
||||
try {
|
||||
const parsedObj : SelfCodeEvent = JSON.parse(decodedStr);
|
||||
if (parsedObj) {
|
||||
const eventArrayLen = parsedObj.events.length;
|
||||
for (let index = 0; index < eventArrayLen; index++) {
|
||||
const eventObj = parsedObj.events[index];
|
||||
const eventId = eventObj.eventID;
|
||||
switch (eventId) {
|
||||
//版本号推送
|
||||
case this.UPDATE_SIG_VERSION:
|
||||
const selfCodeVersionData = eventObj.eventData;
|
||||
//TODO 推送版本号:
|
||||
UgcClient.getInstance().onSelfCodeRemoteVersion(Number(selfCodeVersionData.version))
|
||||
return true;
|
||||
case this.UPDATE_SIG_FORCE:
|
||||
//TODO 推送强制更新版本
|
||||
const forceUpdateVersionData = JSON.stringify(eventObj.eventData);
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getReceiverId(): string {
|
||||
return "SelfCodeChangeObserver";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
IStructReceiver,
|
||||
RequestQueueManagement,
|
||||
StuffBaseStruct,
|
||||
StuffResourceStruct
|
||||
} from '@kernel/lib_communication';
|
||||
import { SidPermissionRefreshClient } from 'biz_auth';
|
||||
|
||||
const USER_RESPONSE_TYPE_NOTHING = 0;
|
||||
|
||||
/**
|
||||
* SID 权限刷新特殊包接收器。
|
||||
*
|
||||
* 对齐 Android SIDInstanceIdReicever:主动刷新 SID 的回包使用固定 instanceId=13,
|
||||
* 服务端可能按 type=0 返回普通 body,连接库会先解成 StuffBaseStruct。
|
||||
* 这里只按包头条件识别该特殊包,把原始 body 包装成 LOGIN_AUTH 资源后继续投递给请求 client。
|
||||
*/
|
||||
export class SidInstanceIdReceiver implements IStructReceiver {
|
||||
onIntercept(stuffBaseStruct: StuffBaseStruct): boolean {
|
||||
return this.isSidRefreshBaseStruct(stuffBaseStruct);
|
||||
}
|
||||
|
||||
handleStruct(stuffBaseStruct: StuffBaseStruct): boolean {
|
||||
if (!this.isSidRefreshBaseStruct(stuffBaseStruct)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// IStructReceiver 返回 true 后会截断原始 StuffBaseStruct 的普通分发流程,
|
||||
// 这里需要把包装后的 LOGIN_AUTH 资源主动投递给注册在 REFRESH_SID 上的请求 client。
|
||||
const sidStruct = this.createSidResourceStruct(stuffBaseStruct);
|
||||
const clients = RequestQueueManagement.getInstance().getNetworkClient(SidPermissionRefreshClient.REFRESH_SID);
|
||||
if (!clients || clients.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const client of clients) {
|
||||
client?.receive(sidStruct);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getReceiverId(): string {
|
||||
return 'SidInstanceIdReceiver';
|
||||
}
|
||||
|
||||
private isSidRefreshBaseStruct(stuffBaseStruct: StuffBaseStruct | null): boolean {
|
||||
if (!stuffBaseStruct || stuffBaseStruct instanceof StuffResourceStruct) {
|
||||
return false;
|
||||
}
|
||||
// 对齐 Android filter:只通过包头 type=0 和固定 instanceId=13 识别 SID 刷新特殊包。
|
||||
// SID 内容不参与判断,避免把权限字段格式差异误判为客户端解析问题。
|
||||
const rawBuffer = stuffBaseStruct.rawBuffer;
|
||||
return stuffBaseStruct.instanceId === SidPermissionRefreshClient.REFRESH_SID &&
|
||||
stuffBaseStruct.headType === USER_RESPONSE_TYPE_NOTHING &&
|
||||
rawBuffer !== null &&
|
||||
rawBuffer.length > 0;
|
||||
}
|
||||
|
||||
private createSidResourceStruct(stuffBaseStruct: StuffBaseStruct): StuffResourceStruct {
|
||||
// 这里不解码、不解析 SID,只复制主站原始 body 并包装成 LOGIN_AUTH。
|
||||
// 后续 SID 字符串解析和权限更新仍交给 SidPermissionRefreshClient 统一处理。
|
||||
const rawBuffer = stuffBaseStruct.rawBuffer as Uint8Array;
|
||||
const sidBuffer = rawBuffer.slice(0, rawBuffer.length);
|
||||
const sidStruct = new StuffResourceStruct(StuffResourceStruct.LOGIN_AUTH, sidBuffer);
|
||||
stuffBaseStruct.copyHead(sidStruct);
|
||||
sidStruct.connectionSeq = stuffBaseStruct.connectionSeq;
|
||||
sidStruct.rawBuffer = sidBuffer;
|
||||
return sidStruct;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { HXLog } from 'biz_common';
|
||||
import { ICheckCallback } from '@kernel/lib_communication/src/main/ets/connection/ICheckCallback';
|
||||
|
||||
/**
|
||||
* 连接库检查回调类
|
||||
* @author menghuiyuan@myhexin.com
|
||||
*/
|
||||
export class ConnectionCheckImpl implements ICheckCallback {
|
||||
|
||||
private readonly TAG = "SocketConnectionCheckImpl"
|
||||
onChannelOk(): void {
|
||||
HXLog.i(this.TAG, "onChannelOk");
|
||||
}
|
||||
|
||||
onChannelBad(badType: number): void {
|
||||
HXLog.i(this.TAG, "onChannelBad" + badType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否需要重新连接
|
||||
* @returns true,默认开启重连
|
||||
*/
|
||||
needReconnect(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import { getConnectionManager } from 'biz_common';
|
||||
import { UgcClient } from '../../register/ugcregister/UgcClient';
|
||||
|
||||
/**
|
||||
* 主动关闭当前的socket连接
|
||||
*/
|
||||
export function closeSocketConnection() : void {
|
||||
if (getConnectionManager()) {
|
||||
getConnectionManager()?.disconnect(false);
|
||||
UgcClient.getInstance().onHXSocketClose()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { getConnectionManager, HXLog, RuntimeStatus } from 'biz_common';
|
||||
import { ConnectionCheckImpl } from './ConnectionCheckImpl';
|
||||
import { closeSocketConnection } from './ConnectionServiceUtil';
|
||||
|
||||
/**
|
||||
* socket的监控管理
|
||||
*/
|
||||
export class HXSocketMonitor {
|
||||
|
||||
private readonly TAG : string = "HXSocketMonitor";
|
||||
|
||||
//当前App是否处于前台
|
||||
private isAppForeground = false;
|
||||
|
||||
onAppForeground() {
|
||||
HXLog.i(this.TAG, "onAppForeground")
|
||||
this.isAppForeground = true;
|
||||
if (getConnectionManager()) {
|
||||
HXLog.i(this.TAG, "onAppForeground checkConnection")
|
||||
getConnectionManager()?.checkConnection(new ConnectionCheckImpl());
|
||||
}
|
||||
}
|
||||
|
||||
onAppBackground() {
|
||||
HXLog.i(this.TAG, "onAppBackground")
|
||||
this.isAppForeground = false;
|
||||
closeSocketConnection()
|
||||
RuntimeStatus.getInstance().setTcpDisConnected()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export class EntryConstant {
|
||||
//微信分享id
|
||||
static readonly WX_SHARE_ID = 'wx6ba37430b7916a86';
|
||||
}
|
||||
@@ -0,0 +1,484 @@
|
||||
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
|
||||
import hilog from '@ohos.hilog';
|
||||
import UIAbility from '@ohos.app.ability.UIAbility';
|
||||
import Want from '@ohos.app.ability.Want';
|
||||
import window from '@ohos.window';
|
||||
import {
|
||||
AppConfigManager,
|
||||
buildHeader,
|
||||
CodeTableMonitor,
|
||||
GlobalContext,
|
||||
HXLog,
|
||||
jumpToQuote,
|
||||
PreferenceService,
|
||||
RuntimeStatus,
|
||||
ScreenUtil,
|
||||
StartManager,
|
||||
updateBreakpoint,
|
||||
updateCurrentWindowWith,
|
||||
WebJumpLinkWhiteManager
|
||||
} from 'biz_common';
|
||||
import { AuthProviderService } from 'biz_auth';
|
||||
import { ContextUtils, StringUtils } from 'hxutil';
|
||||
import { ContextUtils as ContextUtils2b } from '@b2b/hxutil';
|
||||
import { ConnectionSeq } from '@kernel/lib_communication/src/main/ets/data/CommunicationConstants';
|
||||
import { SelfCodeGroupHandler } from '../groupsub/business/SelfCodeGroupHandler';
|
||||
import { HXSocketMonitor } from '../communication/socketmonitor/HXSocketMonitor';
|
||||
import { Configuration, ConfigurationConstant, wantConstant } from '@kit.AbilityKit';
|
||||
import { UgcClient } from '../register/ugcregister/UgcClient';
|
||||
import { ContinuousDataHelper, CurveNaviModel } from 'biz_quote/src/main/ets/data/ContinuousDataHelper';
|
||||
import { DelayMarketManager, FutureMarketDataManager, HQTabManager, InactiveContractManager,
|
||||
MainContractGroupHandler } from 'biz_market';
|
||||
import { MainContractManager } from 'biz_market/src/main/ets/manager/MainContractManager';
|
||||
import { SelfCodeMainListHandler } from '../groupsub/business/SelfCodeMainListHandler';
|
||||
import { UgcAuthListener } from '../register/ugcregister/UgcAuthListener';
|
||||
import { AppUtil } from '@pura/harmony-utils';
|
||||
import { ElkUploadRegister } from '../register/elkregister/ElkUploadRegister';
|
||||
import { HexinVersionControl } from 'biz_trade/src/main/ets/utils/HexinVersionControl';
|
||||
import { SecurityMonitorInit } from '../monitor/SecurityMonitorInit';
|
||||
import { TabConfigManager } from '@b2c/biz_discover';
|
||||
import { ShareConfig } from '@b2c-spi/hx_share_api/src/main/ets/config/ShareConfig';
|
||||
import { HMRouterMgr } from '@hadss/hmrouter';
|
||||
import { EntryConstant } from '../constant/EntryConstant';
|
||||
import { ConnectionInitiator } from '../communication/ConnectionInitiator';
|
||||
import { APMHelper } from '../monitor/APMHelper';
|
||||
import { cbasService } from '../register/cbasregister/CbasServiceRegister';
|
||||
import { eventSubscription } from '../monitor/AppEvent';
|
||||
import { LaunchMonitor } from '../monitor/LaunchMonitor';
|
||||
import { StockGroupConstants } from 'biz_selfcode';
|
||||
import { StockGroupHandler } from '../groupsub/business/StockGroupHandler';
|
||||
import { VarietyGroupHandler } from '../groupsub/business/VarietyGroupHandler';
|
||||
import { SidGroupHandler } from '../groupsub/business/SidGroupHandler';
|
||||
import { FuturesCompanyGroupHandler, YYBListGroupHandler, TradeSettingGroupHandler, TradeCloudSyncConfig,
|
||||
TradeMsgUniqueIdManager,
|
||||
TradeNotificationTrigger } from 'biz_trade';
|
||||
import { GroupSubManager } from 'base_mobilepush';
|
||||
import { CodeTableManager } from 'lib_codetablemanager';
|
||||
import BuildProfile from 'BuildProfile';
|
||||
import { GrayTestPushHandler } from '../groupsub/business/GrayTestPushHandler';
|
||||
import { AccountAnalysisLuaPushHandler } from '../groupsub/business/AccountAnalysisLuaPushHandler';
|
||||
import { QhStrategyAdPushHandler } from '../groupsub/business/QhStrategyAdPushHandler';
|
||||
import { GrayTestInitializor } from '../initialize/GrayTestInitializor';
|
||||
import { CloudStoreLogInitializor } from '../initialize/CloudStoreLogInitializor';
|
||||
import { HXPushService } from 'service_push';
|
||||
import { WantCacheManager } from './WantManager';
|
||||
import { pushService } from '@kit.PushKit';
|
||||
import { HXNetwork } from '../network/HXNetwork';
|
||||
import { AdvertisingMonitorService, KaihuFlagManager, ServerTimeProvider, UserSourceManager } from 'biz_hxservice';
|
||||
import { UnifyUrlManager } from '@b2c-f/url-modify-config';
|
||||
import { FontManager } from '../font/FontManager';
|
||||
import { BizType, IndicatorGroupHandler, CommonIndicatorsManage, IndicatorParamsManage } from 'biz_indicator';
|
||||
import { FigureRegistry } from 'biz_chart_data_processor';
|
||||
import { TradeInitializor } from '../initialize/TradeInitializor';
|
||||
import { AdsInitializer } from '../initialize/AdsInitializor';
|
||||
import { AIGroupClient } from 'service_aigroup'
|
||||
import { AIGroupPushHandler } from '../groupsub/business/AIGroupPushHandler';
|
||||
import { DrawLineGroupHandler } from 'biz_quote';
|
||||
import { CurveStorage } from 'biz_quote';
|
||||
import { OpenAccountManager } from 'biz_openaccount/src/main/ets/manager/OpenAccountManager';
|
||||
import { HttpUtils } from '@kernel/rcp';
|
||||
import { DebugToolUtil } from 'biz_debug';
|
||||
import { TradeDailyPushDataManager } from '../NotificationExtension/TradeDaily/TradeDailyPushDataManager';
|
||||
import { TradeDailyConvertManager } from '../NotificationExtension/TradeDaily/TradeDailyConvertManager';
|
||||
import { PushMessageManager } from 'biz_trade/src/main/ets/service/push/PushMessageManager';
|
||||
import { ColourModeStatus } from '@kernel/theme_manager';
|
||||
import { VarietyCodeTransformManager } from 'biz_chart_data_processor/src/main/ets/utils/VarietyCodeTransformManager';
|
||||
import { ContextUtils as ContextUtils2c } from '@b2c-m/ohos_hx_utils';
|
||||
import { FunctionAuthorityGroupHandler } from '../groupsub/business/FunctionAuthorityGroupHandler';
|
||||
import { FunctionAuthorityRequestManager } from '../functionauthority/FunctionAuthorityRequester';
|
||||
import { QhStrategyAdPagePushHandler } from '../groupsub/business/QhStrategyAdPagePushHandler';
|
||||
|
||||
/**
|
||||
* 应用的主Ability
|
||||
* @author menghuiyuan@myhexin.com
|
||||
*/
|
||||
export default class EntryAbility extends UIAbility {
|
||||
private readonly tag: string = "EntryAbility"
|
||||
private windowObj?: window.Window
|
||||
private curBp: string = ''
|
||||
//socket连接监控,需要接收应用前后台事件
|
||||
private socketMonitor: HXSocketMonitor = new HXSocketMonitor();
|
||||
private curNaviModel: CurveNaviModel | null = null;
|
||||
|
||||
onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
|
||||
|
||||
pushService.receiveMessage('IM', this, (payload) => {
|
||||
// todo 应用前台弹窗接收push逻辑,待补充。
|
||||
try {
|
||||
const data = payload.data;
|
||||
const pushInfo: string = JSON.parse(data)?.data
|
||||
HXLog.d(this.tag, `Succeeded in getting notification,data=${JSON.stringify(pushInfo)}`);
|
||||
|
||||
const pushModel = TradeDailyPushDataManager.getInstance().convertToTradeDailyPushModel(JSON.parse(pushInfo))
|
||||
if (!pushModel) {
|
||||
return
|
||||
}
|
||||
// 推送去重判断
|
||||
TradeMsgUniqueIdManager.getInstance().isSameIdentify(
|
||||
pushModel.time,
|
||||
pushModel.iconType,
|
||||
pushModel.type,
|
||||
pushModel.orderNo
|
||||
).then((hasSame) => {
|
||||
if (!hasSame) {
|
||||
TradeDailyPushDataManager.getInstance().recordPushDaily(JSON.parse(pushInfo)).then(() => {
|
||||
TradeDailyConvertManager.getInstance().convertDailyFromPush()
|
||||
})
|
||||
PushMessageManager.getInstance().processReceiveHXPushMessage(pushInfo, false)
|
||||
}
|
||||
})
|
||||
} catch (error) {
|
||||
HXLog.e(this.tag, `Parse push data failed: ${error}`)
|
||||
}
|
||||
})
|
||||
|
||||
hilog.info(0x0000, this.tag, '%{public}s', 'Ability onCreate');
|
||||
|
||||
// ========== 注册自定义 Figure ==========
|
||||
// 注册所有 iOS 迁移过来的自定义 Figure
|
||||
// 包括:bar, barDouble, polygon, tower, american, trade, yAxisRange
|
||||
FigureRegistry.registerAll()
|
||||
hilog.info(0x0000, this.tag, 'Custom Figures registered successfully');
|
||||
// =====================================
|
||||
|
||||
// 初始化启动性能监控(必须在 early onCreate 注册,确保 reportDrawnCompleted 前 watcher 已就绪)
|
||||
LaunchMonitor.init();
|
||||
|
||||
DebugToolUtil.TARGET_NAME = BuildProfile.TARGET_NAME
|
||||
DebugToolUtil.PRODUCT_NAME = BuildProfile.PRODUCT_NAME
|
||||
DebugToolUtil.BUILD_MODE_NAME = BuildProfile.BUILD_MODE_NAME
|
||||
// Context初始化前移
|
||||
GlobalContext.initContext(this.context.getApplicationContext())
|
||||
ContextUtils.initContext(this.context.getApplicationContext())
|
||||
ContextUtils2c.initContext(this.context.getApplicationContext())
|
||||
ContextUtils2b.initContext(this.context.getApplicationContext())
|
||||
AppUtil.init(this.context)
|
||||
// 交易通知触发器初始化
|
||||
TradeNotificationTrigger.getInstance().init(this.context)
|
||||
//崩溃保护代码
|
||||
SecurityMonitorInit.getInstance().init();
|
||||
GrayTestInitializor.initGrayTest()
|
||||
CloudStoreLogInitializor.initCloudStoreLog()
|
||||
HMRouterMgr.init({ context: this.context });
|
||||
UnifyUrlManager.getInstance().init(this.context);
|
||||
// 启动管理:是否为安装后第一次进入APP
|
||||
StartManager.getInstance().init()
|
||||
RuntimeStatus.getInstance().init()
|
||||
ElkUploadRegister.getInstance().init();
|
||||
UgcClient.getInstance().init()
|
||||
AuthProviderService.getInstance().setUgcAuthListener(UgcAuthListener.getInstance())
|
||||
this.context.setMissionContinueState(AbilityConstant.ContinueState.INACTIVE, (result) => {
|
||||
console.info(`setMissionContinueState: ${JSON.stringify(result)}`);
|
||||
});
|
||||
eventSubscription()
|
||||
this.initRcp()
|
||||
ShareConfig.setAppId(EntryConstant.WX_SHARE_ID);
|
||||
this.initFuture()
|
||||
AdsInitializer.initAds()
|
||||
// 初始化 AI 分组客户端
|
||||
AIGroupClient.getInstance().init(DebugToolUtil.isDebugMode())
|
||||
WantCacheManager.getInstance().stashWant(want)
|
||||
|
||||
FontManager.getInstance().loadTHSFonts()
|
||||
HXLog.init()
|
||||
OpenAccountManager.getInstance().upCfmmcRecord()
|
||||
UserSourceManager.getInstance().initUserSource()
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化Rcp库配置
|
||||
*/
|
||||
initRcp() {
|
||||
HttpUtils.setGlobalHeaders((url) => {
|
||||
return buildHeader(true)
|
||||
})
|
||||
}
|
||||
|
||||
initFuture() {
|
||||
//大内容tab配置初始化
|
||||
TabConfigManager.getInstance().initDiscoverTabConfig()
|
||||
// 初始化常用指标管理(参考 iOS MePool.mm:338 userIdLoginSuccess)
|
||||
CommonIndicatorsManage.getInstance().init(this.context)
|
||||
// 初始化指标参数管理(参考 iOS MePool.mm:340)
|
||||
IndicatorParamsManage.getInstance().init(this.context)
|
||||
FunctionAuthorityRequestManager.requestAuthorityDataAfterLocalLoad()
|
||||
//初始化码表
|
||||
const codeTableInstance = CodeTableManager.get()
|
||||
codeTableInstance.setInnerDebugMode(DebugToolUtil.isInnerNetSimulateMode())
|
||||
codeTableInstance.initCodeTable(this.context, HexinVersionControl.disableCodeTableIncrementUpdate(),
|
||||
CodeTableMonitor.getInstance(), () => {
|
||||
return ServerTimeProvider.getInstance().getServerTime()
|
||||
}, (isJson?: boolean) => {
|
||||
return buildHeader(isJson)
|
||||
}).then(() => {
|
||||
//主力合约初始化
|
||||
MainContractManager.get().init()
|
||||
//初始化期货行情页面的tab配置
|
||||
HQTabManager.get().init()
|
||||
})
|
||||
//期货市场配置
|
||||
FutureMarketDataManager.get().init()
|
||||
//不活跃合约
|
||||
InactiveContractManager.get().init()
|
||||
// 初始化延时市场配置
|
||||
DelayMarketManager.get().init()
|
||||
// 初始化品种码转换映射表(如果还没加载)
|
||||
VarietyCodeTransformManager.init()
|
||||
// 交易管理初始化
|
||||
TradeInitializor.init(this.context)
|
||||
// 分时持久化存储UI状态存储初始化
|
||||
CurveStorage.getInstance().initPersistentStorage()
|
||||
}
|
||||
|
||||
onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
|
||||
HXLog.i(this.tag, `onNewWant ${AbilityConstant.LaunchReason.CONTINUATION}`)
|
||||
WantCacheManager.getInstance().stashWant(want)
|
||||
if (launchParam.launchReason == AbilityConstant.LaunchReason.CONTINUATION) {
|
||||
// 将上述的保存的数据取出恢复
|
||||
let continueInput = '';
|
||||
if (want.parameters != undefined) {
|
||||
continueInput = want.parameters.curveData as string;
|
||||
|
||||
HXLog.i(this.tag, "onNewWant continue input = " + continueInput)
|
||||
this.curNaviModel = JSON.parse(continueInput) as CurveNaviModel;
|
||||
} else {
|
||||
HXLog.i(this.tag, "onNewWant continue input is empty")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDestroy() {
|
||||
OpenAccountManager.getInstance().upCfmmcRecord()
|
||||
PreferenceService.putVal('Install', 'isFirstTimeEntering', false, true);
|
||||
hilog.info(0x0000, this.tag, '%{public}s', 'Ability onDestroy');
|
||||
}
|
||||
|
||||
onContinue(wantParam: Record<string, Object>): AbilityConstant.OnContinueResult | Promise<AbilityConstant.OnContinueResult> {
|
||||
let versionDst = wantParam.version; // 获取迁移对端应用的版本号
|
||||
let versionSrc: number = 1000600; // 获取迁移源端即本端应用的版本号
|
||||
HXLog.i(this.tag, "versionDst = " + versionDst + "versionSrc = " + versionSrc)
|
||||
if (versionDst < versionSrc) { // 兼容性校验
|
||||
// 兼容性校验不满足
|
||||
HXLog.w(this.tag, "onContinue, dst not match the minumum version, abort continuation process")
|
||||
return AbilityConstant.OnContinueResult.MISMATCH;
|
||||
}
|
||||
if (ContinuousDataHelper.getInstance().getCurrentNavi()) {
|
||||
hilog.info(0x0000, this.tag, "onContinue set success = ");
|
||||
wantParam["curveData"] = JSON.stringify(ContinuousDataHelper.getInstance().getCurrentNavi()!);
|
||||
}
|
||||
wantParam[wantConstant.Params.SUPPORT_CONTINUE_PAGE_STACK_KEY] = false;
|
||||
return AbilityConstant.OnContinueResult.AGREE;
|
||||
}
|
||||
|
||||
onWindowStageCreate(windowStage: window.WindowStage) {
|
||||
//存储程序当前的 windowStage,用于创建子窗口。
|
||||
AppStorage.setOrCreate('EntryAbilityWindowStage', windowStage);
|
||||
// 埋点调试工具需要使用的 windowStage
|
||||
AppStorage.setOrCreate('windowStage', windowStage);
|
||||
|
||||
//初始化App的一些公共资源和窗口设置
|
||||
let windowClass: window.Window = windowStage.getMainWindowSync();
|
||||
let avoidArea = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_NAVIGATION_INDICATOR);
|
||||
GlobalContext.initNavigationBarHeight(avoidArea.bottomRect.height)
|
||||
|
||||
|
||||
let statusBarHeight = windowClass.getWindowAvoidArea(window.AvoidAreaType.TYPE_SYSTEM).topRect.height;
|
||||
GlobalContext.initStatusBarHeight(statusBarHeight);
|
||||
// Main window is created, set main page for this ability
|
||||
hilog.info(0x0000, this.tag, '%{public}s', 'Ability onWindowStageCreate');
|
||||
windowStage.getMainWindow().then((windowObj) => {
|
||||
this.windowObj = windowObj
|
||||
// 获取应用启动时的窗口尺寸
|
||||
this.curBp = updateBreakpoint(windowObj.getWindowProperties().windowRect.width, this.curBp)
|
||||
// 注册回调函数,监听窗口尺寸变化
|
||||
windowObj.on('windowSizeChange', (windowSize) => {
|
||||
this.curBp = updateBreakpoint(windowSize.width, this.curBp)
|
||||
updateCurrentWindowWith(windowSize.width)
|
||||
})
|
||||
});
|
||||
this.registerGroupHandlers();
|
||||
ColourModeStatus.initColourModeSetting(this.context)
|
||||
HexinVersionControl.recordFirstInstallVersion();
|
||||
HexinVersionControl.getInstance().requestVersionConfig();
|
||||
|
||||
// 网络不可用
|
||||
const netAvailable = HXNetwork.getInstance().detectNet();
|
||||
if (!netAvailable) {
|
||||
this.loadNetworkAnomalyPage(windowStage);
|
||||
return;
|
||||
}
|
||||
|
||||
this.initializeCbasService().then(() => {
|
||||
// 正常流程
|
||||
}).catch((error: Error) => {
|
||||
// 即使失败也记录日志,但继续加载页面
|
||||
HXLog.e(this.tag, 'cbasService init failed: %{public}s');
|
||||
}).finally(() => {
|
||||
// 确保页面一定会加载
|
||||
if (!PreferenceService.getBooleanValueSync('Privacy', 'isUserAgree', false)) {
|
||||
this.loadPrivacyPage(windowStage);
|
||||
} else {
|
||||
this.loadIndexPage(windowStage);
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
onWindowStageRestore(windowStage: window.WindowStage): void {
|
||||
//应用窗口恢复时,如果有接续的数据就说明当前为接续开启应用,打开分时页
|
||||
HXLog.i(this.tag, "onWindowStageRestore")
|
||||
let parsedData: CurveNaviModel;
|
||||
if (this.curNaviModel) {
|
||||
HXLog.i(this.tag, "onWindowStageRestore data is valid")
|
||||
parsedData = this.curNaviModel;
|
||||
jumpToQuote(parsedData.code, parsedData.marketId, parsedData.codeList, parsedData.marketIdList);
|
||||
this.curNaviModel = null;
|
||||
}
|
||||
}
|
||||
|
||||
private registerGroupHandlers() {
|
||||
GroupSubManager.getInstance().addUserGroupHandler(SelfCodeMainListHandler.GROUP_HANDLE_NAME, new SelfCodeMainListHandler());
|
||||
GroupSubManager.getInstance().addUserGroupHandler(SelfCodeGroupHandler.GROUP_HANDLE_NAME, new SelfCodeGroupHandler());
|
||||
GroupSubManager.getInstance().addUserGroupHandler(YYBListGroupHandler.GROUP_HANDLE_NAME,new YYBListGroupHandler())
|
||||
GroupSubManager.getInstance().addUserGroupHandler(TradeCloudSyncConfig.FILE_NAME, new TradeSettingGroupHandler())
|
||||
const indicatorChannels: string[] = [
|
||||
BizType.COMMON_INDICATORS,
|
||||
BizType.CUSTOM_INDICATORS,
|
||||
BizType.INDICATOR_PARAMS,
|
||||
BizType.SQUARE_INDICATORS
|
||||
]
|
||||
for (const channel of indicatorChannels) {
|
||||
GroupSubManager.getInstance().addUserGroupHandler(channel, new IndicatorGroupHandler(channel))
|
||||
}
|
||||
// 接入画线同步 push 分组,收到远端画线后驱动本地刷新与重绘。
|
||||
GroupSubManager.getInstance().addUserGroupHandler(DrawLineGroupHandler.GROUP_KEY, new DrawLineGroupHandler())
|
||||
GroupSubManager.getInstance().addUserGroupHandler(AIGroupPushHandler.GROUP_HANDLE_NAME, new AIGroupPushHandler())
|
||||
GroupSubManager.getInstance().addUserGroupHandler(SidGroupHandler.GROUP_HANDLE_NAME, new SidGroupHandler())
|
||||
GroupSubManager.getInstance().addUserGroupHandler(
|
||||
FunctionAuthorityGroupHandler.GROUP_HANDLE_NAME,
|
||||
new FunctionAuthorityGroupHandler()
|
||||
)
|
||||
GroupSubManager.getInstance().addUserGroupHandler(QhStrategyAdPushHandler.GROUP_HANDLE_NAME, new QhStrategyAdPushHandler())
|
||||
GroupSubManager.getInstance().addAllGroupHandler(QhStrategyAdPagePushHandler.GROUP_HANDLE_NAME, new QhStrategyAdPagePushHandler())
|
||||
GroupSubManager.getInstance().addAllGroupHandler(GrayTestPushHandler.GROUP_HANDLE_NAME, new GrayTestPushHandler());
|
||||
GroupSubManager.getInstance().addAllGroupHandler(AccountAnalysisLuaPushHandler.GROUP_HANDLE_NAME, new AccountAnalysisLuaPushHandler());
|
||||
GroupSubManager.getInstance().addAllGroupHandler(FuturesCompanyGroupHandler.GROUP_HANDLE_NAME,new FuturesCompanyGroupHandler())
|
||||
GroupSubManager.getInstance().addAllGroupHandler(StockGroupConstants.KEY_STOCK_STORAGE_UPDATE, new StockGroupHandler())
|
||||
GroupSubManager.getInstance().addAllGroupHandler(StockGroupConstants.KEY_VARIETY_STORAGE_UPDATE, new VarietyGroupHandler())
|
||||
GroupSubManager.getInstance().addAllGroupHandler(MainContractGroupHandler.GROUP_HANDLE_NAME, new MainContractGroupHandler())
|
||||
|
||||
}
|
||||
|
||||
onWindowStageDestroy() {
|
||||
hilog.info(0x0000, this.tag, '%{public}s', 'Ability onWindowStageDestroy');
|
||||
}
|
||||
|
||||
onForeground() {
|
||||
// Ability has brought to foreground
|
||||
AppStorage.setOrCreate('isAbilityForeGround', true);
|
||||
cbasService.onForeground()
|
||||
this.socketMonitor.onAppForeground();
|
||||
HXPushService.getInstance().checkPushPermissionStatus()
|
||||
ColourModeStatus.updateSystemColourModeOnAbilityForeground(this.context)
|
||||
}
|
||||
|
||||
onConfigurationUpdate(newConfig: Configuration): void {
|
||||
ColourModeStatus.onSystemConfigurationUpdate(newConfig.colorMode)
|
||||
}
|
||||
|
||||
onBackground() {
|
||||
// Ability has back to background
|
||||
AppStorage.setOrCreate('isAbilityForeGround', false);
|
||||
cbasService.onBackground()
|
||||
this.socketMonitor.onAppBackground();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载应用主页面
|
||||
* @param windowStage
|
||||
*/
|
||||
private loadIndexPage(windowStage: window.WindowStage) {
|
||||
//三方标题栏风险提示,白名单
|
||||
WebJumpLinkWhiteManager.getInstance().loadWhiteJumpLink()
|
||||
|
||||
windowStage.loadContent('pages/Index', (err, data) => {
|
||||
if (err.code) {
|
||||
hilog.error(0x0000, this.tag, 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
|
||||
return;
|
||||
}
|
||||
onAppStartLoading(this.context,true);
|
||||
hilog.info(0x0000, this.tag, 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载隐私政策页面
|
||||
* @param windowStage
|
||||
*/
|
||||
private loadPrivacyPage(windowStage: window.WindowStage) {
|
||||
windowStage.loadContent('pages/LauncherPage', (err, data) => {
|
||||
if (err.code) {
|
||||
hilog.error(0x0000, this.tag, 'Failed to load loadPrivacyPage. Cause: %{public}s', JSON.stringify(err) ?? '');
|
||||
return;
|
||||
}
|
||||
onAppStartLoading(this.context);
|
||||
hilog.info(0x0000, this.tag, 'Succeeded in loadPrivacyPage. Data: %{public}s', JSON.stringify(data) ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载网络异常页面
|
||||
* @param windowStage
|
||||
*/
|
||||
private loadNetworkAnomalyPage(windowStage: window.WindowStage) {
|
||||
windowStage.loadContent('network/NetworkAnomalyPage', (err, data) => {
|
||||
if (err.code) {
|
||||
hilog.error(0x0000, this.tag, 'Failed to load NetworkAnomalyPage. Cause: %{public}s', JSON.stringify(err) ?? '');
|
||||
return;
|
||||
}
|
||||
onAppStartLoading(this.context);
|
||||
hilog.info(0x0000, this.tag, 'Succeeded in NetworkAnomalyPage. Data: %{public}s', JSON.stringify(data) ?? '');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载埋点初始化
|
||||
*/
|
||||
private async initializeCbasService() {
|
||||
try {
|
||||
await AppConfigManager.getInstance().fetchIMEI()
|
||||
let udid = AppConfigManager.getInstance().getUdidSync()
|
||||
if(!StringUtils.isNotEmpty(udid)) {
|
||||
udid = AppConfigManager.UDID_NONE
|
||||
}
|
||||
await cbasService.init(udid)
|
||||
HXLog.i(this.tag, 'cbasService initialized successfully');
|
||||
} catch (error) {
|
||||
HXLog.i( this.tag, 'cbasService init failed');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function onAppStartLoading(context:Context,isAgreePrivacy:boolean = false) {
|
||||
ConnectionInitiator.initConnectionService(context)
|
||||
AppConfigManager.getInstance().fetchIMEI().then((isSuccess: boolean) => {
|
||||
if (isSuccess) {
|
||||
AuthProviderService.getInstance().sendAuthRequest(ConnectionSeq.HQ);
|
||||
}
|
||||
let udid = AppConfigManager.getInstance().getUdidSync()
|
||||
if(!StringUtils.isNotEmpty(udid)) {
|
||||
udid = AppConfigManager.UDID_NONE
|
||||
}
|
||||
cbasService.init(udid)
|
||||
if (isAgreePrivacy) {
|
||||
AdvertisingMonitorService.getInstance().init(context)
|
||||
}
|
||||
});
|
||||
//初始化APM
|
||||
APMHelper.init(context);
|
||||
//初始化窗口工具
|
||||
ScreenUtil.get().init();
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { MDataModel, AppPushMessage } from "service_push";
|
||||
|
||||
export function ProtocolJumpTest() {
|
||||
let appMsg1: MDataModel = {
|
||||
'A': '10',
|
||||
'F': '2205',
|
||||
'C': 'FG2601',
|
||||
'ACT': '70',
|
||||
'TY': '20',
|
||||
'ST': '预警通知'
|
||||
}
|
||||
|
||||
let appMsg2: MDataModel = {
|
||||
'A': '10',
|
||||
'ACT': '70',
|
||||
"F": "2804",
|
||||
"U": "https://news.10jqka.com.cn/20251111/c672408397.shtml"
|
||||
}
|
||||
|
||||
let appMsg3: MDataModel = {
|
||||
// 跳转个股
|
||||
'A': '10',
|
||||
'F': '2205',
|
||||
'C': 'USDIND',
|
||||
'ACT': '60',
|
||||
'ST': '美元指数预警'
|
||||
}
|
||||
|
||||
let appMsg4: MDataModel = {
|
||||
// 跳转外部浏览器
|
||||
'A': '30',
|
||||
'ACT': '70',
|
||||
"F": "2804",
|
||||
"U": "https://news.10jqka.com.cn/20251111/c672408397.shtml"
|
||||
}
|
||||
|
||||
let appMsg5: MDataModel = {
|
||||
// 拉起其他应用
|
||||
'A': '40',
|
||||
'ACT': '70',
|
||||
'PN':'com.hexin.hmn.sjcg',
|
||||
'CN':'EntryAbility',
|
||||
}
|
||||
|
||||
let protocol1: AppPushMessage = {
|
||||
APPID: '207',
|
||||
ID: '604900',
|
||||
TIPTYPE: '1',
|
||||
TITLE: '标题',
|
||||
DESC: '内容',
|
||||
APPMSG: appMsg1
|
||||
}
|
||||
|
||||
let data: Record<string, string> = {};
|
||||
data['payload'] = JSON.stringify(protocol1);
|
||||
return data;
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { RemoteNotificationExtensionAbility, pushCommon } from '@kit.PushKit';
|
||||
import { hilog } from '@kit.PerformanceAnalysisKit';
|
||||
import { TradeDailyPushDataManager } from '../NotificationExtension/TradeDaily/TradeDailyPushDataManager';
|
||||
|
||||
const DOMAIN = 0x0000;
|
||||
|
||||
export default class RemoteNotificationExtAbility extends RemoteNotificationExtensionAbility {
|
||||
async onReceiveMessage(remoteNotificationInfo: pushCommon.RemoteNotificationInfo):
|
||||
Promise<pushCommon.RemoteNotificationContent> {
|
||||
const data = remoteNotificationInfo.data
|
||||
try {
|
||||
const pushInfo: string = JSON.parse(data)?.data
|
||||
await TradeDailyPushDataManager.getInstance().initDB(this.context)
|
||||
const pushModel = TradeDailyPushDataManager.getInstance().convertToTradeDailyPushModel(JSON.parse(pushInfo))
|
||||
if (pushModel) {
|
||||
hilog.info(DOMAIN, 'RemoteNotification', 'info: %{public}s', pushModel.body);
|
||||
await TradeDailyPushDataManager.getInstance().recordPushDaily(pushModel)
|
||||
}
|
||||
} catch (e) {
|
||||
hilog.error(DOMAIN, 'RemoteNotification', 'error: %{public}s', e);
|
||||
}
|
||||
|
||||
await Promise.resolve();
|
||||
return {};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
import Want from '@ohos.app.ability.Want';
|
||||
import { Queue } from '@kit.ArkTS';
|
||||
import { ClipUtils, GlobalContext, HXLog, jumpToWebPage } from 'biz_common';
|
||||
import { AppPushMessage, DoPushCbas, PushConstants, PushMessageManager } from 'service_push';
|
||||
import { ShareManager } from '@b2c-spi/hx_share_api/src/main/ets/manager/ShareManager';
|
||||
import { ELK_LEVEL, sendElk } from 'service_monitor';
|
||||
|
||||
const TAG: string = 'WantCacheManager'
|
||||
const BACK_ACTION: string = 'ohos.want.action.viewData'
|
||||
const BACK_ENTITY: string = 'entity.system.browsable'
|
||||
|
||||
|
||||
export const FIRST_PAGE_ONAPPEAR_EVENT : string = 'FirstPage OnAppear'
|
||||
/**
|
||||
* WantCacheManager 类用于管理 Want 对象的缓存和处理。
|
||||
* 采用单例模式,确保整个应用中只有一个实例。
|
||||
* 该管理器会缓存接收到的 Want 对象,并在合适的时机进行处理。
|
||||
*/
|
||||
export class WantCacheManager {
|
||||
// 单例模式下的唯一实例
|
||||
private static instance: WantCacheManager
|
||||
|
||||
private pushMessageManager : PushMessageManager = PushMessageManager.getInstance();
|
||||
private clipUtils: ClipUtils = ClipUtils.getInstance()
|
||||
|
||||
/**
|
||||
* 获取 WantCacheManager 的单例实例。
|
||||
* 如果实例不存在,则创建一个新实例。
|
||||
* @returns WantCacheManager 的单例实例
|
||||
*/
|
||||
public static getInstance(): WantCacheManager {
|
||||
if (!WantCacheManager.instance) {
|
||||
WantCacheManager.instance = new WantCacheManager()
|
||||
}
|
||||
return WantCacheManager.instance
|
||||
}
|
||||
|
||||
// 标志位,指示是否可以处理缓存的 Want 对象
|
||||
private enabled: boolean = false
|
||||
/**
|
||||
* 首页加载完成的回调函数。
|
||||
* 当首页加载完成后,将 enabled 标志位设置为 true,并尝试处理缓存的 Want 对象。
|
||||
*/
|
||||
private firstPageFinishedCb = () => {
|
||||
this.enabled = true
|
||||
this.popWant()
|
||||
}
|
||||
|
||||
/**
|
||||
* 私有构造函数,确保只能通过 getInstance 方法获取实例。
|
||||
* 注册首页加载完成的事件监听器。
|
||||
*/
|
||||
private constructor() {
|
||||
// 监听首页加载完成事件,当事件触发时调用 firstPageFinishedCb 回调函数
|
||||
GlobalContext.get().eventHub.on(FIRST_PAGE_ONAPPEAR_EVENT, this.firstPageFinishedCb)
|
||||
}
|
||||
|
||||
// 存储 Want 对象的队列
|
||||
private wantQueue: Queue<Want> = new Queue()
|
||||
|
||||
/**
|
||||
* 存储 Want 对象到队列中。
|
||||
* 如果 enabled 标志位为 true,则尝试处理队列中的 Want 对象。
|
||||
* @param want 要存储的 Want 对象
|
||||
*/
|
||||
stashWant(want: Want) {
|
||||
// 将 Want 对象添加到队列中
|
||||
this.wantQueue.add(want)
|
||||
// 记录日志,提示接收到新的 Want 对象
|
||||
HXLog.i(TAG, ' New Want received, want.param : ')
|
||||
// 如果可以处理 Want 对象,则尝试处理队列中的元素
|
||||
if (this.enabled) {
|
||||
this.popWant()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从队列中取出并处理一个 Want 对象。
|
||||
* 如果队列不为空,则调用不同业务方进行处理,业务方处理保证不更改原有对象
|
||||
*/
|
||||
private popWant() {
|
||||
if (this.wantQueue.length > 0) {
|
||||
const w = this.wantQueue.pop()
|
||||
|
||||
// push协议
|
||||
this.handlePayload(w)
|
||||
|
||||
// 微信分享
|
||||
this.handleWeChat(w)
|
||||
|
||||
// 浏览器回流
|
||||
this.handleBackWash(w)
|
||||
|
||||
// appLinking回流
|
||||
this.handleBackWashByBrowsable(w)
|
||||
|
||||
this.handleReflow(w)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理推送协议对象。
|
||||
* 根据want中解析到的推送协议的来源,获取相应的策略并处理。
|
||||
* @param want 要处理的want
|
||||
*/
|
||||
private handlePayload(want : Want) {
|
||||
if (want?.action === 'com.push.action') {
|
||||
const param = want?.parameters
|
||||
if (param) {
|
||||
const payloadJson = param['payload'] as string
|
||||
HXLog.i(PushConstants.AM_PUSH, payloadJson)
|
||||
const payload = JSON.parse(payloadJson) as AppPushMessage
|
||||
DoPushCbas.pushArriveCbas(payload)
|
||||
DoPushCbas.pushShowCbas(payload)
|
||||
this.pushMessageManager.handlerPush(payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 处理wechat分享协议对象。
|
||||
* 根据want中解析到的分享协议,进行处理
|
||||
* @param want 要处理的want
|
||||
*/
|
||||
private handleWeChat(want: Want) {
|
||||
// 判断service是否有效
|
||||
const shareService = ShareManager.getShareService()
|
||||
if (!shareService) {
|
||||
HXLog.e("HXShare", 'shareService is null');
|
||||
return
|
||||
}
|
||||
shareService.getWXApi().handleWant(want, ShareManager.getShareService()?.getWXEventHandler())
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 端外跳转至期货通
|
||||
*/
|
||||
private async handleBackWash(want: Want) {
|
||||
if (want.action === BACK_ACTION && want.uri && want.entities?.length === 0) {
|
||||
const params = this.extractUrlParams(want.uri)
|
||||
if (params['url']) {
|
||||
jumpToWebPage(params)
|
||||
}
|
||||
await this.dealPasteboard()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private async dealPasteboard(){
|
||||
if (this.clipUtils.getClipBoardSwitchState()) {
|
||||
// 请求剪切板权限后再读取
|
||||
const hasPermission = await this.clipUtils.checkAndRequestPermission()
|
||||
if (hasPermission) {
|
||||
const clipboardContent = this.clipUtils.getClipboardContent()
|
||||
if (this.clipUtils.isValidClipPassword(clipboardContent)) {
|
||||
this.clipUtils.parseClipboard(clipboardContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 浏览器跳转至期货通
|
||||
*/
|
||||
private handleBackWashByBrowsable(want: Want) {
|
||||
if (want.action === BACK_ACTION && want.uri && want.entities &&
|
||||
want.entities[0] === BACK_ENTITY) {
|
||||
const params = this.extractUrlParams(want.uri)
|
||||
if (params['url']) {
|
||||
jumpToWebPage(params)
|
||||
}
|
||||
this.dealPasteboard()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理回流埋点。
|
||||
* @param want 要处理的want
|
||||
*/
|
||||
private handleReflow(want: Want) {
|
||||
if (want.parameters) {
|
||||
const callerBundleName = want.parameters['ohos.aafwk.param.callerBundleName'] as string
|
||||
if (callerBundleName === 'com.hexin.hmn.sjcg') {
|
||||
// 手抄跳转
|
||||
sendElk(TAG, `callerBundleName ${callerBundleName}`, ELK_LEVEL.INFO, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extractUrlParams(input: string): Record<string, string> {
|
||||
const result: Record<string, string> = {};
|
||||
// 提取所有形如 key=value 的参数(以 &、^ 或字符串结尾为边界)
|
||||
const matches = Array.from(input.matchAll(/([a-zA-Z_][a-zA-Z0-9_]*)=([^&\^]*)/gi));
|
||||
if (matches.length === 0) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// 迭代解码(兼容含 & 的转义值 %26)
|
||||
matches.forEach(match => {
|
||||
const key = match[1];
|
||||
let decoded = match[2];
|
||||
let prev = '';
|
||||
do {
|
||||
prev = decoded;
|
||||
try {
|
||||
decoded = decodeURIComponent(decoded);
|
||||
} catch {
|
||||
break; // 非法编码终止
|
||||
}
|
||||
} while (decoded !== prev && /%[0-9A-Fa-f]{2}/.test(decoded));
|
||||
result[key] = decoded;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { TextMetricsMeasurer } from "@b2b/hq_table";
|
||||
import { text } from "@kit.ArkGraphics2D";
|
||||
import { HXLog } from "biz_common";
|
||||
|
||||
/**
|
||||
* author:liuqingliang
|
||||
* created on 2025/12/19
|
||||
* desc:全局字体管理
|
||||
*/
|
||||
export class FontManager {
|
||||
private static readonly TAG = "FontManager"
|
||||
private static instance: FontManager
|
||||
|
||||
public static getInstance(): FontManager {
|
||||
if (!FontManager.instance) {
|
||||
FontManager.instance = new FontManager()
|
||||
}
|
||||
return FontManager.instance
|
||||
}
|
||||
|
||||
registerFont(uiContext: UIContext) {
|
||||
try {
|
||||
let font = uiContext.getFont();
|
||||
font.registerFont({
|
||||
familyName: 'bold',
|
||||
familySrc: $rawfile('fonts/THSJinRongTi_bold.otf')
|
||||
});
|
||||
font.registerFont({
|
||||
familyName: 'Regular',
|
||||
familySrc: $rawfile('fonts/THSJinRongTi_Regular.otf')
|
||||
});
|
||||
font.registerFont({
|
||||
familyName: 'Medium',
|
||||
familySrc: $rawfile('fonts/THSJinRongTi_Medium.otf')
|
||||
});
|
||||
|
||||
TextMetricsMeasurer.getInstance().init()
|
||||
} catch (e) {
|
||||
HXLog.e(FontManager.TAG, "registerFont failed:" + JSON.stringify(e))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private loadFontSync(fontFamily: 'bold' | 'Regular' | 'Medium') {
|
||||
let fontCollection = text.FontCollection.getGlobalInstance()
|
||||
fontCollection.loadFontSync(fontFamily, $rawfile(`fonts/THSJinRongTi_${fontFamily}.otf`))
|
||||
}
|
||||
|
||||
loadTHSFonts() {
|
||||
this.loadFontSync('bold');
|
||||
this.loadFontSync('Regular');
|
||||
this.loadFontSync('Medium');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
CookieManager,
|
||||
FunctionAuthorityRequester,
|
||||
HxServiceExportConstants,
|
||||
HXUserService,
|
||||
ProductAuthorityManager
|
||||
} from 'biz_hxservice'
|
||||
import { AppConfigManager } from 'biz_common'
|
||||
import { http } from '@kit.NetworkKit'
|
||||
import { JSON } from '@kit.ArkTS'
|
||||
import { UnifyUrlManager } from '@b2c-f/url-modify-config'
|
||||
import emitter from '@ohos.events.emitter'
|
||||
|
||||
interface EmptyFunctionAuthorityResponse {
|
||||
code: number
|
||||
data: Object[]
|
||||
}
|
||||
|
||||
class AppFunctionAuthorityRequester implements FunctionAuthorityRequester {
|
||||
|
||||
async requestAuthorityData(): Promise<object> {
|
||||
if (HXUserService.getInstance().isTemporaryUser()) {
|
||||
const response: EmptyFunctionAuthorityResponse = { code: 0, data: [] }
|
||||
return response
|
||||
}
|
||||
|
||||
const httpRequest = http.createHttp()
|
||||
try {
|
||||
const requestOptions: http.HttpRequestOptions = {
|
||||
method: http.RequestMethod.GET,
|
||||
header: this.buildHeaders(),
|
||||
connectTimeout: 30000,
|
||||
readTimeout: 30000,
|
||||
expectDataType: http.HttpDataType.STRING
|
||||
}
|
||||
const response = await httpRequest.request(UnifyUrlManager.getRealUrl('custom_authority_url'), requestOptions)
|
||||
if (response.responseCode !== http.ResponseCode.OK) {
|
||||
throw new Error(`http error: ${response.responseCode}`)
|
||||
}
|
||||
const parseOptions: JSON.ParseOptions = {
|
||||
bigIntMode: JSON.BigIntMode.PARSE_AS_BIGINT
|
||||
}
|
||||
return JSON.parse(response.result as string, null, parseOptions) as object
|
||||
} finally {
|
||||
httpRequest.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private buildHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {}
|
||||
headers['Content-Type'] = 'application/json'
|
||||
const cookie = CookieManager.getInstance().getCookieSync()
|
||||
if (cookie) {
|
||||
headers['Cookie'] = cookie
|
||||
}
|
||||
return headers
|
||||
}
|
||||
}
|
||||
|
||||
export class FunctionAuthorityRequestManager {
|
||||
private static initialized: boolean = false
|
||||
private static cookieUpdateListener?: () => void
|
||||
private static requestingAfterCookieUpdate: boolean = false
|
||||
private static userId: string = ''
|
||||
|
||||
static loadAuthorityFromLocal(): boolean {
|
||||
FunctionAuthorityRequestManager.ensureInitialized()
|
||||
return ProductAuthorityManager.getInstance().loadAuthorityFromLocal()
|
||||
}
|
||||
|
||||
static requestAuthorityDataAfterLocalLoad(): Promise<boolean> {
|
||||
FunctionAuthorityRequestManager.loadAuthorityFromLocal()
|
||||
return FunctionAuthorityRequestManager.requestAuthorityData()
|
||||
}
|
||||
|
||||
static requestAuthorityData(): Promise<boolean> {
|
||||
FunctionAuthorityRequestManager.ensureInitialized()
|
||||
return ProductAuthorityManager.getInstance().requestAuthorityData()
|
||||
}
|
||||
|
||||
static requestAuthorityDataForPush(pushTimestamp: number): Promise<boolean> {
|
||||
FunctionAuthorityRequestManager.ensureInitialized()
|
||||
return ProductAuthorityManager.getInstance().requestAuthorityDataForPush(pushTimestamp)
|
||||
}
|
||||
|
||||
private static ensureInitialized(): void {
|
||||
if (FunctionAuthorityRequestManager.initialized) {
|
||||
return
|
||||
}
|
||||
ProductAuthorityManager.getInstance().setRequester(new AppFunctionAuthorityRequester())
|
||||
FunctionAuthorityRequestManager.registerCookieUpdateListener()
|
||||
FunctionAuthorityRequestManager.initialized = true
|
||||
}
|
||||
|
||||
private static registerCookieUpdateListener(): void {
|
||||
if (FunctionAuthorityRequestManager.cookieUpdateListener) {
|
||||
return
|
||||
}
|
||||
FunctionAuthorityRequestManager.cookieUpdateListener = () => {
|
||||
const newUserId = HXUserService.getInstance().getUserIdFromLocal()
|
||||
if (FunctionAuthorityRequestManager.requestingAfterCookieUpdate ||
|
||||
FunctionAuthorityRequestManager.userId == newUserId) {
|
||||
return
|
||||
}
|
||||
FunctionAuthorityRequestManager.userId = newUserId
|
||||
FunctionAuthorityRequestManager.requestingAfterCookieUpdate = true
|
||||
FunctionAuthorityRequestManager.requestAuthorityDataAfterLocalLoad()
|
||||
.finally(() => {
|
||||
FunctionAuthorityRequestManager.requestingAfterCookieUpdate = false
|
||||
})
|
||||
}
|
||||
emitter.on(HxServiceExportConstants.COOKIE_UPDATE, FunctionAuthorityRequestManager.cookieUpdateListener)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import { StringUtils } from "@b2c-m/ohos_hx_utils";
|
||||
import { IStructReceiver, ProtocolDef, StuffBaseStruct, StuffResourceStruct } from '@kernel/lib_communication';
|
||||
import { GroupSubManager, L2DowngradeManager } from 'base_mobilepush';
|
||||
import { HashMap } from '@kit.ArkTS';
|
||||
import { emitter } from '@kit.BasicServicesKit';
|
||||
import { EmitterConstants } from 'biz_common'
|
||||
import { SubscribedPushManager } from "biz_trade";
|
||||
|
||||
export class RealDataStructObserver implements IStructReceiver {
|
||||
constructor() {
|
||||
}
|
||||
|
||||
onIntercept(stuffBaseStruct: StuffBaseStruct): boolean {
|
||||
if (stuffBaseStruct == null) {
|
||||
return false;
|
||||
}
|
||||
const headId = stuffBaseStruct.packageId;
|
||||
if (headId == ProtocolDef.DATAID_NEW_REALTIME) {
|
||||
const dataType = stuffBaseStruct.headType & ProtocolDef.DATA_TYPE_MASK;
|
||||
if (dataType === ProtocolDef.DATA_TYPE_JSON) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
handleStruct(stuffBaseStruct: StuffBaseStruct): boolean {
|
||||
if (!(stuffBaseStruct instanceof StuffResourceStruct)) {
|
||||
return false;
|
||||
}
|
||||
const resStruct = stuffBaseStruct as StuffResourceStruct;
|
||||
const tmpBuffer = resStruct.buffer;
|
||||
const decodedStr = StringUtils.decode(tmpBuffer, "GBK")
|
||||
try {
|
||||
const jsonObject: ESObject = JSON.parse(decodedStr);
|
||||
//解析到分组订阅数据
|
||||
const parsedObj: ESObject = jsonObject["groupdata"];
|
||||
if (parsedObj) {
|
||||
GroupSubManager.getInstance().onReceiveGroupData(JSON.stringify(parsedObj), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 解析到Push订阅数据
|
||||
if (jsonObject[SubscribedPushManager.PARAM_CHANNEL]) {
|
||||
SubscribedPushManager.getInstance().onReceivePushData(decodedStr)
|
||||
}
|
||||
|
||||
const parsedEvents: HashMap<string, object>[] = jsonObject["events"]
|
||||
if (parsedEvents && parsedEvents.length > 0) {
|
||||
for (let event of parsedEvents) {
|
||||
if (event["eventID"] == "00000011") {
|
||||
L2DowngradeManager.getInstance().onReceiveGroupData(JSON.stringify(event));
|
||||
emitter.emit({ eventId: EmitterConstants.EVENT_ID_GET_PERMISSIONSID })
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getReceiverId(): string {
|
||||
return "RealDataStructObserver";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { ISubDataHandler } from 'base_mobilepush'
|
||||
import { AIGroupClient, AI_GROUP_BIZ_NAME } from 'service_aigroup'
|
||||
|
||||
/**
|
||||
* AI分组的数据分组订阅处理器
|
||||
* 参考 iOS LJAIGroupManager 中的 MobiPush 注册机制
|
||||
* ctrlid = future_aigroupconfig
|
||||
*/
|
||||
export class AIGroupPushHandler implements ISubDataHandler {
|
||||
static readonly GROUP_HANDLE_NAME = AI_GROUP_BIZ_NAME
|
||||
|
||||
onReceiveGroupData(data: string): void {
|
||||
try {
|
||||
const pushData: AIGroupPushData = JSON.parse(data) as AIGroupPushData
|
||||
AIGroupClient.getInstance().onAIGroupPushNotification(pushData.ver)
|
||||
} catch (e) {
|
||||
// 解析失败时也触发更新,不传版本号
|
||||
AIGroupClient.getInstance().onAIGroupPushNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface AIGroupPushData {
|
||||
ver?: number
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import { ISubDataHandler } from 'base_mobilepush'
|
||||
import { GlobalContext, HXLog } from 'biz_common'
|
||||
import { ScriptUpdateService } from '@b2c-f/billparsemodule'
|
||||
|
||||
/**
|
||||
* 账户分析 Lua 脚本更新推送处理器
|
||||
*
|
||||
* 处理来自服务端的账户分析 Lua 脚本版本更新通知
|
||||
*/
|
||||
export class AccountAnalysisLuaPushHandler implements ISubDataHandler {
|
||||
static readonly GROUP_HANDLE_NAME = "qhtCapitalLuaUpdate"
|
||||
private static readonly TAG = "AccountAnalysisLuaPush"
|
||||
|
||||
onReceiveGroupData(data: string): void {
|
||||
HXLog.i(AccountAnalysisLuaPushHandler.TAG, `receive: ${data}`)
|
||||
|
||||
try {
|
||||
const pushData: AccountAnalysisLuaPushData = JSON.parse(data) as AccountAnalysisLuaPushData
|
||||
|
||||
if (pushData && pushData.AccountAnalysisLua) {
|
||||
const version = pushData.AccountAnalysisLua.version
|
||||
if (version) {
|
||||
HXLog.i(AccountAnalysisLuaPushHandler.TAG, `收到 Lua 脚本版本更新通知: version=${version}`)
|
||||
this.handleScriptUpdate(version)
|
||||
} else {
|
||||
HXLog.w(AccountAnalysisLuaPushHandler.TAG, '版本号为空')
|
||||
}
|
||||
} else {
|
||||
HXLog.w(AccountAnalysisLuaPushHandler.TAG, '推送数据格式错误')
|
||||
}
|
||||
} catch (error) {
|
||||
HXLog.e(AccountAnalysisLuaPushHandler.TAG, `解析推送数据失败: ${error}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理脚本更新
|
||||
*/
|
||||
private handleScriptUpdate(version: string): void {
|
||||
try {
|
||||
const context = GlobalContext.get()
|
||||
if (!context) {
|
||||
HXLog.e(AccountAnalysisLuaPushHandler.TAG, 'Context 为空,无法更新脚本')
|
||||
return
|
||||
}
|
||||
|
||||
const updateService = new ScriptUpdateService(context)
|
||||
|
||||
// 异步执行更新操作
|
||||
updateService.downloadAndInstall(version)
|
||||
.then((result) => {
|
||||
if (result.success) {
|
||||
if (result.updated) {
|
||||
HXLog.i(AccountAnalysisLuaPushHandler.TAG,
|
||||
`Lua 脚本更新成功: version=${result.version}, scriptPath=${result.scriptPath}`)
|
||||
} else {
|
||||
HXLog.i(AccountAnalysisLuaPushHandler.TAG,
|
||||
`Lua 脚本已是最新版本: version=${result.version}`)
|
||||
}
|
||||
} else {
|
||||
HXLog.e(AccountAnalysisLuaPushHandler.TAG,
|
||||
`Lua 脚本更新失败: ${result.errorMessage}`)
|
||||
}
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
HXLog.e(AccountAnalysisLuaPushHandler.TAG, `Lua 脚本更新异常: ${error.message}`)
|
||||
})
|
||||
} catch (error) {
|
||||
HXLog.e(AccountAnalysisLuaPushHandler.TAG, `处理脚本更新失败: ${error}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 账户分析 Lua 脚本版本信息
|
||||
*/
|
||||
export interface AccountAnalysisLuaVersion {
|
||||
version: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 账户分析 Lua 脚本推送数据格式
|
||||
*/
|
||||
export interface AccountAnalysisLuaPushData {
|
||||
AccountAnalysisLua: AccountAnalysisLuaVersion
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { RandomUtil } from "@pura/harmony-utils";
|
||||
import { ISubDataHandler } from "base_mobilepush";
|
||||
import { HXLog } from "biz_common";
|
||||
import { StringUtil } from "biz_common/src/main/ets/utils/StringUtil";
|
||||
import { HexinVersionControl } from "biz_trade/src/main/ets/utils/HexinVersionControl";
|
||||
import { CodeTableManager } from "lib_codetablemanager";
|
||||
|
||||
export abstract class BaseStorageHandler implements ISubDataHandler {
|
||||
/**
|
||||
* 获取日志tag
|
||||
*/
|
||||
abstract getTag(): string
|
||||
|
||||
/**
|
||||
* 获取推送中的数据更新时间key
|
||||
*/
|
||||
abstract getTimeKey(): string
|
||||
|
||||
/**
|
||||
* 处理升级
|
||||
*/
|
||||
abstract update()
|
||||
|
||||
/**
|
||||
* 获取数据库版本信息
|
||||
*/
|
||||
abstract getDbVersion(): Promise<number>
|
||||
|
||||
private readonly RANDOM_REQUEST_TIME = 10 * 1000
|
||||
|
||||
private mTimeoutId: number | null = null
|
||||
|
||||
async onReceiveGroupData(data: string) {
|
||||
if (HexinVersionControl.disableCodeTableIncrementUpdate()) {
|
||||
return
|
||||
}
|
||||
HXLog.d(CodeTableManager.TAG, `${this.getTag()} onReveiveGroupData: ${data}`)
|
||||
const obj = JSON.parse(data) as Record<string, string>
|
||||
if (!obj) {
|
||||
HXLog.e(CodeTableManager.TAG, `${this.getTag()} onReceiveGroupData obj parse invalid`)
|
||||
return
|
||||
}
|
||||
const time: string = obj[this.getTimeKey()]
|
||||
const dbVersion = await this.getDbVersion()
|
||||
const remoteVersion = StringUtil.toSafeNumber(time, 0)
|
||||
HXLog.d(CodeTableManager.TAG, `${this.getTag()} remoteVersion: ${remoteVersion}, dbVersion: ${dbVersion}`)
|
||||
|
||||
if (StringUtil.isNumber(time) && remoteVersion > dbVersion) {
|
||||
this.randomDelayRequest(remoteVersion)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 随机延时请求
|
||||
*/
|
||||
private randomDelayRequest(remoteVersion: number) {
|
||||
// 取消之前的延迟任务
|
||||
if (this.mTimeoutId !== null) {
|
||||
clearTimeout(this.mTimeoutId)
|
||||
this.mTimeoutId = null
|
||||
}
|
||||
|
||||
const requestDelay: number = RandomUtil.getRandomInt(0, this.RANDOM_REQUEST_TIME)
|
||||
console.debug(CodeTableManager.TAG, `${this.getTag()} requestDelay ${requestDelay}`)
|
||||
|
||||
// 设置延迟任务
|
||||
this.mTimeoutId = setTimeout(() => {
|
||||
this.mTimeoutId = null
|
||||
// 延迟的这段时间,数据库可能被更新了,因此执行前再判断一次,减少不必要的请求
|
||||
this.getDbVersion().then((dbVersion: number) => {
|
||||
HXLog.d(CodeTableManager.TAG, `before update, remoteVersion: ${remoteVersion}, dbVersion: ${dbVersion}`)
|
||||
if (remoteVersion > dbVersion) {
|
||||
this.update()
|
||||
}
|
||||
})
|
||||
}, requestDelay)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { ISubDataHandler } from 'base_mobilepush'
|
||||
import { FunctionAuthorityRequestManager } from '../../functionauthority/FunctionAuthorityRequester'
|
||||
|
||||
interface FunctionAuthorityGroupData {
|
||||
timestamp?: string | number
|
||||
timeStamp?: string | number
|
||||
}
|
||||
|
||||
export class FunctionAuthorityGroupHandler implements ISubDataHandler {
|
||||
static readonly GROUP_HANDLE_NAME = 'futures_custom_authority'
|
||||
|
||||
onReceiveGroupData(data: string, key?: string, isRealPush?: boolean): void {
|
||||
const timestamp = this.parseTimestamp(data)
|
||||
if (timestamp <= 0) {
|
||||
return
|
||||
}
|
||||
FunctionAuthorityRequestManager.requestAuthorityDataForPush(timestamp)
|
||||
}
|
||||
|
||||
private parseTimestamp(data: string | object): number {
|
||||
try {
|
||||
const payload = typeof data === 'string'
|
||||
? JSON.parse(data) as FunctionAuthorityGroupData
|
||||
: data as FunctionAuthorityGroupData
|
||||
return Number(payload.timestamp ?? payload.timeStamp ?? 0)
|
||||
} catch (error) {
|
||||
console.error(`[FunctionAuthorityGroupHandler] 解析权限推送失败: ${JSON.stringify(error)}`)
|
||||
return 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ISubDataHandler } from 'base_mobilepush'
|
||||
import { HXLog } from 'biz_common'
|
||||
import { GrayTestManager } from 'lib_graytestmanager'
|
||||
|
||||
export class GrayTestPushHandler implements ISubDataHandler {
|
||||
static readonly GROUP_HANDLE_NAME = "futures_new_gray_test_config_upgrade_notice"
|
||||
|
||||
onReceiveGroupData(data: string): void {
|
||||
HXLog.i("GrayTest", `GrayTestPushHandler receive: ${data}`)
|
||||
let verTimestamp: PushTimesTamp = JSON.parse(data) as PushTimesTamp
|
||||
if (verTimestamp && verTimestamp.timestamp > 0) {
|
||||
GrayTestManager.getInstance().dealReceiveMobilePush(verTimestamp.timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface PushTimesTamp {
|
||||
timestamp: number
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { AdsPopupManager } from "@b2c-f/fuhm-ads"
|
||||
import { AdsPopupHost } from "@b2c/first_page"
|
||||
import { ISubDataHandler } from "base_mobilepush"
|
||||
import { HXLog } from "biz_common"
|
||||
|
||||
/**
|
||||
* author : liuqingliang@myhexin.com
|
||||
* time : created on 2026/6/9
|
||||
* desc : 全域运营位-只会推送一次,不会实时推送
|
||||
*/
|
||||
export class QhStrategyAdPagePushHandler implements ISubDataHandler {
|
||||
static readonly GROUP_HANDLE_NAME = 'app_page_info'
|
||||
private static readonly KEY_TIME = "app_6"
|
||||
|
||||
onReceiveGroupData(data: string) {
|
||||
try {
|
||||
const pushData = JSON.parse(data) as Record<string, string>
|
||||
const timeStamp = pushData[QhStrategyAdPagePushHandler.KEY_TIME] ?? ''
|
||||
|
||||
if (timeStamp === AdsPopupManager.pushTimestamp) {
|
||||
return
|
||||
}
|
||||
AdsPopupHost.getInstance().requestPageInfo(timeStamp)
|
||||
} catch (error) {
|
||||
HXLog.e('QhStrategyAdPushHandler', `Failed to process push: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import { AdsManager, ADS_TYPE_INDEX_POPUP, ADS_TYPE_QHTMKTPOP } from "@b2c-f/fuhm-ads"
|
||||
import { AdsPopupHost, FirstPagePopupAdHelper } from "@b2c/first_page"
|
||||
import { ISubDataHandler } from "base_mobilepush"
|
||||
import { HXLog, PreferenceService } from "biz_common"
|
||||
import { HXUserService } from "biz_hxservice"
|
||||
|
||||
|
||||
/**
|
||||
* <pre>
|
||||
* author: ME
|
||||
* desc: 期货策略广告推送处理器,对齐 Android QhStragyAdPushHandler
|
||||
* createTime: 2026/06/01
|
||||
* </pre>
|
||||
*/
|
||||
export class QhStrategyAdPushHandler implements ISubDataHandler {
|
||||
static readonly GROUP_HANDLE_NAME = 'qh_strategy_ad_push'
|
||||
private static readonly PUSH_STRATEGY_AD_KEY = 'push_stragy_ad'
|
||||
private static readonly SP_STRATEGY_AD = "sp_stragy_ad"
|
||||
private static readonly KEY_AD_TYPE = "adtype"
|
||||
private static readonly KEY_AD_IDS = "id"
|
||||
|
||||
onReceiveGroupData(data: string) {
|
||||
try {
|
||||
const pushData = JSON.parse(data) as Record<string, string>
|
||||
const adType = pushData[QhStrategyAdPushHandler.KEY_AD_TYPE] ?? ''
|
||||
const adId = pushData[QhStrategyAdPushHandler.KEY_AD_IDS] ?? ''
|
||||
|
||||
if (!adType || !adId) {
|
||||
HXLog.w('QhStrategyAdPushHandler', 'Invalid push data, missing adType or adId')
|
||||
return
|
||||
}
|
||||
|
||||
const pushKey = `${adType}_${adId}`
|
||||
const userId = HXUserService.getInstance().getUserIdFromLocal()
|
||||
|
||||
const preferenceKey = `${QhStrategyAdPushHandler.PUSH_STRATEGY_AD_KEY}_${userId}`
|
||||
const localPushKey = PreferenceService.getStringValueSync(QhStrategyAdPushHandler.SP_STRATEGY_AD,preferenceKey, '')
|
||||
if (pushKey === localPushKey) {
|
||||
HXLog.d('QhStrategyAdPushHandler', `Already pushed: ${pushKey}`)
|
||||
return
|
||||
}
|
||||
PreferenceService.putVal(QhStrategyAdPushHandler.SP_STRATEGY_AD,preferenceKey, pushKey, true)
|
||||
|
||||
if (adType === ADS_TYPE_INDEX_POPUP) {
|
||||
// 收到策略广告后重置启动弹窗过的标志
|
||||
FirstPagePopupAdHelper.getInstance().isShowedPopupAds = false
|
||||
}
|
||||
|
||||
if (adType === ADS_TYPE_QHTMKTPOP) {
|
||||
// 期货营销悬浮球-处理策略广告推送
|
||||
AdsPopupHost.getInstance().handlePush()
|
||||
}
|
||||
|
||||
AdsManager.refreshAdsAfterPushByAdType(adType, adId)
|
||||
} catch (error) {
|
||||
HXLog.e('QhStrategyAdPushHandler', `Failed to process push: ${error.message}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ISubDataHandler } from 'base_mobilepush';
|
||||
import { UgcClient } from '../../register/ugcregister/UgcClient';
|
||||
|
||||
/**
|
||||
* 自选分组的数据分组订阅
|
||||
* @author menghuiyuan@myhexin.com
|
||||
*/
|
||||
export class SelfCodeGroupHandler implements ISubDataHandler {
|
||||
static readonly GROUP_HANDLE_NAME = "blockstock"
|
||||
onReceiveGroupData(data: string): void {
|
||||
let version:groupUpdate = JSON.parse(data) as groupUpdate
|
||||
UgcClient.getInstance().onSelfGroupRemoteVersion(version.ver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface groupUpdate{
|
||||
ver:number
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { ISubDataHandler } from 'base_mobilepush';
|
||||
import { UgcClient } from '../../register/ugcregister/UgcClient';
|
||||
import { groupUpdate } from './SelfCodeGroupHandler';
|
||||
/**
|
||||
* 默认自选数据分组订阅
|
||||
* @author zhangshan2@myhexin.com
|
||||
*/
|
||||
export class SelfCodeMainListHandler implements ISubDataHandler {
|
||||
static readonly GROUP_HANDLE_NAME = "futuresselfcode"
|
||||
onReceiveGroupData(data: string): void {
|
||||
let version:groupUpdate = JSON.parse(data) as groupUpdate
|
||||
UgcClient.getInstance().onSelfCodeRemoteVersion(version.ver)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import { ISubDataHandler } from 'base_mobilepush'
|
||||
import { WxPaySidPermissionMarker } from 'biz_auth'
|
||||
import { HXLog, PreferenceService } from 'biz_common'
|
||||
import { SidPermissionRefreshClient } from 'biz_auth/src/main/ets/auth/sid/SidPermissionRefreshClient'
|
||||
import { HXUserService } from 'biz_hxservice'
|
||||
|
||||
const TAG = 'SidGroupHandler'
|
||||
|
||||
/**
|
||||
* SID权限更新分组推送数据。
|
||||
* 对应分组key: futures_app_sid_update_notice
|
||||
* timeStamp为服务端下发的更新标记,用于和本地已处理标记比较,变化时主动刷新SID权限。
|
||||
*/
|
||||
interface SidGroupData {
|
||||
timeStamp?: string | number
|
||||
}
|
||||
|
||||
export class SidGroupHandler implements ISubDataHandler {
|
||||
static readonly GROUP_HANDLE_NAME = 'futures_app_sid_update_notice'
|
||||
private static readonly PREFERENCES_NAME = 'sid_update_notice'
|
||||
private static readonly KEY_PREFIX = 'sid_update_'
|
||||
|
||||
onReceiveGroupData(data: string): void {
|
||||
try {
|
||||
// 主站只用timeStamp表达“SID权限有新版本”,客户端不解析具体权限内容。
|
||||
const flag = this.parsePushFlag(data)
|
||||
if (!flag || flag.length === 0) {
|
||||
return
|
||||
}
|
||||
const userId = HXUserService.getInstance().getUserId()
|
||||
// 本地仅记录已成功处理的timeStamp,避免同一条分组推送重复触发主动请求。
|
||||
const savedFlag = PreferenceService.getStringValueSync(
|
||||
SidGroupHandler.PREFERENCES_NAME,
|
||||
SidGroupHandler.KEY_PREFIX + userId,
|
||||
''
|
||||
)
|
||||
if (flag === savedFlag) {
|
||||
return
|
||||
}
|
||||
// 主动请求成功且SID发生变化后,SidPermissionRefreshClient会写入本次flag。
|
||||
WxPaySidPermissionMarker.clear()
|
||||
SidPermissionRefreshClient.requestPermission(true, flag)
|
||||
} catch (error) {
|
||||
HXLog.e(TAG, `handle sid group push failed: ${JSON.stringify(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
private parsePushFlag(data: string | object): string {
|
||||
if (!data) {
|
||||
return ''
|
||||
}
|
||||
let payload: SidGroupData | null = null
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
payload = JSON.parse(data) as SidGroupData
|
||||
} catch (error) {
|
||||
HXLog.e(TAG, `parse sid group push failed: ${JSON.stringify(error)}`)
|
||||
return ''
|
||||
}
|
||||
} else {
|
||||
// 分组分发链路可能已把payload解析为对象,这里兼容两种输入形态。
|
||||
payload = data as SidGroupData
|
||||
}
|
||||
const flag = payload?.timeStamp
|
||||
if (flag === undefined || flag === null) {
|
||||
return ''
|
||||
}
|
||||
return flag.toString()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { AppUtil } from "@pura/harmony-utils";
|
||||
import { HXLog } from "biz_common";
|
||||
import { CodeTableManager } from "lib_codetablemanager";
|
||||
import { BaseStorageHandler } from "./BaseStorageHandler";
|
||||
|
||||
export class StockGroupHandler extends BaseStorageHandler {
|
||||
getTag(): string {
|
||||
return 'StockGroupHandler'
|
||||
}
|
||||
|
||||
getTimeKey(): string {
|
||||
return 'time'
|
||||
}
|
||||
|
||||
update(): void {
|
||||
HXLog.d(CodeTableManager.TAG, `${this.getTag()} update`)
|
||||
CodeTableManager.get().requestStock(AppUtil.getContext(), this.getTag())
|
||||
}
|
||||
|
||||
async getDbVersion(): Promise<number> {
|
||||
const version = await CodeTableManager.get().getStockInfoDbVersion()
|
||||
HXLog.d(CodeTableManager.TAG, `${this.getTag()} getDbVersion ${version}`)
|
||||
return version
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { AppUtil } from "@pura/harmony-utils";
|
||||
import { HXLog } from "biz_common";
|
||||
import { CodeTableManager } from "lib_codetablemanager";
|
||||
import { BaseStorageHandler } from "./BaseStorageHandler";
|
||||
|
||||
export class VarietyGroupHandler extends BaseStorageHandler {
|
||||
getTag(): string {
|
||||
return 'VarietyGroupHandler'
|
||||
}
|
||||
|
||||
getTimeKey(): string {
|
||||
return 'timestamp'
|
||||
}
|
||||
|
||||
update(): void {
|
||||
HXLog.d(CodeTableManager.TAG, `${this.getTag()} update`)
|
||||
CodeTableManager.get().requestVariety(AppUtil.getContext(), this.getTag())
|
||||
}
|
||||
|
||||
async getDbVersion(): Promise<number> {
|
||||
const version = await CodeTableManager.get().getVarietyInfoDbVersion()
|
||||
HXLog.d(CodeTableManager.TAG, `${this.getTag()} getDbVersion ${version}`)
|
||||
return version
|
||||
}
|
||||
|
||||
}
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.5 KiB |
|
After Width: | Height: | Size: 486 B |
|
After Width: | Height: | Size: 1.4 KiB |
|
After Width: | Height: | Size: 197 B |
|
After Width: | Height: | Size: 513 B |
|
After Width: | Height: | Size: 503 B |
|
After Width: | Height: | Size: 316 B |
|
After Width: | Height: | Size: 1011 B |
|
After Width: | Height: | Size: 948 B |
|
After Width: | Height: | Size: 930 B |
|
After Width: | Height: | Size: 950 B |
|
After Width: | Height: | Size: 930 B |
|
After Width: | Height: | Size: 919 B |
|
After Width: | Height: | Size: 698 B |
|
After Width: | Height: | Size: 693 B |
|
After Width: | Height: | Size: 435 B |
|
After Width: | Height: | Size: 384 B |
|
After Width: | Height: | Size: 439 B |
|
After Width: | Height: | Size: 389 B |
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1,103 @@
|
||||
|
||||
import {
|
||||
AdsManager,
|
||||
AdRuntimeConfig
|
||||
} from '@b2c-f/fuhm-ads'
|
||||
import { AppConfigManager, HXLog, ScreenUtil } from 'biz_common'
|
||||
import { CookieManager, HXUserService, KaihuFlagManager, NewUserManager, ServerTimeProvider,
|
||||
UserSourceManager } from 'biz_hxservice'
|
||||
import { AppUtil } from '@pura/harmony-utils'
|
||||
|
||||
const TAG = 'AdsInitializer'
|
||||
|
||||
/**
|
||||
* FuHmAds 运行时配置
|
||||
* 提供广告组件所需的上下文、用户信息和业务能力
|
||||
*/
|
||||
class AdsRuntimeConfig implements AdRuntimeConfig {
|
||||
|
||||
|
||||
getContext(): Object | null {
|
||||
return AppUtil.getContext()
|
||||
}
|
||||
|
||||
getScreenWidth(): string {
|
||||
return ScreenUtil.get().getScreenWidth().toString()
|
||||
}
|
||||
|
||||
getScreenHeight(): string {
|
||||
return ScreenUtil.get().getScreenHeight().toString()
|
||||
}
|
||||
|
||||
getInnerVersion(): string {
|
||||
return AppConfigManager.getInstance().getConfigProvider()?.getInnerVersionMiddleNumberStr()??''
|
||||
}
|
||||
|
||||
getComeFrom(): string {
|
||||
return KaihuFlagManager.getInstance().getKaihuFlag()
|
||||
}
|
||||
|
||||
getTrade(): string {
|
||||
let userSource = UserSourceManager.getInstance().getUserSource()
|
||||
if (UserSourceManager.NO_SOURCE != userSource) {
|
||||
return userSource
|
||||
}else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
getDeviceCode(): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
getUserId(): string {
|
||||
const userId = HXUserService.getInstance().getUserId()
|
||||
return userId || ''
|
||||
}
|
||||
|
||||
getUserIdFromPassport(): string {
|
||||
const userId = HXUserService.getInstance().getUserIdFromLocal()
|
||||
return userId || ''
|
||||
}
|
||||
|
||||
isNewUser(): boolean {
|
||||
return NewUserManager.getInstance().isNewUser()
|
||||
}
|
||||
|
||||
getChannelId(): string {
|
||||
return NewUserManager.getChannelId()
|
||||
}
|
||||
|
||||
getCurrentTimeMillis(): number {
|
||||
return ServerTimeProvider.getInstance().getServerTimeMillis()
|
||||
}
|
||||
|
||||
|
||||
getUserAgent(): string {
|
||||
return AppConfigManager.getInstance().getUA()
|
||||
}
|
||||
|
||||
getCookie(): string {
|
||||
return CookieManager.getInstance().getCookieSync()
|
||||
}
|
||||
|
||||
|
||||
uploadElk(message: string): void {
|
||||
HXLog.w(TAG, `Ads elk ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 广告模块初始化器
|
||||
*/
|
||||
export class AdsInitializer {
|
||||
/**
|
||||
* 初始化广告模块
|
||||
*/
|
||||
static initAds(): void {
|
||||
HXLog.i(TAG, 'initAds start')
|
||||
const adsConfig = new AdsRuntimeConfig()
|
||||
AdsManager.initAdContext(adsConfig)
|
||||
HXLog.i(TAG, 'initAds done')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { HXLog } from "biz_common"
|
||||
import { CloudStoreLogManager, ICloudStoreLogger } from "lib_cloud_store"
|
||||
import { ElkService, ElkUploadMessage, ElkUploadMessageBuilder } from '@b2c-m/ohos_elk_logger_upload'
|
||||
import { HXUserService } from 'biz_hxservice'
|
||||
|
||||
const CLOUD_STORE = "cloud_store"
|
||||
|
||||
/**
|
||||
* 云存储错误消息构建类
|
||||
*/
|
||||
class CloudStoreErrMessageBuilder extends ElkUploadMessageBuilder {
|
||||
constructor(bizLevel: string) {
|
||||
super(Date.now(), CLOUD_STORE, bizLevel)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传云存储错误信息到 ELK
|
||||
* @param errorMessage 错误信息
|
||||
*/
|
||||
function uploadCloudStoreError(error: string) {
|
||||
const errorMessageBuilder = new CloudStoreErrMessageBuilder('e')
|
||||
errorMessageBuilder.userid(HXUserService.getInstance().getUserId())
|
||||
errorMessageBuilder.messageKey('CloudStore')
|
||||
errorMessageBuilder.messageValue(error)
|
||||
const message = new ElkUploadMessage(errorMessageBuilder)
|
||||
ElkService.getInstance().pushBusinessLog(message, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* HXLog 适配器
|
||||
* 将 lib_cloud_store 的日志桥接到 HXLog
|
||||
*/
|
||||
class HXLogAdapter implements ICloudStoreLogger {
|
||||
debug(module: string, message: string): void {
|
||||
HXLog.d(module, message)
|
||||
}
|
||||
|
||||
info(module: string, message: string): void {
|
||||
HXLog.i(module, message)
|
||||
}
|
||||
|
||||
warn(module: string, message: string): void {
|
||||
HXLog.w(module, message)
|
||||
}
|
||||
|
||||
error(module: string, message: string): void {
|
||||
HXLog.e(module, message)
|
||||
uploadCloudStoreError(`[${module}] ${message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 云存储日志初始化器
|
||||
*/
|
||||
export class CloudStoreLogInitializor {
|
||||
/**
|
||||
* 初始化云存储日志
|
||||
*/
|
||||
static initCloudStoreLog() {
|
||||
const hxLogAdapter = new HXLogAdapter()
|
||||
CloudStoreLogManager.getInstance().setLogger(hxLogAdapter)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import {AppInfoConfig, IAppConfig, IDeviceConfig } from 'biz_common';
|
||||
export class AppConfigProvider implements IAppConfig {
|
||||
|
||||
private static readonly BEGIN_INDEX = 5
|
||||
getBundleName(): string {
|
||||
return AppInfoConfig.getBundleName();
|
||||
}
|
||||
|
||||
|
||||
getInnerVersionFull(): string {
|
||||
return AppInfoConfig.APPLET_INNER_VERSION_FULL;
|
||||
}
|
||||
|
||||
getInnerVersionMiddle(): string {
|
||||
return AppInfoConfig.APPLET_INNER_VERSION.substring(AppConfigProvider.BEGIN_INDEX -1)
|
||||
}
|
||||
|
||||
getInnerVersionMiddleNumberStr(): string {
|
||||
return AppInfoConfig.APPLET_INNER_VERSION.substring(AppConfigProvider.BEGIN_INDEX).replaceAll(".","")
|
||||
}
|
||||
|
||||
getOuterVersion(): string {
|
||||
|
||||
return AppInfoConfig.getVersionName();
|
||||
}
|
||||
|
||||
getVersionCode(): string {
|
||||
return AppInfoConfig.getVersionCode();
|
||||
}
|
||||
|
||||
getAppPlatform(): string {
|
||||
return AppInfoConfig.APP_PLATFORM;
|
||||
}
|
||||
|
||||
getAppSign(): string {
|
||||
return AppInfoConfig.APP_FOR;
|
||||
}
|
||||
|
||||
getProgramId(): string {
|
||||
return AppInfoConfig.APP_PROGID;
|
||||
}
|
||||
|
||||
getChannelId(): string {
|
||||
return AppInfoConfig.APP_CHANNEL_ID;
|
||||
}
|
||||
|
||||
getSourceId(): string {
|
||||
return AppInfoConfig.APP_CHANNEL_ID;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class DeviceConfigProvider implements IDeviceConfig {
|
||||
getDeviceWidth(): number {
|
||||
return 720;
|
||||
}
|
||||
|
||||
getDeviceHeight(): number {
|
||||
return 1440;
|
||||
}
|
||||
|
||||
getDensity(): number {
|
||||
return 3.0;
|
||||
}
|
||||
|
||||
getDeviceType(): string {
|
||||
return "android";
|
||||
}
|
||||
|
||||
getIMEI(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getIMSI(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
|
||||
getMACAddress(): string {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
}
|
||||