diff --git a/GrowingAnalytics/src/main/ets/components/core/AnalyticsCore.ets b/GrowingAnalytics/src/main/ets/components/core/AnalyticsCore.ets index e55ac89f..d30e6d1b 100644 --- a/GrowingAnalytics/src/main/ets/components/core/AnalyticsCore.ets +++ b/GrowingAnalytics/src/main/ets/components/core/AnalyticsCore.ets @@ -379,6 +379,10 @@ export default class AnalyticsCore implements GrowingAnalyticsInterface { Plugins.onEventWroteToDisk(pst, eventScene) if (pst.eventType == EventType.Visit) { EventSender.sendEvent(context) + } else if (DeviceInfo.isWearable && pst.eventType == EventType.AppClosed) { + // 手表退到后台后会被系统快速冻结,定时上报大概率等不到, + // 事件会积压到下次冷启动。因此在生成 APP_CLOSED 后立即冲刷一次。 + EventSender.sendEvent(context) } } diff --git a/GrowingAnalytics/src/main/ets/components/core/DeviceInfo.ets b/GrowingAnalytics/src/main/ets/components/core/DeviceInfo.ets index 0c35de14..4a1c6483 100644 --- a/GrowingAnalytics/src/main/ets/components/core/DeviceInfo.ets +++ b/GrowingAnalytics/src/main/ets/components/core/DeviceInfo.ets @@ -34,14 +34,23 @@ import { ConfigMode, IgnoreFields } from '../interfaces/GrowingConfig' export const SDK_PLATFORM: string = 'HarmonyOS' +export const DEVICE_TYPE_WEARABLE: string = 'wearable' + +// 手表主流分辨率为 466 x 466(圆屏取外接正方形) +const WEARABLE_DEFAULT_SCREEN_SIZE: number = 466 + export default class DeviceInfo { - static defaultScreenHeight: number = 1260 - static defaultScreenWidth: number = 2720 + static defaultScreenHeight: number = 2720 + static defaultScreenWidth: number = 1260 static defaultPlatformVersion: string = '5.0.0' static defaultDeviceBrand: string = 'HUAWEI' static defaultDeviceModel: string = '-' static defaultDeviceType: string = 'phone' + // 设备形态,仅用于 SDK 内部策略(兜底值、上报时机),不随事件上报, + // 因此不受 IgnoreFields.DeviceType 约束 + static isWearable: boolean = false + static deviceId: string = '' static platform: string = '' static platformVersion?: string = '' @@ -59,6 +68,13 @@ export default class DeviceInfo { static initDeviceInfo(context: GrowingContext) { DeviceInfo.platform = SDK_PLATFORM + DeviceInfo.isWearable = niceTry(() => deviceInfo.deviceType, '') == DEVICE_TYPE_WEARABLE + if (DeviceInfo.isWearable) { + DeviceInfo.defaultScreenHeight = WEARABLE_DEFAULT_SCREEN_SIZE + DeviceInfo.defaultScreenWidth = WEARABLE_DEFAULT_SCREEN_SIZE + DeviceInfo.defaultDeviceType = DEVICE_TYPE_WEARABLE + } + if (DeviceInfo.isNotIgnoreField(context, IgnoreFields.ScreenSize)) { let displayInfo = niceTry(() => display.getDefaultDisplaySync()) if (displayInfo) { @@ -137,6 +153,10 @@ export default class DeviceInfo { } else if (data.netCap.bearerTypes[0] == connection.NetBearType.BEARER_ETHERNET) { LogUtil.info(() => 'NetCapability change to Ethernet') DeviceInfo.networkState = 'WIFI' //Ethernet + } else if (data.netCap.bearerTypes[0] == connection.NetBearType.BEARER_BLUETOOTH) { + // 手表默认网络优先级为「蓝牙 > WIFI > 蜂窝」,蓝牙代理网络归为 WIFI + LogUtil.info(() => 'NetCapability change to Bluetooth') + DeviceInfo.networkState = 'WIFI' //Bluetooth } }) } else { diff --git a/GrowingAnalytics/src/main/module.json5 b/GrowingAnalytics/src/main/module.json5 index fbca4a21..67719b00 100644 --- a/GrowingAnalytics/src/main/module.json5 +++ b/GrowingAnalytics/src/main/module.json5 @@ -5,7 +5,8 @@ "deviceTypes": [ "default", "tablet", - "2in1" + "2in1", + "wearable" ], "metadata": [ { diff --git a/GrowingToolsKit/src/main/module.json5 b/GrowingToolsKit/src/main/module.json5 index 9f9b6c46..f793f46e 100644 --- a/GrowingToolsKit/src/main/module.json5 +++ b/GrowingToolsKit/src/main/module.json5 @@ -5,7 +5,8 @@ "deviceTypes": [ "default", "tablet", - "2in1" + "2in1", + "wearable" ], "metadata": [ { diff --git a/build-profile.json5 b/build-profile.json5 index 9a031ca4..1fb072d6 100644 --- a/build-profile.json5 +++ b/build-profile.json5 @@ -37,6 +37,18 @@ } ] }, + { + "name": "entry_wearable", + "srcPath": "./entry_wearable", + "targets": [ + { + "name": "default", + "applyToProducts": [ + "default" + ] + } + ] + }, { "name": "GrowingAnalytics", "srcPath": "./GrowingAnalytics", diff --git a/docs/GrowingAnalytics/core/AnalyticsCore.md b/docs/GrowingAnalytics/core/AnalyticsCore.md index e106b04d..5f4b2b6c 100644 --- a/docs/GrowingAnalytics/core/AnalyticsCore.md +++ b/docs/GrowingAnalytics/core/AnalyticsCore.md @@ -412,10 +412,24 @@ static async writeEventToDisk( // 4. VISIT 事件立即触发发送 if (pst.eventType == EventType.Visit) { EventSender.sendEvent(context) + } else if (DeviceInfo.isWearable && pst.eventType == EventType.AppClosed) { + // 手表:APP_CLOSED(进入后台)立即触发发送 + EventSender.sendEvent(context) } } ``` +### 立即上报的时机 + +除 `setInterval` 定时上报外,以下两种事件写入磁盘后会立即触发一次上报: + +| 事件 | 适用设备 | 原因 | +|---|---|---| +| `VISIT` | 全部 | 保证会话起点及时到达服务端 | +| `APP_CLOSED` | 仅手表(`DeviceInfo.isWearable`) | 手表退到后台后会被系统快速冻结,定时器大概率等不到,事件会积压到下次冷启动 | + +手表侧该行为不受 `dataUploadInterval` 配置影响 —— 调大间隔省电的同时,进入后台仍会冲刷一次,两者互补。手机/平板/PC 不做此处理,维持原有的定时上报节奏。 + ### 写入流程图 ``` diff --git a/docs/GrowingAnalytics/core/DeviceInfo.md b/docs/GrowingAnalytics/core/DeviceInfo.md index 21e9445d..ebc0bc0b 100644 --- a/docs/GrowingAnalytics/core/DeviceInfo.md +++ b/docs/GrowingAnalytics/core/DeviceInfo.md @@ -73,14 +73,28 @@ SDK 初始化 ### 默认值 ```typescript -static defaultScreenHeight: number = 1260 -static defaultScreenWidth: number = 2720 +static defaultScreenHeight: number = 2720 +static defaultScreenWidth: number = 1260 static defaultPlatformVersion: string = '5.0.0' static defaultDeviceBrand: string = 'HUAWEI' static defaultDeviceModel: string = '-' static defaultDeviceType: string = 'phone' ``` +上表为手机形态的默认值。手表(`deviceType == 'wearable'`)在 `initDeviceInfo()` 中会覆盖其中三项: + +| 字段 | 手机 | 手表 | +|---|---|---| +| `defaultScreenHeight` | 2720 | 466 | +| `defaultScreenWidth` | 1260 | 466 | +| `defaultDeviceType` | `'phone'` | `'wearable'` | + +手机默认值取竖屏形态(宽 1260 × 高 2720),与 `orientation` 的默认值 `'PORTRAIT'` 保持一致。 + +默认值仅在 `display.getDefaultDisplaySync()` 等系统 API 取值失败时兜底,且只被圈选(`Hybrid`)与移动调试(`WebSocket`)使用,不进入事件上报字段。 + +此外 `DeviceInfo.isWearable` 记录设备形态,供 SDK 内部策略使用(兜底值、上报时机,见 [`AnalyticsCore.md`](./AnalyticsCore.md))。该字段不随事件上报,因此不受 `IgnoreFields.DeviceType` 约束。 + ### 初始化流程 ```typescript @@ -88,6 +102,14 @@ static initDeviceInfo(context: GrowingContext) { // 1. 设置平台 DeviceInfo.platform = SDK_PLATFORM // 'HarmonyOS' + // 1.1 设备形态:手表覆盖默认值 + DeviceInfo.isWearable = niceTry(() => deviceInfo.deviceType, '') == 'wearable' + if (DeviceInfo.isWearable) { + DeviceInfo.defaultScreenHeight = 466 + DeviceInfo.defaultScreenWidth = 466 + DeviceInfo.defaultDeviceType = 'wearable' + } + // 2. 屏幕信息 if (DeviceInfo.isNotIgnoreField(context, IgnoreFields.ScreenSize)) { let displayInfo = niceTry(() => display.getDefaultDisplaySync()) @@ -345,6 +367,8 @@ static initNetworkState(context: GrowingContext) { DeviceInfo.networkState = 'WIFI' } else if (bearerType == connection.NetBearType.BEARER_ETHERNET) { DeviceInfo.networkState = 'WIFI' // Ethernet 归类为 WIFI + } else if (bearerType == connection.NetBearType.BEARER_BLUETOOTH) { + DeviceInfo.networkState = 'WIFI' // 蓝牙代理网络归类为 WIFI } }) } else { @@ -355,12 +379,16 @@ static initNetworkState(context: GrowingContext) { ### 网络类型映射 -| 系统网络类型 | SDK 网络状态 | -|-------------|-------------| -| BEARER_CELLULAR | 5G (简化处理) | -| BEARER_WIFI | WIFI | -| BEARER_ETHERNET | WIFI | -| 无网络 | UNKNOWN | +| 系统网络类型 | SDK 网络状态 | 说明 | +|-------------|-------------|------| +| BEARER_CELLULAR | 5G | 简化处理,未细分制式 | +| BEARER_WIFI | WIFI | | +| BEARER_ETHERNET | WIFI | | +| BEARER_BLUETOOTH | WIFI | 手表默认网络优先级为「蓝牙 > WIFI > 蜂窝」,蓝牙代理网络归为 WIFI | +| 其他(含 BEARER_VPN) | 保持上一个值 | 无 else 分支,不覆盖 | +| 无网络 | UNKNOWN | | + +> **已知行为**:`networkState` 初始值为 `'UNKNOWN'`,由异步的 `netCapabilitiesChange` 回调填充;而 VISIT 事件在 `initDeviceInfo()` 的同步调用栈中生成(`AnalyticsCore.startCore` → `Session.refreshSession` → `Session.generateVisit`),回调赶不上。因此**每次冷启动的首个 VISIT 事件的 `networkState` 为 `UNKNOWN`**,其后的事件均为真实值。这是权衡后接受的行为(同步查询网络状态需两次 IPC,会挤占初始化的主线程预算),不是缺陷。 ### 实时更新 diff --git a/entry_wearable/.gitignore b/entry_wearable/.gitignore new file mode 100644 index 00000000..e2713a27 --- /dev/null +++ b/entry_wearable/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/oh_modules +/.preview +/build +/.cxx +/.test \ No newline at end of file diff --git a/entry_wearable/README.md b/entry_wearable/README.md new file mode 100644 index 00000000..42301b98 --- /dev/null +++ b/entry_wearable/README.md @@ -0,0 +1,67 @@ +# entry_wearable — 手表端示例应用 + +面向 HarmonyOS 智能手表(`deviceTypes: ["wearable"]`)的 GrowingAnalytics 接入示例。 + +手机端示例见 `entry` 模块。两者刻意拆开:按官方多设备工程结构,"一次开发多端部署"只在业务逻辑层与数据层复用,UI 与交互层仍需按设备分别开发。 + +## 与手机端 demo 的关键差异 + +| 维度 | entry(手机) | entry_wearable(手表) | +|---|---|---| +| 无埋点 | 开启 | **不开启** | +| `GrowingAnalytics.onWindowStageCreate` | 调用 | **不调用** | +| `autotrackEnabled` | `true` | `false` | +| `hybridAutotrackEnabled` | 默认 `true` | `false`(无 WebView 场景) | +| GrowingToolsKit 插件 | 挂载 | 不挂载(悬浮窗形态,手表放不下) | +| `dataUploadInterval` | 15s | 60s(减少网络唤醒,照顾续航) | +| PAGE 事件 | 无埋点自动产生 | **不产生**,且不新增手动接口(见下) | + +`dataUploadInterval` 敢调到 60s,是因为 SDK 侧对手表做了特殊处理:**进入后台生成 `APP_CLOSED` 后会立即冲刷一次上报**(`AnalyticsCore.writeEventToDisk`,仅 `deviceType == 'wearable'` 生效)。手表落腕后会被系统快速冻结,只靠定时器大概率等不到下一次触发,事件会积压到下次冷启动。两者互补:平时低频省电,退出时保证送达。 + +## 如何关掉无埋点 + +SDK 有两道独立开关,本 demo 两道都关: + +1. **`config.autotrackEnabled = false`** —— 默认值即为 `false`。它控制回调内部是否继续处理,但**不阻止监听注册**。 +2. **不调用 `GrowingAnalytics.onWindowStageCreate(this, windowStage)`** —— 这才是决定性的一步。 + +第 2 点是关键。该方法是无埋点 UI 监听的唯一注册入口,内部会挂上 4 个 `UIObserver` 回调: + +``` +willClick → AutotrackClick.onWillClick +navDestinationUpdate → AutotrackPage.onPageUpdate +navDestinationSwitch → AutotrackPage.onPageUpdate +routerPageUpdate → AutotrackPage.onPageUpdate +``` + +只设 `autotrackEnabled = false` 而仍然调用它,这 4 个回调依然会注册,每次点击和路由跳转都会进入 SDK 回调再被开关拦下 —— 白白付出唤醒成本。手表 CPU 与续航都更紧张,不调用即零注册,是更干净的做法。 + +不调用不影响其余功能:手动埋点、会话管理、设备信息、事件入库与上报走的是另一条链路(`AnalyticsCore.setLifecycleCallback` 内部注册,与 `Autotrack` 无关)。 + +## 没有 PAGE 事件 + +不调用 `onWindowStageCreate` 就没有无埋点,SDK 也不再产生 PAGE 事件;公开 API 中没有手动上报页面浏览的接口(`trackFlutterPage` 仅供 Flutter 桥接使用)。手表端不打算为此新增 PAGE 接口——页面层级浅、停留短,分析价值不足以支撑一个新的公开 API。 + +确需页面维度可以自己发自定义事件,但要清楚它**不是 PAGE 事件**:`eventType` 为 `Custom`,进不了页面分析,属性也不会映射到 `PageEvent` 的 `path` / `title` / `timestamp` 等顶层字段。本 demo 不演示这种写法,以免被当成推荐 schema。 + +## 构建 + +```bash +/Applications/DevEco-Studio.app/Contents/tools/node/bin/node \ + /Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw.js \ + --mode module -p module=entry_wearable@default -p product=default \ + -p requiredDeviceType=wearable assembleHap \ + --analyze=normal --parallel --incremental --daemon +``` + +产物:`entry_wearable/build/default/outputs/default/entry_wearable-default-signed.hap` + +## 尚未验证 + +以下需要手表模拟器或真机确认,编译期查不到: + +- `deviceInfo.deviceType` 实际返回值是否为 `wearable`,以及后端埋点协议是否接受该枚举 +- 圆形屏幕下 `display.getDefaultDisplaySync()` 的返回值 +- 弱网/息屏场景下事件入库与补发行为 + +demo 首页已把 `deviceType` 和 `deviceId` 直接显示在界面上,便于真机核对第一项。 diff --git a/entry_wearable/build-profile.json5 b/entry_wearable/build-profile.json5 new file mode 100644 index 00000000..2014fa25 --- /dev/null +++ b/entry_wearable/build-profile.json5 @@ -0,0 +1,25 @@ +{ + "apiType": "stageMode", + "buildOption": { + }, + "buildOptionSet": [ + { + "name": "release", + "arkOptions": { + "obfuscation": { + "ruleOptions": { + "enable": true, + "files": [ + "./obfuscation-rules.txt" + ] + } + } + }, + }, + ], + "targets": [ + { + "name": "default" + } + ] +} diff --git a/entry_wearable/hvigorfile.ts b/entry_wearable/hvigorfile.ts new file mode 100644 index 00000000..c6edcd90 --- /dev/null +++ b/entry_wearable/hvigorfile.ts @@ -0,0 +1,6 @@ +import { hapTasks } from '@ohos/hvigor-ohos-plugin'; + +export default { + system: hapTasks, /* Built-in plugin of Hvigor. It cannot be modified. */ + plugins:[] /* Custom plugin to extend the functionality of Hvigor. */ +} diff --git a/entry_wearable/obfuscation-rules.txt b/entry_wearable/obfuscation-rules.txt new file mode 100644 index 00000000..e9c21f29 --- /dev/null +++ b/entry_wearable/obfuscation-rules.txt @@ -0,0 +1,23 @@ +# 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 + +-enable-property-obfuscation +-enable-toplevel-obfuscation +-enable-filename-obfuscation +-enable-export-obfuscation \ No newline at end of file diff --git a/entry_wearable/oh-package.json5 b/entry_wearable/oh-package.json5 new file mode 100644 index 00000000..d57c957e --- /dev/null +++ b/entry_wearable/oh-package.json5 @@ -0,0 +1,12 @@ +{ + "name": "entry_wearable", + "version": "1.0.0", + "description": "GrowingIO Analytics demo for HarmonyOS wearable (watch).", + "main": "", + "author": "", + "license": "", + "dependencies": { + "@growingio/analytics": "file:../GrowingAnalytics" + }, + "devDependencies": {} +} diff --git a/entry_wearable/src/main/ets/entryability/EntryAbility.ets b/entry_wearable/src/main/ets/entryability/EntryAbility.ets new file mode 100644 index 00000000..981619f8 --- /dev/null +++ b/entry_wearable/src/main/ets/entryability/EntryAbility.ets @@ -0,0 +1,48 @@ +import UIAbility from '@ohos.app.ability.UIAbility' +import hilog from '@ohos.hilog' +import window from '@ohos.window' +import Want from '@ohos.app.ability.Want' +import AbilityConstant from '@ohos.app.ability.AbilityConstant' + +export default class EntryAbility extends UIAbility { + onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { + hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onCreate') + } + + onDestroy(): void { + hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onDestroy') + } + + onWindowStageCreate(windowStage: window.WindowStage): void { + hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onWindowStageCreate') + + windowStage.loadContent('pages/Index', (err) => { + if (err.code) { + hilog.error(0x0000, 'growingWear', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '') + return + } + hilog.info(0x0000, 'growingWear', 'Succeeded in loading the content.') + + // 手表端不采集无埋点,因此刻意不调用: + // GrowingAnalytics.onWindowStageCreate(this, windowStage) + // + // 该方法是无埋点 UI 监听的唯一注册入口,内部会挂上 4 个 UIObserver 回调 + // (willClick / navDestinationUpdate / navDestinationSwitch / routerPageUpdate)。 + // 不调用即零注册,每次点击与路由跳转都不会进入 SDK 回调。 + // + // 手动埋点、会话、设备信息、事件入库与上报均不依赖此方法,功能不受影响。 + }) + } + + onWindowStageDestroy(): void { + hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onWindowStageDestroy') + } + + onForeground(): void { + hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onForeground') + } + + onBackground(): void { + hilog.info(0x0000, 'growingWear', '%{public}s', 'Ability onBackground') + } +} diff --git a/entry_wearable/src/main/ets/entryabilitystage/EntryAbilityStage.ets b/entry_wearable/src/main/ets/entryabilitystage/EntryAbilityStage.ets new file mode 100644 index 00000000..fb926a4c --- /dev/null +++ b/entry_wearable/src/main/ets/entryabilitystage/EntryAbilityStage.ets @@ -0,0 +1,46 @@ +import AbilityStage from '@ohos.app.ability.AbilityStage' +import { GrowingAnalytics, GrowingConfig } from '@growingio/analytics' + +export default class EntryAbilityStage extends AbilityStage { + onCreate(): void { + // 初始化 SDK + this.setupAnalytics() + + // 确保终端已授权个人隐私信息合规收集和处理后,开启 SDK 数据采集 + this.startAnalytics() + } + + setupAnalytics() { + let config = new GrowingConfig().NewSaaS( + '0a1b4118dd954ec3bcc69da5138bdb96', + '8eeb910e0db7a73b', + 'growing.84ff920737d56b1d' + ) + config.debugEnabled = true + config.sessionInterval = 30 + + // 手表端建议放宽上报间隔:屏幕小、会话短,频繁唤醒网络对续航不友好 + config.dataUploadInterval = 60 + + config.idMappingEnabled = true + config.requestOptions.connectTimeout = 10 + config.requestOptions.transferTimeout = 10 + + // 手表端不采集无埋点。 + // autotrackEnabled 默认即为 false,此处显式写出以表明是有意为之; + // 真正决定是否注册 UI 监听的是 EntryAbility 里有没有调用 + // GrowingAnalytics.onWindowStageCreate —— 不调用则一个 UIObserver 都不会注册。 + config.autotrackEnabled = false + + // 手表端无 WebView 场景,关闭 Hybrid 无埋点 + config.hybridAutotrackEnabled = false + + // GrowingToolsKit 是悬浮窗形态的调试工具,手表屏幕放不下,故不挂载 + + GrowingAnalytics.configure(config) + } + + startAnalytics() { + GrowingAnalytics.startAnalytics(this.context) + } +} diff --git a/entry_wearable/src/main/ets/pages/Index.ets b/entry_wearable/src/main/ets/pages/Index.ets new file mode 100644 index 00000000..80c32394 --- /dev/null +++ b/entry_wearable/src/main/ets/pages/Index.ets @@ -0,0 +1,120 @@ +import deviceInfo from '@ohos.deviceInfo' +import { GrowingAnalytics, GrowingAttrType } from '@growingio/analytics' + +/** + * 手表端 demo 首页。 + * + * 与 entry(手机)demo 的关键差异:本模块不开启无埋点,因此不产生 PAGE 与 + * VIEW_CLICK 事件,下面的事件全部来自手动埋点 API。 + * 圆形屏幕下所有可点区域集中在中心竖列,四角不放内容。 + */ +@Entry +@Component +struct Index { + @State status: string = '等待操作' + @State collectionEnabled: boolean = true + @State timerId: string = '' + + build() { + Scroll() { + Column({ space: 12 }) { + Text('GrowingIO Wearable') + .fontSize(16) + .fontWeight(FontWeight.Bold) + + Text(`deviceType: ${deviceInfo.deviceType}`) + .fontSize(12) + .fontColor($r('sys.color.font_secondary')) + + Text(this.status) + .fontSize(12) + .fontColor($r('sys.color.font_secondary')) + .textAlign(TextAlign.Center) + .maxLines(2) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .width('80%') + + Button('自定义事件') + .wearableButton() + .onClick(() => { + GrowingAnalytics.track('wear_button_click') + this.status = 'track: wear_button_click' + }) + + Button('带属性事件') + .wearableButton() + .onClick(() => { + let attrs: GrowingAttrType = { + 'source': 'wearable', + 'battery_saver': true, + 'timestamp': Date.now() + } + GrowingAnalytics.track('wear_workout_finish', attrs) + this.status = 'track: wear_workout_finish' + }) + + Button(this.timerId.length > 0 ? '结束计时' : '开始计时') + .wearableButton() + .onClick(() => { + this.toggleTimer() + }) + + Button('登录用户') + .wearableButton() + .onClick(() => { + GrowingAnalytics.setLoginUserId('wear_user_001') + this.status = 'setLoginUserId: wear_user_001' + }) + + Button('登出') + .wearableButton() + .onClick(() => { + GrowingAnalytics.cleanLoginUserId() + this.status = 'cleanLoginUserId' + }) + + Button(this.collectionEnabled ? '关闭采集' : '开启采集') + .wearableButton() + .onClick(() => { + this.collectionEnabled = !this.collectionEnabled + GrowingAnalytics.setDataCollectionEnabled(this.collectionEnabled) + this.status = `dataCollectionEnabled = ${this.collectionEnabled}` + }) + + Text(`deviceId: ${GrowingAnalytics.getDeviceId()}`) + .fontSize(10) + .fontColor($r('sys.color.font_tertiary')) + .textAlign(TextAlign.Center) + .maxLines(2) + .textOverflow({ overflow: TextOverflow.Ellipsis }) + .width('80%') + } + .width('100%') + .padding({ top: 40, bottom: 40, left: 16, right: 16 }) + } + .scrollBar(BarState.Off) + .width('100%') + .height('100%') + } + + private toggleTimer() { + if (this.timerId.length > 0) { + GrowingAnalytics.trackTimerEnd(this.timerId) + this.status = `trackTimerEnd: ${this.timerId}` + this.timerId = '' + return + } + this.timerId = GrowingAnalytics.trackTimerStart('wear_workout') + this.status = `trackTimerStart: ${this.timerId}` + } +} + +/** + * 手表触控面积小,统一放大点按区域,避免误触。 + */ +@Extend(Button) +function wearableButton() { + .width('80%') + .height(40) + .fontSize(14) +} diff --git a/entry_wearable/src/main/module.json5 b/entry_wearable/src/main/module.json5 new file mode 100644 index 00000000..6b3ba7fd --- /dev/null +++ b/entry_wearable/src/main/module.json5 @@ -0,0 +1,47 @@ +{ + "module": { + "name": "entry_wearable", + "type": "entry", + "srcEntry": "./ets/entryabilitystage/EntryAbilityStage.ets", + "description": "$string:module_desc", + "mainElement": "EntryAbility", + // 手表独立 entry:UI 与交互层无法与手机复用,按官方多设备工程结构单独成模块。 + // 此处只声明 wearable,手机/平板/PC 仍由 entry 模块承载。 + "deviceTypes": [ + "wearable" + ], + "deliveryWithInstall": true, + "installationFree": false, + "pages": "$profile:main_pages", + "abilities": [ + { + "name": "EntryAbility", + "srcEntry": "./ets/entryability/EntryAbility.ets", + "description": "$string:EntryAbility_desc", + "icon": "$media:layered_image", + "label": "$string:EntryAbility_label", + "startWindowIcon": "$media:startIcon", + "startWindowBackground": "$color:start_window_background", + "exported": true, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ] + } + ], + "requestPermissions": [ + { + "name": "ohos.permission.INTERNET" + }, + { + "name": "ohos.permission.GET_NETWORK_INFO" + } + ] + } +} diff --git a/entry_wearable/src/main/resources/base/element/color.json b/entry_wearable/src/main/resources/base/element/color.json new file mode 100644 index 00000000..2e711ab9 --- /dev/null +++ b/entry_wearable/src/main/resources/base/element/color.json @@ -0,0 +1,8 @@ +{ + "color": [ + { + "name": "start_window_background", + "value": "#000000" + } + ] +} diff --git a/entry_wearable/src/main/resources/base/element/string.json b/entry_wearable/src/main/resources/base/element/string.json new file mode 100644 index 00000000..38d49916 --- /dev/null +++ b/entry_wearable/src/main/resources/base/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "GrowingIO Analytics wearable demo" + }, + { + "name": "EntryAbility_desc", + "value": "GrowingIO Analytics wearable demo" + }, + { + "name": "EntryAbility_label", + "value": "GIO Wear" + } + ] +} diff --git a/entry_wearable/src/main/resources/base/media/background.png b/entry_wearable/src/main/resources/base/media/background.png new file mode 100644 index 00000000..f939c9fa Binary files /dev/null and b/entry_wearable/src/main/resources/base/media/background.png differ diff --git a/entry_wearable/src/main/resources/base/media/foreground.png b/entry_wearable/src/main/resources/base/media/foreground.png new file mode 100644 index 00000000..4483ddad Binary files /dev/null and b/entry_wearable/src/main/resources/base/media/foreground.png differ diff --git a/entry_wearable/src/main/resources/base/media/layered_image.json b/entry_wearable/src/main/resources/base/media/layered_image.json new file mode 100644 index 00000000..fb499204 --- /dev/null +++ b/entry_wearable/src/main/resources/base/media/layered_image.json @@ -0,0 +1,7 @@ +{ + "layered-image": + { + "background" : "$media:background", + "foreground" : "$media:foreground" + } +} \ No newline at end of file diff --git a/entry_wearable/src/main/resources/base/media/startIcon.png b/entry_wearable/src/main/resources/base/media/startIcon.png new file mode 100644 index 00000000..205ad8b5 Binary files /dev/null and b/entry_wearable/src/main/resources/base/media/startIcon.png differ diff --git a/entry_wearable/src/main/resources/base/profile/main_pages.json b/entry_wearable/src/main/resources/base/profile/main_pages.json new file mode 100644 index 00000000..1898d94f --- /dev/null +++ b/entry_wearable/src/main/resources/base/profile/main_pages.json @@ -0,0 +1,5 @@ +{ + "src": [ + "pages/Index" + ] +} diff --git a/entry_wearable/src/main/resources/en_US/element/string.json b/entry_wearable/src/main/resources/en_US/element/string.json new file mode 100644 index 00000000..38d49916 --- /dev/null +++ b/entry_wearable/src/main/resources/en_US/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "GrowingIO Analytics wearable demo" + }, + { + "name": "EntryAbility_desc", + "value": "GrowingIO Analytics wearable demo" + }, + { + "name": "EntryAbility_label", + "value": "GIO Wear" + } + ] +} diff --git a/entry_wearable/src/main/resources/zh_CN/element/string.json b/entry_wearable/src/main/resources/zh_CN/element/string.json new file mode 100644 index 00000000..88e20c80 --- /dev/null +++ b/entry_wearable/src/main/resources/zh_CN/element/string.json @@ -0,0 +1,16 @@ +{ + "string": [ + { + "name": "module_desc", + "value": "GrowingIO 手表端 demo" + }, + { + "name": "EntryAbility_desc", + "value": "GrowingIO 手表端 demo" + }, + { + "name": "EntryAbility_label", + "value": "GIO 手表" + } + ] +}