Skip to content

Commit 94fb7e6

Browse files
authored
Merge pull request #3 from OpenFlutter/codex/harmonyos-support
feat: add HarmonyOS support and bump to 1.1.0
2 parents a20b31a + 433bf5c commit 94fb7e6

46 files changed

Lines changed: 1117 additions & 111 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/build_test.yml‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,21 @@ on:
77
pull_request:
88

99
jobs:
10+
contracts:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: subosito/flutter-action@v2
15+
with:
16+
channel: stable
17+
- uses: actions/setup-node@v4
18+
with:
19+
node-version: '24'
20+
- run: flutter test
21+
- run: node --test test/ohos/*.test.mjs
22+
- run: flutter test
23+
working-directory: example
24+
1025
test_iOS:
1126
name: Test iOS on ${{ matrix.os }}
1227
runs-on: ${{ matrix.os }}

‎.gitignore‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,13 @@ migrate_working_dir/
3131
.flutter-plugins
3232
.flutter-plugins-dependencies
3333
build/
34+
35+
# HarmonyOS generated files and local signing
36+
oh_modules/
37+
.hvigor/
38+
local.properties
39+
oh-package-lock.json5
40+
**/GeneratedPluginRegistrant.ets
41+
*.p12
42+
*.p7b
43+
*.cer

‎CHANGELOG.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
## 1.1.0
2+
3+
- Add HarmonyOS OAID implementation, permission-aware APIs, example and Chinese setup guide. Native build/device verification is pending.
4+
- Add Dart API, example UI and host-side native logic tests.
5+
16
## 1.0.3
27
* Add `FlutterFramework` in `Package.swift`.
38

‎README.OpenHarmony_CN.md‎

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# 鸿蒙接入指南
2+
3+
本实现为 `advertising_id_flutter` 提供 HarmonyOS OAID 支持,基于 TPC 的
4+
`fluttertpc_advertising_id` 鸿蒙代码适配。保留原有 Android、iOS、macOS API。
5+
6+
## 环境与验证状态
7+
8+
- 使用支持 `ohos` 的 Flutter 适配版,随附 Dart 必须满足 `>=3.8.0 <4.0.0`。
9+
普通 Flutter 可运行 Dart 测试,但不能构建鸿蒙 HAP。
10+
- 需要 DevEco Studio、HarmonyOS SDK、应用签名和具备 Ads Kit 的真机。
11+
- 示例沿用来源项目的 `5.0.0(12)` SDK 配置;这是配置基线,**不是本项目已经验证通过的版本声明**。
12+
- 当前完成 Dart 和宿主侧原生逻辑测试;尚未完成 ArkTS 编译和真机验证。
13+
不承诺所有 OpenHarmony 发行版都具备 `@kit.AdsKit`。
14+
15+
## 1. 引入插件
16+
17+
当前改动尚未发布,开发阶段使用本地路径,指向本仓库根目录:
18+
19+
```yaml
20+
dependencies:
21+
advertising_id_flutter:
22+
path: ../flutter_advertising_id
23+
```
24+
25+
使用鸿蒙 Flutter 执行 `flutter pub get`。插件已经声明 `ohos` 平台,
26+
宿主应使用工具链生成的插件注册文件,不需要手动创建 MethodChannel。
27+
28+
## 2. 声明应用权限
29+
30+
在宿主 `ohos/entry/src/main/module.json5` 的 `module.requestPermissions` 中添加:
31+
32+
```json
33+
{
34+
"name": "ohos.permission.APP_TRACKING_CONSENT",
35+
"reason": "$string:tracking_reason",
36+
"usedScene": {
37+
"abilities": ["EntryAbility"],
38+
"when": "inuse"
39+
}
40+
}
41+
```
42+
43+
将 `EntryAbility` 替换为实际 Ability 名称。在宿主
44+
`ohos/entry/src/main/resources/base/element/string.json` 的 `string` 数组中添加:
45+
46+
```json
47+
{
48+
"name": "tracking_reason",
49+
"value": "用于获取广告标识符以提供个性化广告服务"
50+
}
51+
```
52+
53+
用途说明应符合应用实际用途,有多语言资源时一并翻译。权限必须由宿主声明,
54+
仅依赖插件 HAR 不会替应用完成权限配置。
55+
56+
## 3. 调用
57+
58+
```dart
59+
import 'package:advertising_id_flutter/flutter_advertising_id.dart';
60+
import 'package:flutter/services.dart';
61+
62+
final advertisingId = AdvertisingId();
63+
64+
// 不弹出权限申请:没有权限时返回 null。
65+
final existingId = await advertisingId.getAdvertisingId();
66+
67+
// 在前台页面、合适的用户操作之后申请权限。
68+
try {
69+
final id = await advertisingId.getAdvertisingId(true);
70+
// id == null 表示拒绝授权或没有有效 OAID;由业务决定后续行为。
71+
} on PlatformException catch (error) {
72+
// 可根据 error.code 展示错误或记录诊断信息,避免记录广告 ID。
73+
}
74+
75+
// 这两个查询不会申请权限。
76+
final status = await advertisingId.authorizationStatus;
77+
final limited = await advertisingId.limitAdTrackingEnabled;
78+
```
79+
80+
| API | 鸿蒙行为 |
81+
| --- | --- |
82+
| `getAdvertisingId(false)` | 已授权时读取 OAID;否则返回 null,不弹框 |
83+
| `getAdvertisingId(true)` | 必要时使用前台 Ability 请求权限;拒绝返回 null |
84+
| `authorizationStatus` | 已授权为 authorized,未授权为 denied;不区分首次未申请与已拒绝 |
85+
| `limitAdTrackingEnabled` | APP_TRACKING_CONSENT 未授予时为 true,授予时为 false |
86+
87+
限制状态是**当前应用的跟踪权限状态**,不是独立的全局广告跟踪开关。
88+
空 OAID 和全零 OAID 返回 null。插件不缓存 OAID,每次查询重新读取权限。
89+
被拒绝后能否再次弹框由系统控制,应用可提示用户到系统设置修改授权。
90+
91+
| 错误码 | 含义 |
92+
| --- | --- |
93+
| `PERMISSION_ERROR` | 权限查询或申请调用失败;检查权限声明和环境 |
94+
| `NO_ACTIVITY` | 需要申请权限但没有前台 Ability,或申请期间 Ability 被分离 |
95+
| `GET_AD_ID_FAILED` | OAID 服务调用失败 |
96+
97+
三个 API 都可能抛出 PlatformException。权限拒绝本身不抛异常。
98+
99+
## 4. 运行示例
100+
101+
使用鸿蒙 Flutter,在仓库 `example` 目录执行:
102+
103+
```sh
104+
flutter pub get
105+
flutter devices
106+
flutter build hap --debug
107+
flutter run -d <设备ID>
108+
```
109+
110+
首次运行前,在 DevEco Studio 打开 `example/ohos`,配置本机 SDK 和应用签名。
111+
按工具链要求准备 `@ohos/flutter_ohos`;生成的依赖、注册文件及本地签名不提交。
112+
示例提供“不申请读取”“申请授权并读取”“查询状态”三个入口,启动时不会申请权限。
113+
114+
## 5. 测试与验收
115+
116+
仓库根目录:
117+
118+
```sh
119+
flutter test
120+
node --test test/ohos/*.test.mjs
121+
```
122+
123+
示例目录:
124+
125+
```sh
126+
flutter test
127+
```
128+
129+
Node 测试要求 Node 24,直接执行去除类型的插件源码,仅替换系统 SDK 边界。
130+
它验证权限分支、并发和错误行为,**不验证 ArkTS 类型规则、SDK 链接或系统弹框**。
131+
132+
发布前必须补齐真机记录:首次不申请、允许、拒绝、系统撤销、重复调用、
133+
并发申请、无前台 Ability、OAID 重置;同时记录 Flutter/Dart/SDK/IDE/ROM 版本。
134+
来源项目的兼容性列表不等同于本项目的验收结果。

‎README.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ A Flutter plugin to access advertising ID.
1414

1515
Wraps [ASIdentifierManager.advertisingIdentifier](https://developer.apple.com/documentation/adsupport/asidentifiermanager/1614151-advertisingidentifier) (on iOS) and [advertising ID](https://developers.google.com/android/reference/com/google/android/gms/ads/identifier/AdvertisingIdClient) (on Android).
1616

17+
## HarmonyOS
18+
19+
HarmonyOS OAID implementation is available in source. See the [中文接入指南](README.OpenHarmony_CN.md) for permissions, API semantics and toolchain requirements. Native build and device validation are still pending.
20+
1721
## Getting Started
1822

1923
Run this command
Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,20 @@
1-
// This is a basic Flutter integration test.
2-
//
3-
// Since integration tests run in a full Flutter application, they can interact
4-
// with the host side of a plugin implementation, unlike Dart unit tests.
5-
//
6-
// For more information about Flutter integration tests, please see
7-
// https://flutter.dev/to/integration-testing
8-
9-
101
import 'package:flutter_test/flutter_test.dart';
112
import 'package:integration_test/integration_test.dart';
12-
133
import 'package:advertising_id_flutter/flutter_advertising_id.dart';
144

155
void main() {
166
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
177

18-
testWidgets('getPlatformVersion test', (WidgetTester tester) async {
19-
final AdvertisingId plugin = AdvertisingId();
20-
final String? version = await plugin.getAdvertisingId();
21-
// The version string depends on the host platform running the test, so
22-
// just assert that some non-empty string is returned.
23-
expect(version?.isNotEmpty, true);
8+
testWidgets('advertising ID may be unavailable without authorization', (
9+
tester,
10+
) async {
11+
final plugin = AdvertisingId();
12+
final id = await plugin.getAdvertisingId(false);
13+
expect(id, anyOf(isNull, isNotEmpty));
14+
expect(
15+
await plugin.authorizationStatus,
16+
isA<AdTrackingAuthorizationStatus>(),
17+
);
18+
expect(await plugin.limitAdTrackingEnabled, anyOf(isNull, isA<bool>()));
2419
});
2520
}

‎example/lib/main.dart‎

Lines changed: 48 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
1+
import 'package:advertising_id_flutter/flutter_advertising_id.dart';
12
import 'package:flutter/material.dart';
2-
import 'dart:async';
3-
43
import 'package:flutter/services.dart';
5-
import 'package:advertising_id_flutter/flutter_advertising_id.dart';
64

7-
void main() {
8-
runApp(const MyApp());
9-
}
5+
void main() => runApp(const MyApp());
106

117
class MyApp extends StatefulWidget {
128
const MyApp({super.key});
@@ -16,52 +12,59 @@ class MyApp extends StatefulWidget {
1612
}
1713

1814
class _MyAppState extends State<MyApp> {
19-
String _platformVersion = 'Unknown';
20-
String _status = 'Unknown';
21-
final _flutterAdvertisingIdPlugin = AdvertisingId();
15+
final _plugin = AdvertisingId();
16+
String _id = '尚未读取';
17+
String _status = '尚未查询';
18+
String? _error;
2219

23-
@override
24-
void initState() {
25-
super.initState();
20+
Future<void> _readId([bool request = false]) async {
21+
try {
22+
final id = await _plugin.getAdvertisingId(request);
23+
if (!mounted) return;
24+
setState(() {
25+
_id = id ?? '不可用(未授权或无有效 ID)';
26+
_error = null;
27+
});
28+
} on PlatformException catch (error) {
29+
if (mounted) setState(() => _error = error.code);
30+
}
2631
}
2732

28-
// Platform messages are asynchronous, so we initialize in an async method.
29-
Future<void> initPlatformState() async {
30-
String platformVersion;
31-
// Platform messages may fail, so we use a try/catch PlatformException.
32-
// We also handle the message potentially returning null.
33+
Future<void> _queryStatus() async {
3334
try {
34-
platformVersion =
35-
await _flutterAdvertisingIdPlugin.getAdvertisingId() ?? 'Unknown platform version';
36-
_status = (await _flutterAdvertisingIdPlugin.authorizationStatus).toString();
37-
} on PlatformException {
38-
platformVersion = 'Failed to get platform version.';
35+
final status = await _plugin.authorizationStatus;
36+
final limited = await _plugin.limitAdTrackingEnabled;
37+
if (!mounted) return;
38+
setState(() {
39+
_status = '${status.name};限制跟踪:$limited';
40+
_error = null;
41+
});
42+
} on PlatformException catch (error) {
43+
if (mounted) setState(() => _error = error.code);
3944
}
40-
41-
// If the widget was removed from the tree while the asynchronous platform
42-
// message was in flight, we want to discard the reply rather than calling
43-
// setState to update our non-existent appearance.
44-
if (!mounted) return;
45-
46-
setState(() {
47-
_platformVersion = platformVersion;
48-
});
4945
}
5046

5147
@override
52-
Widget build(BuildContext context) {
53-
return MaterialApp(
54-
home: Scaffold(
55-
appBar: AppBar(
56-
title: const Text('Plugin example app'),
57-
),
58-
body: Center(
59-
child: Text('Running on: $_platformVersion\n status: $_status'),
60-
),
61-
floatingActionButton: FloatingActionButton(onPressed: (){
62-
initPlatformState();
63-
},child: Icon(Icons.ssid_chart),),
48+
Widget build(BuildContext context) => MaterialApp(
49+
home: Scaffold(
50+
appBar: AppBar(title: const Text('Advertising ID')),
51+
body: ListView(
52+
padding: const EdgeInsets.all(24),
53+
children: [
54+
Text('广告 ID:$_id'),
55+
Text('授权状态:$_status'),
56+
if (_error != null) Text('错误:$_error'),
57+
ElevatedButton(
58+
onPressed: () => _readId(),
59+
child: const Text('读取 ID(不申请)'),
60+
),
61+
ElevatedButton(
62+
onPressed: () => _readId(true),
63+
child: const Text('申请授权并读取'),
64+
),
65+
ElevatedButton(onPressed: _queryStatus, child: const Text('查询状态')),
66+
],
6467
),
65-
);
66-
}
68+
),
69+
);
6770
}

‎example/ohos/.gitignore‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/node_modules
2+
/oh_modules
3+
/local.properties
4+
/.idea
5+
**/build
6+
/.hvigor
7+
.cxx
8+
/.clangd
9+
/.clang-format
10+
/.clang-tidy
11+
**/.test
12+
**/BuildProfile.ets
13+
**/oh-package-lock.json5
14+
/package.json
15+
/package-lock.json
16+
17+
**/src/main/resources/rawfile/flutter_assets/
18+
**/libs/**/libapp.so
19+
**/libs/**/libflutter.so
20+
**/libs/**/libvmservice_snapshot.so

‎example/ohos/AppScope/app.json5‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"app": {
3+
"bundleName": "dev.openflutter.advertising_id_example",
4+
"vendor": "example",
5+
"versionCode": 1000000,
6+
"versionName": "1.0.0",
7+
"icon": "$media:app_icon",
8+
"label": "$string:app_name"
9+
}
10+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"string": [
3+
{
4+
"name": "app_name",
5+
"value": "flutter_advertising_id_example"
6+
}
7+
]
8+
}

0 commit comments

Comments
 (0)