deveco_hmigbot 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +26 -0
- package/README.md +50 -0
- package/agents/hmigbot-worker.md +61 -0
- package/agents/hmigbot.md +22 -0
- package/agents/workflow-subagent.md +55 -0
- package/commands/hmigbot.md +17 -0
- package/dist/index.js +1 -0
- package/manifest.json +19 -0
- package/package.json +29 -0
- package/skills/migrate-core/FILES.md +26 -0
- package/skills/migrate-core/SKILL.md +484 -0
- package/skills/migrate-core/references/README.md +64 -0
- package/skills/migrate-core/references/flow/arkts-vector-gate.md +55 -0
- package/skills/migrate-core/references/flow/build-error-patterns.md +52 -0
- package/skills/migrate-core/references/flow/conventions-template.md +244 -0
- package/skills/migrate-core/references/flow/navigation-migration.md +42 -0
- package/skills/migrate-core/references/flow/platform-api-guards.md +59 -0
- package/skills/migrate-core/references/flow/platform-model-gaps.md +43 -0
- package/skills/migrate-core/references/flow/resource-conversion.md +46 -0
- package/skills/migrate-core/references/flow/ui-layout-semantics.md +124 -0
- package/skills/migrate-core/references/flow/unit-breakdown.md +42 -0
- package/skills/migrate-core/references/host-capabilities.md +24 -0
- package/skills/migrate-core/references/topics/app-identity.md +214 -0
- package/skills/migrate-core/references/topics/env-doctor.md +245 -0
- package/skills/migrate-core/references/topics/i18n/README.md +458 -0
- package/skills/migrate-core/references/topics/i18n/references/code-examples.md +304 -0
- package/skills/migrate-core/references/topics/i18n/references/common-pitfalls.md +354 -0
- package/skills/migrate-core/references/topics/i18n/references/dynamic-language-switch.md +464 -0
- package/skills/migrate-core/references/topics/i18n/references/language-codes.md +104 -0
- package/skills/migrate-core/references/topics/icon-sizing.md +98 -0
- package/skills/migrate-core/references/topics/library-migration/README.md +234 -0
- package/skills/migrate-core/references/topics/library-migration/closed-source-sdk.md +128 -0
- package/skills/migrate-core/references/topics/library-migration/download-api-decision.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/library-mapping-table.md +100 -0
- package/skills/migrate-core/references/topics/library-migration/napi-compile-guide.md +84 -0
- package/skills/migrate-core/references/topics/library-migration/ohpm-search-guide.md +73 -0
- package/skills/migrate-core/references/topics/library-migration/stdlib-mapping-table.md +34 -0
- package/skills/migrate-core/references/topics/resources/aar-decompile.md +25 -0
- package/skills/migrate-core/references/topics/resources/conversion-rules.md +625 -0
- package/skills/migrate-core/references/topics/resources/dependency-analysis-rules.md +328 -0
- package/skills/migrate-core/references/topics/resources/material-design-icons.md +173 -0
- package/skills/migrate-core/references/topics/resources/svg-fix-patterns.md +175 -0
- package/skills/migrate-core/references/topics/resources/xml-drawable-to-svg-rules.md +513 -0
- package/skills/migrate-core/references/topics/system-capabilities/README.md +331 -0
- package/skills/migrate-core/references/topics/system-capabilities/avplayer-guide.md +161 -0
- package/skills/migrate-core/references/topics/system-capabilities/background-tasks.md +403 -0
- package/skills/migrate-core/references/topics/system-capabilities/browser-intent.md +121 -0
- package/skills/migrate-core/references/topics/system-capabilities/camera-picker.md +118 -0
- package/skills/migrate-core/references/topics/system-capabilities/document-picker.md +246 -0
- package/skills/migrate-core/references/topics/system-capabilities/file-utils.md +131 -0
- package/skills/migrate-core/references/topics/system-capabilities/permission-helper.md +112 -0
- package/skills/migrate-core/references/topics/system-capabilities/photo-access-helper.md +208 -0
- package/skills/migrate-core/references/topics/system-capabilities/print-management.md +213 -0
- package/skills/migrate-core/references/topics/system-capabilities/share-panel.md +177 -0
- package/skills/migrate-core/references/topics/system-capabilities/system-settings.md +322 -0
- package/skills/migrate-core/references/topics/system-capabilities/telephony-dial.md +49 -0
- package/skills/migrate-core/references/topics/system-capabilities/video-playback.md +42 -0
- package/skills/migrate-core/references/topics/system-capabilities/webview-patterns.md +38 -0
- package/skills/migrate-core/references/topics/ui-alignment/README.md +344 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/dark-mode.md +47 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/layout-mapping.md +301 -0
- package/skills/migrate-core/references/topics/ui-alignment/references/visual-patterns.md +411 -0
- package/skills/migrate-core/scripts/closure/check-anchors.mjs +186 -0
- package/skills/migrate-core/scripts/closure/check-api-guards.mjs +175 -0
- package/skills/migrate-core/scripts/closure/check-consumers.mjs +301 -0
- package/skills/migrate-core/scripts/closure/check-permissions.mjs +165 -0
- package/skills/migrate-core/scripts/closure/check-resources.mjs +130 -0
- package/skills/migrate-core/scripts/closure/check-routes.mjs +527 -0
- package/skills/migrate-core/scripts/closure/check-safearea.mjs +122 -0
- package/skills/migrate-core/scripts/closure/check-stubs.mjs +69 -0
- package/skills/migrate-core/scripts/closure/closure-suite.mjs +256 -0
- package/skills/migrate-core/scripts/closure/idioms.json +105 -0
- package/skills/migrate-core/scripts/convert/convert-resources.mjs +437 -0
- package/skills/migrate-core/scripts/feasibility/feasibility.mjs +235 -0
- package/skills/migrate-core/scripts/feasibility/tables/cross-platform.json +11 -0
- package/skills/migrate-core/scripts/feasibility/tables/deprecated-api.json +10 -0
- package/skills/migrate-core/scripts/feasibility/tables/imported-arkts-core.json +425 -0
- package/skills/migrate-core/scripts/feasibility/tables/lib-equivalence.json +206 -0
- package/skills/migrate-core/scripts/feasibility/tables/system-capabilities.json +22 -0
- package/skills/migrate-core/scripts/front.mjs +107 -0
- package/skills/migrate-core/scripts/interface/ark-extract.mjs +172 -0
- package/skills/migrate-core/scripts/interface/interface.mjs +152 -0
- package/skills/migrate-core/scripts/ledger/ledger.mjs +383 -0
- package/skills/migrate-core/scripts/ledger/parse-cards.mjs +98 -0
- package/skills/migrate-core/scripts/lib/literals.mjs +37 -0
- package/skills/migrate-core/scripts/lib/scan.mjs +315 -0
- package/skills/migrate-core/scripts/smoke/align-sdk.mjs +118 -0
- package/skills/migrate-core/scripts/smoke/ensure-sign.mjs +53 -0
- package/skills/migrate-core/scripts/smoke/smoke.mjs +238 -0
- package/skills/migrate-core/scripts/smoke/verdict.mjs +31 -0
- package/skills/migrate-core/scripts/smoke/walk.mjs +480 -0
- package/skills/migrate-core/scripts/transpile/mapping.json +76 -0
- package/skills/migrate-core/scripts/transpile/transpile-layout.mjs +404 -0
- package/skills/migrate-core/scripts/vectors/run-arkts-vectors.mjs +107 -0
- package/skills/migrate-core/scripts/vectors/setup-arkts-test.mjs +90 -0
- package/skills/migrate-core/scripts/wire/extractors.mjs +258 -0
- package/skills/migrate-core/scripts/wire/wire-routes.mjs +507 -0
- package/skills/migrate-core/templates/acceptance.js +365 -0
- package/skills/migrate-core/templates/explore.js +86 -0
- package/skills/migrate-core/templates/implement.js +211 -0
- package/skills/migrate-core/templates/mig_slices.js +491 -0
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# 后台任务指南
|
|
2
|
+
|
|
3
|
+
> workScheduler(延迟任务)、ContinuousTask(长时任务)、AVSession(媒体会话)的使用指南。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 后台任务类型选择
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
需要什么后台能力?
|
|
11
|
+
│
|
|
12
|
+
├─ 定时/延迟执行任务(如定时更新数据)
|
|
13
|
+
│ └─ workScheduler(见下方)
|
|
14
|
+
│ ⚠️ 最短间隔比 Android WorkManager 更保守
|
|
15
|
+
│
|
|
16
|
+
├─ 持续运行(如后台播放音乐)
|
|
17
|
+
│ └─ ContinuousTask(长时任务)
|
|
18
|
+
│ 需要 module.json5 声明 backgroundModes
|
|
19
|
+
│
|
|
20
|
+
└─ 媒体播放控制(通知栏/锁屏控制)
|
|
21
|
+
└─ AVSession(媒体会话)
|
|
22
|
+
配合 ContinuousTask 使用
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## workScheduler(延迟任务)
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { workScheduler } from '@kit.BackgroundTasksKit'
|
|
31
|
+
|
|
32
|
+
// 注册延迟任务
|
|
33
|
+
const workInfo: workScheduler.WorkInfo = {
|
|
34
|
+
workId: 1,
|
|
35
|
+
bundleName: 'com.example.myapp',
|
|
36
|
+
abilityName: 'UpdateWorker',
|
|
37
|
+
isPersisted: true,
|
|
38
|
+
repeatCycleTime: 7200000, // 2 小时(官方循环任务最短间隔,不是 30 分钟)
|
|
39
|
+
isRepeat: true,
|
|
40
|
+
networkType: workScheduler.NetworkType.NETWORK_TYPE_ANY
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
workScheduler.startWork(workInfo)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
**注意**:
|
|
47
|
+
- ⚠️ HarmonyOS `workScheduler` 最短重复间隔比 Android `WorkManager` 更保守
|
|
48
|
+
- 不要依赖精确间隔,系统会根据电量和资源情况调整
|
|
49
|
+
- 如需频繁更新,改为用户手动触发 + 后台半定时
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## ContinuousTask(长时任务)
|
|
54
|
+
|
|
55
|
+
后台播放音乐等场景必须使用长时任务:
|
|
56
|
+
|
|
57
|
+
> 官方 AUDIO_PLAYBACK 类型本身覆盖"音频、视频在后台播放"。但系统会做一致性校验:申请了 AUDIO_PLAYBACK 却检测不到实际播放(典型是无声/静音的视频页)会被判未执行业务而挂起或拒批。无声视频页不要申请 AUDIO_PLAYBACK,后台展示画面走 PiP,见 video-playback.md。
|
|
58
|
+
|
|
59
|
+
### 前提条件(三个条件缺一不可)
|
|
60
|
+
|
|
61
|
+
1. **module.json5 声明 backgroundModes**:
|
|
62
|
+
|
|
63
|
+
```json5
|
|
64
|
+
{
|
|
65
|
+
"module": {
|
|
66
|
+
"abilities": [{
|
|
67
|
+
"name": "EntryAbility",
|
|
68
|
+
"backgroundModes": ["audioPlayback"]
|
|
69
|
+
}]
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
2. **代码申请长时任务**:
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { backgroundTaskManager } from '@kit.BackgroundTasksKit'
|
|
78
|
+
import { wantAgent, WantAgent } from '@kit.AbilityKit'
|
|
79
|
+
import { common } from '@kit.AbilityKit'
|
|
80
|
+
|
|
81
|
+
async function startBackgroundPlay(context: common.UIAbilityContext): Promise<void> {
|
|
82
|
+
// 创建 WantAgent(点击通知时的跳转意图)
|
|
83
|
+
const wantAgentInfo: wantAgent.WantAgentInfo = {
|
|
84
|
+
wants: [{
|
|
85
|
+
bundleName: 'com.example.myapp',
|
|
86
|
+
abilityName: 'EntryAbility'
|
|
87
|
+
}],
|
|
88
|
+
actionType: wantAgent.OperationType.START_ABILITY,
|
|
89
|
+
requestCode: 0
|
|
90
|
+
}
|
|
91
|
+
const wantAgentObj = await wantAgent.getWantAgent(wantAgentInfo)
|
|
92
|
+
|
|
93
|
+
// 申请长时任务
|
|
94
|
+
await backgroundTaskManager.startBackgroundRunning(
|
|
95
|
+
context,
|
|
96
|
+
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK,
|
|
97
|
+
wantAgentObj
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 停止长时任务
|
|
102
|
+
async function stopBackgroundPlay(context: common.UIAbilityContext): Promise<void> {
|
|
103
|
+
await backgroundTaskManager.stopBackgroundRunning(context)
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
3. **注册 AVSession**(见下方)
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## AVSession(媒体会话)
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { avSession } from '@kit.AVSessionKit'
|
|
115
|
+
|
|
116
|
+
// 创建会话
|
|
117
|
+
const session = await avSession.createAVSession(context, 'musicPlayer', 'audio')
|
|
118
|
+
|
|
119
|
+
// 设置元数据
|
|
120
|
+
session.setAVMetadata({
|
|
121
|
+
assetId: 'song_001',
|
|
122
|
+
title: '歌曲名',
|
|
123
|
+
artist: '歌手',
|
|
124
|
+
duration: 300000 // 毫秒
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// 设置播放状态
|
|
128
|
+
session.setAVPlaybackState({
|
|
129
|
+
state: avSession.PlaybackState.PLAYBACK_STATE_PLAY,
|
|
130
|
+
position: { elapsedTime: 50000, updateTime: Date.now() }
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
// 监听控制命令(来自通知栏/锁屏)
|
|
134
|
+
session.on('play', () => { avPlayer.play() })
|
|
135
|
+
session.on('pause', () => { avPlayer.pause() })
|
|
136
|
+
session.on('playNext', () => { playNext() })
|
|
137
|
+
session.on('playPrevious', () => { playPrevious() })
|
|
138
|
+
|
|
139
|
+
// 激活会话
|
|
140
|
+
await session.activate()
|
|
141
|
+
|
|
142
|
+
// 释放会话(退出时)
|
|
143
|
+
await session.destroy()
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## backgroundModes 类型
|
|
149
|
+
|
|
150
|
+
| 模式 | 用途 | module.json5 值 |
|
|
151
|
+
|------|------|----------------|
|
|
152
|
+
| 音频播放 | 后台播放音乐/播客 | `"audioPlayback"` |
|
|
153
|
+
| 定位 | 后台持续定位 | `"location"` |
|
|
154
|
+
| 蓝牙交互 | 后台蓝牙通信 | `"bluetoothInteraction"` |
|
|
155
|
+
| 数据传输 | 后台大文件下载/上传 | `"dataTransfer"` |
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 后台播放三要素实战模式
|
|
160
|
+
|
|
161
|
+
> 基于实测播客应用后台播放管理器提取的完整实战代码。
|
|
162
|
+
|
|
163
|
+
### 三要素缺一不可
|
|
164
|
+
|
|
165
|
+
```
|
|
166
|
+
1. module.json5 声明 backgroundModes: ["audioPlayback"]
|
|
167
|
+
2. backgroundTaskManager.startBackgroundRunning() 申请长时任务
|
|
168
|
+
3. avSession.createAVSession() + activate() 注册媒体会话
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### 完整实现 — BackgroundPlaybackManager
|
|
172
|
+
|
|
173
|
+
```typescript
|
|
174
|
+
// 文件: playback/BackgroundPlaybackManager.ets
|
|
175
|
+
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';
|
|
176
|
+
import { wantAgent } from '@kit.AbilityKit';
|
|
177
|
+
import { avSession } from '@kit.AVSessionKit';
|
|
178
|
+
import { GlobalState } from '../common/GlobalState';
|
|
179
|
+
|
|
180
|
+
export interface PlaybackCallbacks {
|
|
181
|
+
onPlay: () => Promise<void>;
|
|
182
|
+
onPause: () => Promise<void>;
|
|
183
|
+
onSeek: (timeMs: number) => Promise<void>;
|
|
184
|
+
onFastForward: () => Promise<void>;
|
|
185
|
+
onRewind: () => Promise<void>;
|
|
186
|
+
onNext: () => Promise<void>;
|
|
187
|
+
onPrevious: () => Promise<void>;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export class BackgroundPlaybackManager {
|
|
191
|
+
private static instance: BackgroundPlaybackManager | undefined = undefined;
|
|
192
|
+
private session: avSession.AVSession | undefined = undefined;
|
|
193
|
+
private isBackgroundRunning: boolean = false;
|
|
194
|
+
|
|
195
|
+
static getInstance(): BackgroundPlaybackManager {
|
|
196
|
+
if (BackgroundPlaybackManager.instance === undefined) {
|
|
197
|
+
BackgroundPlaybackManager.instance = new BackgroundPlaybackManager();
|
|
198
|
+
}
|
|
199
|
+
return BackgroundPlaybackManager.instance;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async start(callbacks: PlaybackCallbacks): Promise<void> {
|
|
203
|
+
if (this.isBackgroundRunning) { return; }
|
|
204
|
+
const context = GlobalState.getContext();
|
|
205
|
+
|
|
206
|
+
// 1. 创建 WantAgent(通知栏点击跳转)
|
|
207
|
+
const wantAgentInfo: wantAgent.WantAgentInfo = {
|
|
208
|
+
wants: [{
|
|
209
|
+
bundleName: 'com.example.podcast',
|
|
210
|
+
abilityName: 'EntryAbility'
|
|
211
|
+
}],
|
|
212
|
+
actionType: wantAgent.OperationType.START_ABILITY,
|
|
213
|
+
requestCode: 0,
|
|
214
|
+
actionFlags: [wantAgent.WantAgentFlags.UPDATE_PRESENT_FLAG]
|
|
215
|
+
};
|
|
216
|
+
const agent = await wantAgent.getWantAgent(wantAgentInfo);
|
|
217
|
+
|
|
218
|
+
// 2. 启动后台长时任务
|
|
219
|
+
await backgroundTaskManager.startBackgroundRunning(context,
|
|
220
|
+
backgroundTaskManager.BackgroundMode.AUDIO_PLAYBACK, agent);
|
|
221
|
+
|
|
222
|
+
// 3. 创建并激活 AVSession
|
|
223
|
+
this.session = await avSession.createAVSession(context, 'YourApp', 'audio');
|
|
224
|
+
await this.session.activate();
|
|
225
|
+
|
|
226
|
+
// 4. 注册 7 个控制命令
|
|
227
|
+
this.session.on('play', () => { callbacks.onPlay(); });
|
|
228
|
+
this.session.on('pause', () => { callbacks.onPause(); });
|
|
229
|
+
this.session.on('seek', (time: number) => { callbacks.onSeek(time); });
|
|
230
|
+
this.session.on('fastForward', () => { callbacks.onFastForward(); });
|
|
231
|
+
this.session.on('rewind', () => { callbacks.onRewind(); });
|
|
232
|
+
this.session.on('playNext', () => { callbacks.onNext(); });
|
|
233
|
+
this.session.on('playPrevious', () => { callbacks.onPrevious(); });
|
|
234
|
+
|
|
235
|
+
this.isBackgroundRunning = true;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async stop(): Promise<void> {
|
|
239
|
+
if (!this.isBackgroundRunning) { return; }
|
|
240
|
+
|
|
241
|
+
// 释放顺序:取消监听 → 停用会话 → 销毁会话 → 停止后台任务
|
|
242
|
+
if (this.session !== undefined) {
|
|
243
|
+
this.session.off('play');
|
|
244
|
+
this.session.off('pause');
|
|
245
|
+
this.session.off('seek');
|
|
246
|
+
this.session.off('fastForward');
|
|
247
|
+
this.session.off('rewind');
|
|
248
|
+
this.session.off('playNext');
|
|
249
|
+
this.session.off('playPrevious');
|
|
250
|
+
await this.session.deactivate();
|
|
251
|
+
await this.session.destroy();
|
|
252
|
+
this.session = undefined;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const context = GlobalState.getContext();
|
|
256
|
+
await backgroundTaskManager.stopBackgroundRunning(context);
|
|
257
|
+
this.isBackgroundRunning = false;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
async updateMetadata(title: string, artist: string, durationMs: number): Promise<void> {
|
|
261
|
+
if (this.session === undefined) { return; }
|
|
262
|
+
const metadata: avSession.AVMetadata = {
|
|
263
|
+
assetId: '0',
|
|
264
|
+
title: title,
|
|
265
|
+
artist: artist,
|
|
266
|
+
duration: durationMs
|
|
267
|
+
};
|
|
268
|
+
await this.session.setAVMetadata(metadata);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async updatePlaybackState(isPlaying: boolean, positionMs: number): Promise<void> {
|
|
272
|
+
if (this.session === undefined) { return; }
|
|
273
|
+
const pos: avSession.PlaybackPosition = {
|
|
274
|
+
elapsedTime: positionMs,
|
|
275
|
+
updateTime: Date.now()
|
|
276
|
+
};
|
|
277
|
+
const playbackState: avSession.AVPlaybackState = {
|
|
278
|
+
state: isPlaying ? avSession.PlaybackState.PLAYBACK_STATE_PLAY :
|
|
279
|
+
avSession.PlaybackState.PLAYBACK_STATE_PAUSE,
|
|
280
|
+
position: pos
|
|
281
|
+
};
|
|
282
|
+
await this.session.setAVPlaybackState(playbackState);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### 调用时机
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
// PlaybackController.onPrepared() 中调用 start
|
|
291
|
+
private onPrepared(): void {
|
|
292
|
+
// AVPlayer prepare 完成后,启动后台播放
|
|
293
|
+
const callbacks: PlaybackCallbacks = {
|
|
294
|
+
onPlay: async (): Promise<void> => { await this.play(); },
|
|
295
|
+
onPause: async (): Promise<void> => { await this.pause(); },
|
|
296
|
+
onSeek: async (timeMs: number): Promise<void> => { await this.seekTo(timeMs); },
|
|
297
|
+
onFastForward: async (): Promise<void> => { await this.fastForward(); },
|
|
298
|
+
onRewind: async (): Promise<void> => { await this.rewind(); },
|
|
299
|
+
onNext: async (): Promise<void> => { await this.skipToNext(); },
|
|
300
|
+
onPrevious: async (): Promise<void> => { await this.skipToPrevious(); }
|
|
301
|
+
};
|
|
302
|
+
this.backgroundManager.start(callbacks);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// PlaybackController.stopInternal() 中调用 stop
|
|
306
|
+
private async stopInternal(clearUI: boolean): Promise<void> {
|
|
307
|
+
await this.backgroundManager.stop();
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## workScheduler module.json5 配置模板
|
|
314
|
+
|
|
315
|
+
### extensionAbilities 注册
|
|
316
|
+
|
|
317
|
+
```json5
|
|
318
|
+
{
|
|
319
|
+
"module": {
|
|
320
|
+
"extensionAbilities": [
|
|
321
|
+
{
|
|
322
|
+
"name": "FeedUpdateWorkAbility",
|
|
323
|
+
"srcEntry": "./ets/workers/FeedUpdateWorkAbility.ets",
|
|
324
|
+
"type": "workScheduler",
|
|
325
|
+
"exported": false
|
|
326
|
+
}
|
|
327
|
+
]
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
### WorkSchedulerExtensionAbility 实现
|
|
333
|
+
|
|
334
|
+
```typescript
|
|
335
|
+
// 文件: workers/FeedUpdateWorkAbility.ets
|
|
336
|
+
import { WorkSchedulerExtensionAbility, workScheduler } from '@kit.BackgroundTasksKit';
|
|
337
|
+
import { hilog } from '@kit.PerformanceAnalysisKit';
|
|
338
|
+
import { FeedUpdateService } from '../network/FeedUpdateService';
|
|
339
|
+
|
|
340
|
+
const DOMAIN: number = 0x0000;
|
|
341
|
+
const TAG: string = 'FeedUpdateWorkAbility';
|
|
342
|
+
|
|
343
|
+
export default class FeedUpdateWorkAbility extends WorkSchedulerExtensionAbility {
|
|
344
|
+
onWorkStart(work: workScheduler.WorkInfo): void {
|
|
345
|
+
hilog.info(DOMAIN, TAG, 'Feed update work started, workId=%{public}d', work.workId);
|
|
346
|
+
const service = new FeedUpdateService();
|
|
347
|
+
service.refreshAllFeeds().then(() => {
|
|
348
|
+
hilog.info(DOMAIN, TAG, 'Feed update work completed');
|
|
349
|
+
}).catch((e: Error) => {
|
|
350
|
+
hilog.error(DOMAIN, TAG, 'Feed update work failed: %{public}s', e.message);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
onWorkStop(work: workScheduler.WorkInfo): void {
|
|
355
|
+
hilog.info(DOMAIN, TAG, 'Feed update work stopped, workId=%{public}d', work.workId);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### 注册定时工作
|
|
361
|
+
|
|
362
|
+
```typescript
|
|
363
|
+
import { workScheduler } from '@kit.BackgroundTasksKit';
|
|
364
|
+
|
|
365
|
+
function registerFeedUpdateWork(): void {
|
|
366
|
+
const workInfo: workScheduler.WorkInfo = {
|
|
367
|
+
workId: 1001,
|
|
368
|
+
bundleName: 'com.example.podcast',
|
|
369
|
+
abilityName: 'FeedUpdateWorkAbility',
|
|
370
|
+
isPersisted: true, // 重启后保留
|
|
371
|
+
repeatCycleTime: 7200000, // 2 小时(官方循环任务最短间隔;设了循环间隔须同时设 isRepeat 或 repeatCount 之一)
|
|
372
|
+
isRepeat: true, // 重复执行
|
|
373
|
+
networkType: workScheduler.NetworkType.NETWORK_TYPE_ANY // 需要网络
|
|
374
|
+
};
|
|
375
|
+
workScheduler.startWork(workInfo);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 取消定时工作
|
|
379
|
+
// ⚠️ stopWork(work: WorkInfo, needCancel?: boolean): void —— 首参是【完整 WorkInfo】,
|
|
380
|
+
// 与 startWork 同形,须含 workId + bundleName + abilityName(缺则编译报错 10505001:
|
|
381
|
+
// Type '{ workId: number; }' is missing the following properties from type 'WorkInfo': bundleName, abilityName)。
|
|
382
|
+
// 不要传 { workId } 裸对象。复用注册时那份 workInfo(或重建含三个必填项的完整对象)。
|
|
383
|
+
function cancelFeedUpdateWork(): void {
|
|
384
|
+
const workInfo: workScheduler.WorkInfo = {
|
|
385
|
+
workId: 1001,
|
|
386
|
+
bundleName: 'com.example.podcast',
|
|
387
|
+
abilityName: 'FeedUpdateWorkAbility'
|
|
388
|
+
};
|
|
389
|
+
workScheduler.stopWork(workInfo);
|
|
390
|
+
}
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
### workScheduler vs Android WorkManager 对比
|
|
394
|
+
|
|
395
|
+
| 特性 | Android WorkManager | HarmonyOS workScheduler |
|
|
396
|
+
|------|-------------------|----------------------|
|
|
397
|
+
| 最短间隔 | 15 分钟 | 2 小时(循环任务,官方下限) |
|
|
398
|
+
| 约束条件 | 网络、充电、空闲 | 网络、充电、存储 |
|
|
399
|
+
| 持久化 | 自动 | `isPersisted: true` |
|
|
400
|
+
| 链式任务 | 支持 (WorkContinuation) | 不支持 |
|
|
401
|
+
| 精确执行 | 不保证 | 不保证 |
|
|
402
|
+
| 注册方式 | `WorkManager.enqueue()` | `workScheduler.startWork()` |
|
|
403
|
+
| 实现方式 | `Worker` 类 | `WorkSchedulerExtensionAbility` |
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# 浏览器/URL 跳转
|
|
2
|
+
|
|
3
|
+
> 使用 `Want` + `startAbility` 拉起系统浏览器或其他应用,替代 Android 的 `Intent.ACTION_VIEW`。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 基本导入
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { common, Want } from '@kit.AbilityKit'
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 拉起浏览器打开 URL
|
|
16
|
+
|
|
17
|
+
> ✅ **验证状态**:已在 AboutPage.ets 中验证(网页和邮件跳转)
|
|
18
|
+
|
|
19
|
+
```typescript
|
|
20
|
+
function openBrowser(context: common.UIAbilityContext, url: string): void {
|
|
21
|
+
const want: Want = {
|
|
22
|
+
action: 'ohos.want.action.viewData',
|
|
23
|
+
uri: url
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
context.startAbility(want)
|
|
27
|
+
.then(() => {
|
|
28
|
+
hilog.info(DOMAIN, TAG, `Opened browser to ${url}`)
|
|
29
|
+
})
|
|
30
|
+
.catch((err: Error) => {
|
|
31
|
+
hilog.error(DOMAIN, TAG, `Failed to open browser: ${err.message}`)
|
|
32
|
+
})
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 常见 URL 协议
|
|
39
|
+
|
|
40
|
+
| 协议 | 用途 | 示例 | 验证状态 |
|
|
41
|
+
|------|------|------|---------|
|
|
42
|
+
| `https://` | 网页 | `openBrowser(ctx, 'https://example.com')` | ✅ 已验证 |
|
|
43
|
+
| `http://` | 网页 | `openBrowser(ctx, 'http://example.com')` | ✅ 已验证 |
|
|
44
|
+
| `mailto:` | 邮件 | `openBrowser(ctx, 'mailto:hello@example.com')` | ✅ 已验证 |
|
|
45
|
+
| `tel:` | 电话 | **电话拨号不属 URL 跳转** → 见 `telephony-dial.md`(用 `call.makeCall`);**不要**用 `openBrowser` / `viewData`+`tel:` | — |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 发送邮件(带主题)
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
function sendEmail(context: common.UIAbilityContext, to: string, subject: string): void {
|
|
53
|
+
const want: Want = {
|
|
54
|
+
action: 'ohos.want.action.viewData',
|
|
55
|
+
uri: `mailto:${to}?subject=${encodeURIComponent(subject)}`
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
context.startAbility(want).catch((err: Error) => {
|
|
59
|
+
hilog.error(DOMAIN, TAG, `Failed to send email: ${err.message}`)
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 其他常见 Want Action
|
|
67
|
+
|
|
68
|
+
| Action | 用途 |
|
|
69
|
+
|--------|------|
|
|
70
|
+
| `ohos.want.action.viewData` | 浏览器/数据查看 |
|
|
71
|
+
| `ohos.want.action.send` | 发送数据 |
|
|
72
|
+
| `ohos.want.action.edit` | 编辑数据 |
|
|
73
|
+
| `ohos.want.action.pick` | 选择数据 |
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## module.json5 配置
|
|
78
|
+
|
|
79
|
+
### Action 区别说明
|
|
80
|
+
|
|
81
|
+
| Action | 用途 | 是否需要配置 |
|
|
82
|
+
|--------|------|------------|
|
|
83
|
+
| `ohos.want.action.viewData` | 拉起浏览器查看数据 | 通常不需要(系统内置) |
|
|
84
|
+
| `ohos.want.action.view` | 响应外部应用的查看请求 | 需要配置(如相册响应外部查看图片) |
|
|
85
|
+
|
|
86
|
+
### 配置示例
|
|
87
|
+
|
|
88
|
+
```json5
|
|
89
|
+
{
|
|
90
|
+
"skills": [{
|
|
91
|
+
"entities": ["entity.system.home"],
|
|
92
|
+
"actions": [
|
|
93
|
+
"ohos.want.action.home",
|
|
94
|
+
"ohos.want.action.view" // 响应外部查看请求(如其他应用查看图片)
|
|
95
|
+
]
|
|
96
|
+
}]
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
> ⚠️ **重要**:
|
|
101
|
+
> - 使用 `ohos.want.action.viewData` **主动拉起**浏览器时,通常无需配置
|
|
102
|
+
> - 配置 `ohos.want.action.view` 是为了**被动响应**其他应用的查看请求
|
|
103
|
+
> - 相册应用需要配置 `ohos.want.action.view` 以响应外部查看图片/视频的请求
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## 常见错误
|
|
108
|
+
|
|
109
|
+
### 错误 1:URL 包含特殊字符未编码
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
// 错误 — 主题包含空格和特殊字符
|
|
113
|
+
uri: 'mailto:hello@example.com?subject=Hello World'
|
|
114
|
+
|
|
115
|
+
// 正确 — URL 编码
|
|
116
|
+
uri: 'mailto:hello@example.com?subject=' + encodeURIComponent('Hello World')
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 错误 2:startAbility 返回的 Promise 未处理
|
|
120
|
+
|
|
121
|
+
`startAbility` 可能抛出异常(如应用不存在),必须 catch 处理。
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# 系统相机 CameraKit
|
|
2
|
+
|
|
3
|
+
> 使用 `@kit.CameraKit` 的 `cameraPicker` 拉起系统相机,替代 Android 的 `MediaStore.ACTION_IMAGE_CAPTURE`。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 基本导入
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { cameraPicker, camera } from '@kit.CameraKit'
|
|
11
|
+
import { common } from '@kit.AbilityKit'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 拉起系统相机拍照
|
|
17
|
+
|
|
18
|
+
```typescript
|
|
19
|
+
async function openCamera(context: common.UIAbilityContext): Promise<string | null> {
|
|
20
|
+
// 配置相机参数
|
|
21
|
+
const pickerProfile: cameraPicker.PickerProfile = {
|
|
22
|
+
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK // 后置相机 ✅ 已验证
|
|
23
|
+
// cameraPosition: camera.CameraPosition.CAMERA_POSITION_FRONT // 前置相机(SDK 有声明)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// 拉起系统相机,用户拍照后返回
|
|
28
|
+
const result: cameraPicker.PickerResult = await cameraPicker.pick(
|
|
29
|
+
context,
|
|
30
|
+
[cameraPicker.PickerMediaType.PHOTO], // 拍照
|
|
31
|
+
pickerProfile
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
hilog.info(DOMAIN, TAG, `Photo picked: ${result.resultUri}`)
|
|
35
|
+
return result.resultUri
|
|
36
|
+
} catch (err) {
|
|
37
|
+
hilog.error(DOMAIN, TAG, `Pick photo error: ${err.code}, ${err.message}`)
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## 拉起系统相机录像
|
|
46
|
+
|
|
47
|
+
> ⚠️ **验证状态**:录像功能在当前代码仓未使用,仅供参考
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
async function openCameraVideo(context: common.UIAbilityContext): Promise<string | null> {
|
|
51
|
+
const pickerProfile: cameraPicker.PickerProfile = {
|
|
52
|
+
cameraPosition: camera.CameraPosition.CAMERA_POSITION_BACK
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const result: cameraPicker.PickerResult = await cameraPicker.pick(
|
|
57
|
+
context,
|
|
58
|
+
[cameraPicker.PickerMediaType.VIDEO], // 录像
|
|
59
|
+
pickerProfile
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
hilog.info(DOMAIN, TAG, `Video picked: ${result.resultUri}`)
|
|
63
|
+
return result.resultUri
|
|
64
|
+
} catch (err) {
|
|
65
|
+
hilog.error(DOMAIN, TAG, `Pick video error: ${err.code}, ${err.message}`)
|
|
66
|
+
return null
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## PickerMediaType 类型
|
|
74
|
+
|
|
75
|
+
| 类型 | 用途 | 验证状态 |
|
|
76
|
+
|------|------|---------|
|
|
77
|
+
| `PHOTO` | 拍照 | ✅ 已验证(Index.ets) |
|
|
78
|
+
| `VIDEO` | 录像 | ✅ SDK 有声明(PickerMediaType.VIDEO = 'video') |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## PickerResult 返回
|
|
83
|
+
|
|
84
|
+
```typescript
|
|
85
|
+
interface PickerResult {
|
|
86
|
+
resultUri: string // 拍摄/录制后的文件 URI
|
|
87
|
+
// 其他字段见官方 API 文档
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## module.json5 配置
|
|
94
|
+
|
|
95
|
+
需要在 skills 中声明相机相关 action 和 entity:
|
|
96
|
+
|
|
97
|
+
```json5
|
|
98
|
+
{
|
|
99
|
+
"skills": [{
|
|
100
|
+
"entities": [
|
|
101
|
+
"entity.system.home",
|
|
102
|
+
"entity.system.camera" // ← 关键:声明相机能力
|
|
103
|
+
],
|
|
104
|
+
"actions": [
|
|
105
|
+
"ohos.want.action.home",
|
|
106
|
+
"ohos.want.action.camera" // ← 关键:声明相机 action
|
|
107
|
+
]
|
|
108
|
+
}]
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 常见错误
|
|
115
|
+
|
|
116
|
+
### 错误:未在 module.json5 声明 camera entity
|
|
117
|
+
|
|
118
|
+
如果未声明 `entity.system.camera`,调用 `cameraPicker.pick()` 会失败,错误码因设备而异。
|