android2harmony 0.1.4 → 0.1.6
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/README.md +1 -407
- package/agents/self-tester.md +55 -376
- package/dist/index.js +238 -124
- package/dist/index.js.map +4 -4
- package/package.json +36 -32
- package/skills/a2h-resource-convert/SKILL.md +931 -0
- package/skills/a2h-resource-convert/references/code-vector-icon-rules.md +335 -0
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/conversion-rules.md +15 -2
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/dependency-analysis-rules.md +24 -2
- package/skills/a2h-resource-convert/references/lottie-conversion-rules.md +219 -0
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/resource-mapping-rules.md +27 -2
- package/skills/{hmos-resources-convert → a2h-resource-convert}/references/xml-drawable-to-svg-rules.md +118 -1
- package/skills/a2h-resource-convert/scripts/a2h_resource_convert.js +2186 -0
- package/skills/a2h-resource-convert/scripts/app_identity.js +741 -0
- package/skills/a2h-resource-convert/scripts/code_vector_icons.js +607 -0
- package/skills/a2h-resource-convert/scripts/package.json +3 -0
- package/skills/a2h-resource-convert/scripts/svg_fidelity_check.js +632 -0
- package/skills/a2h-ui-transfer/SKILL.md +431 -0
- package/skills/a2h-ui-transfer/references/conversion-procedure.md +547 -0
- package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/_/350/214/203/345/274/217/351/200/211/346/213/251/350/257/264/346/230/216.md +1 -1
- package/skills/a2h-ui-transfer/references/mvvm-v2/_/350/243/205/351/245/260/345/231/250/351/200/237/346/237/245.md +133 -0
- package/skills/{hmos-batch-ui-align/scripts/android_parse_fast.ts → a2h-ui-transfer/scripts/android_parse_fast.js} +532 -301
- package/skills/a2h-ui-transfer/scripts/arkts_static_check.js +1624 -0
- package/skills/a2h-ui-transfer/scripts/measure_pack.js +1005 -0
- package/skills/a2h-ui-transfer/scripts/package.json +3 -0
- package/skills/hmos-fix-build-errors/SKILL.md +1 -1
- package/skills/hmos-incremental-ui-align/README.md +15 -15
- package/skills/hmos-incremental-ui-align/SKILL.md +15 -15
- package/skills/hmos-incremental-ui-align/references/State_Model_Template.md +2 -2
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.js +790 -0
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.js +285 -0
- package/skills/hmos-incremental-ui-align/scripts/navigation-capure.md +76 -76
- package/skills/hmos-incremental-ui-align/scripts/page_capture.js +756 -0
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.js +155 -0
- package/skills/hmos-integration-test/README.md +341 -0
- package/skills/hmos-integration-test/SKILL.md +446 -0
- package/skills/hmos-integration-test/scripts/report-tool.mjs +646 -0
- package/skills/hmos-integration-test/scripts/resolve-metadata-tool.mjs +147 -0
- package/skills/hmos-integration-test/scripts/self-test-runner.mjs +1006 -0
- package/skills/hmos-integration-test/scripts/testcases-tool.mjs +189 -0
- package/skills/hmos-spec-generate/SKILL.md +26 -24
- package/skills/hmos-spec-generate/scripts/parse_requirements.ts +515 -0
- package/skills/hmos-spec-generate/template/REQ.txt +22 -0
- package/skills/hmos-spec-generate/template/REQ.xlsx +0 -0
- package/skills/hmos-batch-ui-align/SKILL.md +0 -141
- package/skills/hmos-batch-ui-align/references/conversion-procedure.md +0 -217
- package/skills/hmos-incremental-ui-align/scripts/app_feature_verify.ts +0 -999
- package/skills/hmos-incremental-ui-align/scripts/extract_checklist.ts +0 -343
- package/skills/hmos-incremental-ui-align/scripts/page_capture.ts +0 -977
- package/skills/hmos-incremental-ui-align/scripts/page_capture_burst.ts +0 -188
- package/skills/hmos-resources-convert/SKILL.md +0 -654
- package/skills/hmos-resources-convert/template/AppScope/app.json5 +0 -10
- package/skills/hmos-resources-convert/template/AppScope/resources/base/element/string.json +0 -8
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/AppScope/resources/base/media/layered_image.json +0 -7
- package/skills/hmos-resources-convert/template/build-profile.json5 +0 -42
- package/skills/hmos-resources-convert/template/code-linter.json5 +0 -32
- package/skills/hmos-resources-convert/template/entry/build-profile.json5 +0 -33
- package/skills/hmos-resources-convert/template/entry/hvigorfile.ts +0 -6
- package/skills/hmos-resources-convert/template/entry/obfuscation-rules.txt +0 -23
- package/skills/hmos-resources-convert/template/entry/oh-package.json5 +0 -10
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entryability/EntryAbility.ets +0 -48
- package/skills/hmos-resources-convert/template/entry/src/main/ets/entrybackupability/EntryBackupAbility.ets +0 -16
- package/skills/hmos-resources-convert/template/entry/src/main/ets/pages/Index.ets +0 -23
- package/skills/hmos-resources-convert/template/entry/src/main/module.json5 +0 -55
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/color.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/float.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/element/string.json +0 -16
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/background.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/foreground.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/layered_image.json +0 -7
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/backup_config.json +0 -3
- package/skills/hmos-resources-convert/template/entry/src/main/resources/base/profile/main_pages.json +0 -5
- package/skills/hmos-resources-convert/template/entry/src/main/resources/dark/element/color.json +0 -8
- package/skills/hmos-resources-convert/template/entry/src/mock/mock-config.json5 +0 -2
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/Ability.test.ets +0 -35
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/ets/test/List.test.ets +0 -5
- package/skills/hmos-resources-convert/template/entry/src/ohosTest/module.json5 +0 -16
- package/skills/hmos-resources-convert/template/entry/src/test/List.test.ets +0 -5
- package/skills/hmos-resources-convert/template/entry/src/test/LocalUnit.test.ets +0 -33
- package/skills/hmos-resources-convert/template/hvigor/hvigor-config.json5 +0 -23
- package/skills/hmos-resources-convert/template/hvigorfile.ts +0 -6
- package/skills/hmos-resources-convert/template/oh-package-lock.json5 +0 -28
- package/skills/hmos-resources-convert/template/oh-package.json5 +0 -10
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-atomic-component-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-interaction-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mappings/android-to-harmonyOS-ui-layout-mapping-reference.md +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Link/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Observed/350/243/205/351/245/260/345/231/250/345/222/214@ObjectLink/350/243/205/351/245/260/345/231/250/357/274/232/345/265/214/345/245/227/347/261/273/345/257/271/350/261/241/345/261/236/346/200/247/345/217/230/345/214/226.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Prop/350/243/205/351/245/260/345/231/250/357/274/232/347/210/266/345/255/220/345/215/225/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Provide/350/243/205/351/245/260/345/231/250/345/222/214@Consume/350/243/205/351/245/260/345/231/250/357/274/232/344/270/216/345/220/216/344/273/243/347/273/204/344/273/266/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@State/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Track/350/243/205/351/245/260/345/231/250/357/274/232class/345/257/271/350/261/241/345/261/236/346/200/247/347/272/247/346/233/264/346/226/260.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/@Watch/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/346/233/264/346/224/271/351/200/232/347/237/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/AppStorage/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200/347/232/204UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/Environment/357/274/232/350/256/276/345/244/207/347/216/257/345/242/203/346/237/245/350/257/242.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/LocalStorage/357/274/232/351/241/265/351/235/242/347/272/247UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/MVVM/346/250/241/345/274/217/357/274/210V1/357/274/211.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm/PersistentStorage/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm//347/256/241/347/220/206/345/272/224/347/224/250/346/213/245/346/234/211/347/232/204/347/212/266/346/200/201/346/246/202/350/277/260.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/!!/350/257/255/346/263/225/357/274/232/345/217/214/345/220/221/347/273/221/345/256/232.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Computed/350/243/205/351/245/260/345/231/250/357/274/232/350/256/241/347/256/227/345/261/236/346/200/247.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Event/350/243/205/351/245/260/345/231/250/357/274/232/350/247/204/350/214/203/347/273/204/344/273/266/350/276/223/345/207/272.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Local/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/206/205/351/203/250/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Monitor/350/243/205/351/245/260/345/231/250/357/274/232/347/212/266/346/200/201/345/217/230/351/207/217/344/277/256/346/224/271/345/274/202/346/255/245/347/233/221/345/220/254.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@ObservedV2/350/243/205/351/245/260/345/231/250/345/222/214@Trace/350/243/205/351/245/260/345/231/250/357/274/232/347/261/273/345/261/236/346/200/247/345/217/230/345/214/226/350/247/202/346/265/213.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Once/350/243/205/351/245/260/345/231/250/357/274/232/345/210/235/345/247/213/345/214/226/345/220/214/346/255/245/344/270/200/346/254/241.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Param/350/243/205/351/245/260/345/231/250/357/274/232/347/273/204/344/273/266/345/244/226/351/203/250/350/276/223/345/205/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Provider/350/243/205/351/245/260/345/231/250/345/222/214@Consumer/350/243/205/351/245/260/345/231/250/357/274/232/350/267/250/347/273/204/344/273/266/345/261/202/347/272/247/345/217/214/345/220/221/345/220/214/346/255/245.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/@Type/350/243/205/351/245/260/345/231/250/357/274/232/346/240/207/350/256/260/347/261/273/345/261/236/346/200/247/347/232/204/347/261/273/345/236/213.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/AppStorageV2/357/274/232/345/272/224/347/224/250/345/205/250/345/261/200UI/347/212/266/346/200/201/345/255/230/345/202/250.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/MVVM/346/250/241/345/274/217/357/274/210V2/357/274/211.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2/PersistenceV2/357/274/232/346/214/201/344/271/205/345/214/226/345/255/230/345/202/250UI/347/212/266/346/200/201.md" +0 -0
- /package/skills/{hmos-batch-ui-align → a2h-ui-transfer}/references/mvvm-v2//347/212/266/346/200/201/347/256/241/347/220/206V1/345/220/221V2/350/277/201/347/247/273/344/270/216/346/267/267/347/224/250/346/214/207/345/257/274.md" +0 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
# Android to HarmonyOS Code-Defined Vector Icon Rules
|
|
2
|
+
|
|
3
|
+
This document defines how to migrate vector icons that an Android project draws from **code** rather than from `res/`. Read this before converting any project that uses Jetpack Compose. Every code-defined icon observed during conversion must produce an inventory row and a mapping row per the rules in `references/resource-mapping-rules.md`.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
1. [Scope: Why `res/` Scanning Misses These Icons](#scope-why-res-scanning-misses-these-icons)
|
|
7
|
+
2. [Detection: When to Run This Analysis](#detection-when-to-run-this-analysis)
|
|
8
|
+
3. [Three Sources of Truth](#three-sources-of-truth)
|
|
9
|
+
4. [Extracting the Icon Inventory from `classes*.dex`](#extracting-the-icon-inventory-from-classesdex)
|
|
10
|
+
5. [Extracting Screen Attribution from Android Source](#extracting-screen-attribution-from-android-source)
|
|
11
|
+
6. [Resolving an Icon Name to Upstream Artwork](#resolving-an-icon-name-to-upstream-artwork)
|
|
12
|
+
7. [HarmonyOS Target Path and Filename](#harmonyos-target-path-and-filename)
|
|
13
|
+
8. [SVG Post-Processing](#svg-post-processing)
|
|
14
|
+
9. [AutoMirrored and Right-to-Left](#automirrored-and-right-to-left)
|
|
15
|
+
10. [Report and Mapping Requirements](#report-and-mapping-requirements)
|
|
16
|
+
11. [Fallbacks and Out-of-Scope Cases](#fallbacks-and-out-of-scope-cases)
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Scope: Why `res/` Scanning Misses These Icons
|
|
21
|
+
|
|
22
|
+
Jetpack Compose applications take most of their iconography from `androidx.compose.material:material-icons-core` and `androidx.compose.material:material-icons-extended`. Those icons are **Kotlin-generated `ImageVector` objects** built with a path DSL. They are compiled into `classes*.dex` and never appear in:
|
|
23
|
+
|
|
24
|
+
- the project's `res/drawable*/`,
|
|
25
|
+
- the APK's `resources.arsc`,
|
|
26
|
+
- therefore also not in `<decoded_output_path>/res/` after Step 3.
|
|
27
|
+
|
|
28
|
+
Every other step of this skill reads a `res/` tree. That makes these icons **structurally invisible** to the rest of the pipeline, and the failure is silent in a particularly bad way:
|
|
29
|
+
|
|
30
|
+
- Step 5.3 scans `layout*/` and `menu/` for `@type/name` references. A Compose app has no layout XML, so the scan finds nothing missing.
|
|
31
|
+
- Step 6.8 cross-checks `$r('app.media.xxx')` references that already exist in `.ets` files. A HarmonyOS shell project that has not been written yet contains no such references, so the check finds nothing missing.
|
|
32
|
+
|
|
33
|
+
Both gates report "all dependencies satisfied" while the migrated page renders no icons at all.
|
|
34
|
+
|
|
35
|
+
**Observed case.** On a real Compose application (Mihon), the More screen displays 10 icons. Exactly **one** of them (`R.drawable.ic_glasses_24dp`) is an Android resource. The other nine — including the icons in front of *Download queue* and *Categories* — come from `Icons.Outlined.*` / `Icons.AutoMirrored.Outlined.*`. Resource conversion converted the one resource correctly and produced nothing for the other nine, with no warning anywhere in the report.
|
|
36
|
+
|
|
37
|
+
## Detection: When to Run This Analysis
|
|
38
|
+
|
|
39
|
+
Run this analysis when **any** of the following is true:
|
|
40
|
+
|
|
41
|
+
1. `build.gradle`, `build.gradle.kts`, or `gradle/libs.versions.toml` declares `androidx.compose.material:material-icons-core` or `androidx.compose.material:material-icons-extended`.
|
|
42
|
+
2. Any `.kt` file in the project imports a path under `androidx.compose.material.icons.`.
|
|
43
|
+
3. `classes*.dex` in the decoded APK contains at least one string matching the icon-name pattern in the next section.
|
|
44
|
+
|
|
45
|
+
Condition 3 alone is sufficient — a project can pull the icon library in transitively through another dependency without declaring it.
|
|
46
|
+
|
|
47
|
+
If none hold, write `Code-defined vector icons: none detected` in the report's summary and skip the rest of this document.
|
|
48
|
+
|
|
49
|
+
## Three Sources of Truth
|
|
50
|
+
|
|
51
|
+
Three separate questions need three separate sources. Do not try to answer all three from one place.
|
|
52
|
+
|
|
53
|
+
| Question | Source | Needs `android_project_dir`? |
|
|
54
|
+
|---|---|---|
|
|
55
|
+
| **Which** icons does this app use? | `classes*.dex` string pool | No |
|
|
56
|
+
| **What** does each icon look like? | Upstream Material Icons snapshot | No |
|
|
57
|
+
| **Where** is each icon used (screen attribution)? | Android source `.kt` files | **Yes** |
|
|
58
|
+
|
|
59
|
+
### Why the dex is authoritative for scope
|
|
60
|
+
|
|
61
|
+
The dex reflects what was actually packaged: it covers icons used by **library** code as well as application code, and it **excludes** icons eliminated as dead code by R8. Grepping the Kotlin source alone both over-reports (references that were shrunk away) and under-reports (library-internal usage, and references reached through import aliases or indirection).
|
|
62
|
+
|
|
63
|
+
### Why the dex cannot give attribution
|
|
64
|
+
|
|
65
|
+
R8 obfuscates the icon library. In a release APK the package path `androidx/compose/material/icons` is absent from every dex, and the surviving icon identifiers are the plain name strings passed to the `materialIcon(name = "...")` builder, sitting in a flat, sorted string pool:
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
... Outlined.Public Outlined.PushPin Outlined.QueryStats Outlined.RadioButt ...
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The string tells you `QueryStats` is used. It does not tell you the *Statistics* row uses it. Recovering that link from the dex would require disassembly plus call-graph analysis to connect an obfuscated getter to both its name string and its call sites. Application class names typically **do** survive (e.g. `Leu/kanade/presentation/more/MoreScreenKt`), so the analysis is tractable — but reading the one line of Kotlin source is exact and costs nothing:
|
|
72
|
+
|
|
73
|
+
```kotlin
|
|
74
|
+
TextPreferenceWidget(
|
|
75
|
+
title = stringResource(MR.strings.label_download_queue),
|
|
76
|
+
icon = Icons.Outlined.GetApp, // <- attribution, directly readable
|
|
77
|
+
onPreferenceClick = onClickDownloadQueue,
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`android_project_dir` is already a required input of this skill (Step 2 resolves the application module from it; Step 5 correlates library dependencies from it), so attribution adds no new input requirement.
|
|
82
|
+
|
|
83
|
+
## Extracting the Icon Inventory from `classes*.dex`
|
|
84
|
+
|
|
85
|
+
Read each `classes*.dex` at `<decoded_output_path>/` as raw bytes and match this pattern against the byte stream (the strings are plain ASCII inside the DEX string pool):
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
(AutoMirrored\.)?(Filled|Outlined|Rounded|Sharp|TwoTone)\.[A-Z_][A-Za-z0-9_]*
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Deduplicate across all dex files. Each unique match is one **icon reference**.
|
|
92
|
+
|
|
93
|
+
Notes:
|
|
94
|
+
|
|
95
|
+
- **The name must start with an uppercase letter or `_`.** Compose icon names are PascalCase, and digit-leading names carry a `_` prefix. Without this constraint the pattern also captures Kotlin source filenames that the compiler embeds as debug strings — `Outlined.kt`, `Filled.kt`, `Rounded.kt` fit the shape exactly. Three such false positives were measured on one real application.
|
|
96
|
+
- `Icons.Default` is an alias of `Icons.Filled`, and `Icons.AutoMirrored.Default` of `Icons.AutoMirrored.Filled`. The string recorded in the dex is always the `Filled` form; no normalization is needed on the dex side (it *is* needed on the source side — see below).
|
|
97
|
+
- Names that begin with a digit are prefixed with `_` in Compose (`Icons.Filled._360`). Strip the leading `_` before name resolution.
|
|
98
|
+
- Do not silently drop a captured string that fails to resolve. Report it. A capture that resolves to nothing is either a false positive worth knowing about or a genuine gap in the snapshot.
|
|
99
|
+
|
|
100
|
+
When the decoded APK is unavailable (the Step 3 decode fell back to source `res/`), fall back to source-only extraction using the patterns in the next section, and record in the report that the inventory is source-derived and therefore may miss library-internal icon usage.
|
|
101
|
+
|
|
102
|
+
### When the icon library was not shrunk
|
|
103
|
+
|
|
104
|
+
The dex is authoritative for scope **only when R8 actually shrank the icon library**. When it did not, the entire `material-icons-extended` set is packaged and the dex inventory stops meaning "icons this app uses".
|
|
105
|
+
|
|
106
|
+
The signal is a large reference count that is almost entirely unattributed. One measured application produced **2149** dex references against **92** source-attributed ones — 96% dex-only. Synthesizing artwork for all of them would write ~2100 unused SVGs into `base/media/`.
|
|
107
|
+
|
|
108
|
+
When the reference count is at or above 300 **and** at least 80% of references have no source attribution, treat the library as unshrunk:
|
|
109
|
+
|
|
110
|
+
- synthesize artwork only for source-attributed references (`--attributed-only`),
|
|
111
|
+
- still emit an inventory and mapping row for every other reference, with `mapping_kind = unmappable` and reason `packaged by an unshrunk icon library; not referenced by application code`,
|
|
112
|
+
- state the situation in the report.
|
|
113
|
+
|
|
114
|
+
This is a deliberate trade-off, not a failure: the rows are all still there, so nothing is silently omitted, and the output stays proportionate to what the application actually renders.
|
|
115
|
+
|
|
116
|
+
## Extracting Screen Attribution from Android Source
|
|
117
|
+
|
|
118
|
+
Grep `.kt` files under `android_project_dir` for:
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
Icons\.(AutoMirrored\.)?(Filled|Outlined|Rounded|Sharp|TwoTone|Default)\.[A-Z_][A-Za-z0-9_]*
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Normalize `Default` → `Filled` so source matches join cleanly against dex matches.
|
|
125
|
+
|
|
126
|
+
Attribute each match to a screen using this evidence priority — the same best-effort discipline as `references/resource-mapping-rules.md` §6:
|
|
127
|
+
|
|
128
|
+
1. **Enclosing composable.** The nearest preceding `fun <Name>(` declaration in the file. If that name ends in `Screen`, `Page`, `Tab`, or `Dialog`, use it directly.
|
|
129
|
+
2. **File name.** `MoreScreen.kt` → `MoreScreen`.
|
|
130
|
+
3. **Shared widget.** If the enclosing composable is a reusable widget referenced from several screens, record the known screens, or `Common` when the list is too broad to be useful.
|
|
131
|
+
4. **No evidence.** `Unknown`.
|
|
132
|
+
|
|
133
|
+
Then propose a HarmonyOS page name the same way `references/lottie-conversion-rules.md` does: strip the `Screen` / `Activity` / `Fragment` / `Tab` suffix and append `Page` (`MoreScreen` → `MorePage`). Record it in `notes` as `page=<PageName>` and mark it as a suggestion to verify against the actual ArkUI pages.
|
|
134
|
+
|
|
135
|
+
An icon that appears in the dex but has **no** source match is still a required row. Record `Screen(s) = Unknown` and note `dex-only; no source reference found (likely used by a library)`.
|
|
136
|
+
|
|
137
|
+
## Resolving an Icon Name to Upstream Artwork
|
|
138
|
+
|
|
139
|
+
The Compose icon set is generated from the classic Material Icons in `google/material-design-icons`. The mapping is a pure string transform plus one index lookup.
|
|
140
|
+
|
|
141
|
+
### Step 1 — name transform
|
|
142
|
+
|
|
143
|
+
Strip any leading `_`, then split on **case boundaries only** and lowercase:
|
|
144
|
+
|
|
145
|
+
```
|
|
146
|
+
(?<=[a-z0-9])(?=[A-Z]) -> _ aB -> a_B
|
|
147
|
+
(?<=[A-Z])(?=[A-Z][a-z]) -> _ ABc -> A_Bc
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**Do not split on digit boundaries.** Upstream has no consistent rule there, and any fixed rule gets a batch of names wrong:
|
|
151
|
+
|
|
152
|
+
| Compose name | Material name | A digit rule would produce |
|
|
153
|
+
|---|---|---|
|
|
154
|
+
| `Filter1` | `filter_1` | — (needs the split) |
|
|
155
|
+
| `Co2` | `co2` | `co_2` ✗ |
|
|
156
|
+
| `Rotate90DegreesCcw` | `rotate_90_degrees_ccw` | — (needs the split) |
|
|
157
|
+
| `StarPurple500` | `star_purple500` | `star_purple_500` ✗ |
|
|
158
|
+
| `Grid3x3` | `grid_3x3` | `grid_3_x_3` ✗ |
|
|
159
|
+
| `Crop169` | `crop_16_9` | `crop_169` ✗ |
|
|
160
|
+
| `_3dRotation` | `3d_rotation` | `3_d_rotation` ✗ |
|
|
161
|
+
|
|
162
|
+
### Step 2 — three-stage lookup
|
|
163
|
+
|
|
164
|
+
Resolve the transformed name against the index in this order. Every stage is deterministic.
|
|
165
|
+
|
|
166
|
+
1. **Exact match.** `GetApp` → `get_app`, present in the index. Covers the large majority.
|
|
167
|
+
2. **Underscore-insensitive match.** Compare after deleting every `_` on both sides. This absorbs the entire digit-boundary problem in one rule: `crop169` matches `crop_16_9`, `grid3x3` matches `grid_3x3`, `filter1` matches `filter_1`, `starpurple500` matches `star_purple500`, `3drotation` matches `3d_rotation`. Measured across the upstream index, 2208 of 2209 names remain unique after deleting underscores; the single collision (`add_chart` / `addchart`, a legacy upstream duplicate) is already settled by stage 1, and otherwise breaks ties lexicographically.
|
|
168
|
+
3. **Alias table.** A few icons were renamed upstream while Compose kept the old name. Only entries verified against the index belong here — never guess:
|
|
169
|
+
|
|
170
|
+
| Compose name | Upstream name |
|
|
171
|
+
|---|---|
|
|
172
|
+
| `Motorcycle` | `two_wheeler` |
|
|
173
|
+
| `LeaveBagsAtHome` | `no_luggage` |
|
|
174
|
+
|
|
175
|
+
Anything still unresolved after stage 3 becomes an `unmappable` row. Do not invent artwork.
|
|
176
|
+
|
|
177
|
+
Measured over 10 real applications (2500 icon references), stage 1 resolved 2408, stage 2 resolved 91, the alias table resolved 2, and nothing was left unresolved.
|
|
178
|
+
|
|
179
|
+
### Step 3 — category lookup
|
|
180
|
+
|
|
181
|
+
The upstream repository stores icons at `src/<category>/<name>/<style_dir>/24px.svg`, and the category is not derivable from the name. Build a `name → category` index once from `update/current_versions.json`, whose keys have the form `<category>::<name>`.
|
|
182
|
+
|
|
183
|
+
**Exclude the `symbols` category.** That key namespace is the parallel *Material Symbols* set, which lives at `symbols/web/<name>/...` with a different on-disk layout. It duplicates nearly every classic icon name, so leaving it in makes every single lookup ambiguous and can produce paths that do not exist in `src/`.
|
|
184
|
+
|
|
185
|
+
After excluding `symbols`, a name that still maps to more than one category is a genuine ambiguity: pick the first in lexicographic order for determinism and record the alternatives in `notes`.
|
|
186
|
+
|
|
187
|
+
### Step 4 — style directory
|
|
188
|
+
|
|
189
|
+
| Compose style | Upstream directory | Filename suffix |
|
|
190
|
+
|---|---|---|
|
|
191
|
+
| `Filled` (and `Default`) | `materialicons` | `filled` |
|
|
192
|
+
| `Outlined` | `materialiconsoutlined` | `outlined` |
|
|
193
|
+
| `Rounded` | `materialiconsround` | `round` |
|
|
194
|
+
| `Sharp` | `materialiconssharp` | `sharp` |
|
|
195
|
+
| `TwoTone` | `materialiconstwotone` | `twotone` |
|
|
196
|
+
|
|
197
|
+
`AutoMirrored` is **not** a style. `AutoMirrored.Outlined.Label` and a hypothetical `Outlined.Label` resolve to the same upstream file.
|
|
198
|
+
|
|
199
|
+
### Step 5 — assemble
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
src/<category>/<material_name>/<style_dir>/24px.svg
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Worked examples from the Mihon More screen:
|
|
206
|
+
|
|
207
|
+
| Compose reference | Upstream path |
|
|
208
|
+
|---|---|
|
|
209
|
+
| `Outlined.GetApp` | `src/action/get_app/materialiconsoutlined/24px.svg` |
|
|
210
|
+
| `AutoMirrored.Outlined.Label` | `src/action/label/materialiconsoutlined/24px.svg` |
|
|
211
|
+
| `Outlined.QueryStats` | `src/editor/query_stats/materialiconsoutlined/24px.svg` |
|
|
212
|
+
| `Outlined.Storage` | `src/device/storage/materialiconsoutlined/24px.svg` |
|
|
213
|
+
| `Outlined.CloudOff` | `src/file/cloud_off/materialiconsoutlined/24px.svg` |
|
|
214
|
+
| `Filled.VolunteerActivism` | `src/maps/volunteer_activism/materialicons/24px.svg` |
|
|
215
|
+
|
|
216
|
+
### Snapshot pinning
|
|
217
|
+
|
|
218
|
+
The artwork source MUST be a pinned snapshot, not a moving branch. Reproducibility is the whole point of Step 2 resource patches: two runs of the same commit must produce byte-identical output. Record the pinned ref in the report and in the mapping document metadata.
|
|
219
|
+
|
|
220
|
+
**The default is `--fetch`: artwork is downloaded from a pinned upstream commit at run time.** The pin is what makes the run reproducible — never point the tool at a moving branch.
|
|
221
|
+
|
|
222
|
+
This carries one operational requirement worth recording in the run environment: **the machine needs network access to `raw.githubusercontent.com`**. A network-isolated runner (a hardened container, an offline test bench) will fail this step. Two consequences follow, and both should be handled by reporting rather than by silently degrading:
|
|
223
|
+
|
|
224
|
+
- the step exits non-zero and the icons are recorded as `unmappable` with reason `icon snapshot unavailable`;
|
|
225
|
+
- the `res/` conversion is unaffected and still valuable, so the run continues.
|
|
226
|
+
|
|
227
|
+
For such environments, `--snapshot <dir>` reads a local copy instead. It does not need the whole upstream repository — the clone is dominated by PNG exports, the Android XML tree, and the Material Symbols tree, none of which this skill reads. Only these are required (measured at commit `50f0603`: 10,751 files, 5.4 MB):
|
|
228
|
+
|
|
229
|
+
```
|
|
230
|
+
<material_icon_snapshot_dir>/
|
|
231
|
+
update/current_versions.json # the name -> category index
|
|
232
|
+
src/<category>/<name>/<style_dir>/24px.svg
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Record which upstream commit the copy was taken from.
|
|
236
|
+
|
|
237
|
+
## HarmonyOS Target Path and Filename
|
|
238
|
+
|
|
239
|
+
```
|
|
240
|
+
<ui_module>/src/main/resources/base/media/ic_<material_name>_<style_suffix>.svg
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
**The style suffix is mandatory.** In the classic Material Icons set, `Outlined.X` is sometimes different artwork from `Filled.X` and sometimes byte-identical:
|
|
244
|
+
|
|
245
|
+
```
|
|
246
|
+
bookmark filled d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"
|
|
247
|
+
bookmark outlined d="M17 3H7c-1.1 0-2 .9-2 2v16l7-3 7 3V5c0-1.1-.9-2-2-2z" <- differs
|
|
248
|
+
favorite filled / outlined <- identical
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Naming by bare `ic_<name>.svg` therefore silently drops one of the two whenever they differ. Always include the style. Keeping the byte-identical duplicates costs a few hundred bytes each and keeps the naming rule free of special cases.
|
|
252
|
+
|
|
253
|
+
`AutoMirrored` is **not** part of the filename — it is a property of the *reference*, handled at the component level (next section). So `AutoMirrored.Filled.KeyboardArrowLeft` and `Filled.KeyboardArrowLeft` correctly share one file.
|
|
254
|
+
|
|
255
|
+
**Collision with a `res/`-derived media file.** If the target filename already exists in the HarmonyOS output because a real Android drawable produced it, append `_material` (`ic_settings_outlined_material.svg`) and record both the collision and the rename in `notes`. Never overwrite a `res/`-derived asset.
|
|
256
|
+
|
|
257
|
+
## SVG Post-Processing
|
|
258
|
+
|
|
259
|
+
The upstream files are already 24×24 SVG with `viewBox="0 0 24 24"`, so no geometry transform is required. Apply exactly two transforms:
|
|
260
|
+
|
|
261
|
+
1. **Remove the no-op guard path.** Upstream files open with a transparent bounding rectangle that carries no visual information:
|
|
262
|
+
|
|
263
|
+
```xml
|
|
264
|
+
<path d="M0 0h24v24H0V0z" fill="none"/>
|
|
265
|
+
<path d="M0 0h24v24H0z" fill="none"/> <!-- variant -->
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Both variants are removed.
|
|
269
|
+
|
|
270
|
+
2. **Do not inject `fill`.** The remaining path deliberately carries no `fill` attribute. That matches Compose semantics, where the icon is tinted at the use site by `LocalContentColor`; on HarmonyOS the equivalent is `Image($r('app.media.…')).fillColor('#666666')`. Injecting a hard-coded `fill` here is the same defect that `references/xml-drawable-to-svg-rules.md` calls `FILL_INJECTED`, reached from a different source.
|
|
271
|
+
|
|
272
|
+
Keep `viewBox`, `width`, and `height` as upstream emits them.
|
|
273
|
+
|
|
274
|
+
## AutoMirrored and Right-to-Left
|
|
275
|
+
|
|
276
|
+
HarmonyOS qualifier directories have no left-to-right / right-to-left dimension — the qualifier order is `MCC_MNC-language_script_country/region-orientation-device-colormode-density`. Mirroring therefore **cannot** be expressed as a resource variant the way Android's `-ldrtl` does.
|
|
277
|
+
|
|
278
|
+
Handle it at the component level in ArkUI and record the requirement so the developer does not lose it:
|
|
279
|
+
|
|
280
|
+
- set `auto_mirrored = true` on the mapping record,
|
|
281
|
+
- write `AutoMirrored: mirror horizontally under right-to-left locales` into `notes`.
|
|
282
|
+
|
|
283
|
+
Do not emit a second, pre-mirrored SVG file. A pre-mirrored asset would have to be selected by code anyway, and it doubles the assets for no gain.
|
|
284
|
+
|
|
285
|
+
## Report and Mapping Requirements
|
|
286
|
+
|
|
287
|
+
### Conversion report
|
|
288
|
+
|
|
289
|
+
Add a dedicated section (see SKILL.md Step 7 for its position):
|
|
290
|
+
|
|
291
|
+
```
|
|
292
|
+
### Code-Defined Vector Icons
|
|
293
|
+
|
|
294
|
+
| Compose Reference | Material Name | Style | HarmonyOS Target | Host Screen(s) | Suggested Page | Notes |
|
|
295
|
+
|---|---|---|---|---|---|---|
|
|
296
|
+
| Icons.Outlined.GetApp | get_app | Outlined | base/media/ic_get_app_outlined.svg | MoreScreen | MorePage | |
|
|
297
|
+
| Icons.AutoMirrored.Outlined.Label | label | Outlined | base/media/ic_label_outlined.svg | MoreScreen | MorePage | AutoMirrored: mirror under RTL |
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
and these summary counters:
|
|
301
|
+
|
|
302
|
+
- Code-defined icon references found (dex)
|
|
303
|
+
- Resolved to upstream artwork
|
|
304
|
+
- Unresolved (recorded as unmappable)
|
|
305
|
+
- Distinct media files written
|
|
306
|
+
- AutoMirrored references
|
|
307
|
+
- Screen-attributed / `Unknown`
|
|
308
|
+
|
|
309
|
+
### Mapping document
|
|
310
|
+
|
|
311
|
+
Every icon reference gets one inventory row and one mapping row.
|
|
312
|
+
|
|
313
|
+
- **`android_resource_path`** — use `<source file>#Icons.<Style>.<Name>` when a source reference was found (e.g. `app/src/main/java/eu/kanade/presentation/more/MoreScreen.kt#Icons.Outlined.GetApp`); otherwise `classes.dex#Icons.<Style>.<Name>`.
|
|
314
|
+
- **`android_resource_name`** — the material snake_case name (`get_app`).
|
|
315
|
+
- **`function`** — describe the role from the host row's label when known (`Download queue row icon`), otherwise a conservative fallback (`Material icon used in code`).
|
|
316
|
+
- **`screens`** — per the attribution rules above; `Unknown` when dex-only.
|
|
317
|
+
- **`source_category`** — `第三方库资源`, with `notes` naming `androidx.compose.material:material-icons-extended`. These icons are library-provided; they are not application `res/` assets.
|
|
318
|
+
- **`type_category`** — `drawable`. Do not invent a new top-level type category.
|
|
319
|
+
- **`mapping_kind`** — `code-vector synthesized`, or `unmappable` when unresolved.
|
|
320
|
+
- **`notes`** — MUST record the upstream path and the pinned snapshot ref, so a reviewer can verify the artwork.
|
|
321
|
+
|
|
322
|
+
**Hard requirement:** never silently omit a code-defined icon reference. This is the same rule as §13 of `references/resource-mapping-rules.md`, and it is the rule whose absence caused the whole class of defects this document exists to fix.
|
|
323
|
+
|
|
324
|
+
## Fallbacks and Out-of-Scope Cases
|
|
325
|
+
|
|
326
|
+
| Situation | Handling |
|
|
327
|
+
|---|---|
|
|
328
|
+
| Name captured from dex does not resolve in the index | Emit the row with `harmony_target = N/A`, `mapping_kind = unmappable`, reason `not found in Material Icons index`. **Never invent artwork.** |
|
|
329
|
+
| Snapshot unavailable (no vendored copy, no network) | Emit every icon row as `unmappable` with reason `icon snapshot unavailable`, and state it prominently in the report. Do **not** fail the whole conversion — the `res/` conversion is still valuable. |
|
|
330
|
+
| `android_project_dir` unavailable or unreadable | Inventory still comes from the dex; set every `screens` to `Unknown` and note the limitation. |
|
|
331
|
+
| Decode failed, only source `res/` available | Extract from source `.kt` only; note that library-internal icon usage may be missing. |
|
|
332
|
+
| App-defined `ImageVector` built by hand in application code | **Out of scope.** Detect via `ImageVector.Builder` in application source, record as `unmappable` with reason `app-defined ImageVector, no upstream artwork`, and list it in the report so the developer redraws it manually. |
|
|
333
|
+
| Non-Material code icon libraries (e.g. a third-party Compose icon pack) | Out of scope for artwork. Record the references as `unmappable` with the library name in `notes` so the gap is visible. |
|
|
334
|
+
|
|
335
|
+
The last three rows matter: this document narrows a known blind spot, it does not close it completely. Anything it cannot resolve must become a visible row, never a silent omission.
|
package/skills/{hmos-resources-convert → a2h-resource-convert}/references/conversion-rules.md
RENAMED
|
@@ -37,7 +37,7 @@ Android resources live under `app/src/main/res/`. Common subdirectories:
|
|
|
37
37
|
|
|
38
38
|
## HarmonyOS Resource Directory Structure
|
|
39
39
|
|
|
40
|
-
HarmonyOS resources live under
|
|
40
|
+
HarmonyOS resources live under `<ui_module>/src/main/resources/` (where `<ui_module>` is typically `entry` for single-module projects, or a path like `products/phone` for layered projects). Structure:
|
|
41
41
|
|
|
42
42
|
```
|
|
43
43
|
resources/
|
|
@@ -533,6 +533,19 @@ HarmonyOS format: `<language>` or `<language>_<REGION>`
|
|
|
533
533
|
| `land` | `horizontal` |
|
|
534
534
|
| `port` | `vertical` |
|
|
535
535
|
|
|
536
|
+
### Device Type Qualifiers
|
|
537
|
+
|
|
538
|
+
HarmonyOS has a device dimension in its qualifier order, so Android UI-mode qualifiers that name a device map onto it rather than being stripped.
|
|
539
|
+
|
|
540
|
+
| Android | HarmonyOS |
|
|
541
|
+
|---|---|
|
|
542
|
+
| `watch` | `wearable` |
|
|
543
|
+
| `television` | `tv` |
|
|
544
|
+
| `car` | `car` |
|
|
545
|
+
| `desk`, `appliance` | `phone` (no distinct HarmonyOS target; note the approximation in the report) |
|
|
546
|
+
|
|
547
|
+
Do not silently drop these. Dropping `watch` merges wearable-specific artwork into `base/`, where it can overwrite the phone variant of the same resource name.
|
|
548
|
+
|
|
536
549
|
### Night Mode Qualifiers
|
|
537
550
|
|
|
538
551
|
| Android | HarmonyOS |
|
|
@@ -550,7 +563,7 @@ Android directory names can contain multiple qualifiers separated by hyphens, e.
|
|
|
550
563
|
2. The first segment is the resource type (`drawable`, `mipmap`, `values`, etc.)
|
|
551
564
|
3. The remaining segments are qualifiers. Parse them left-to-right, recognizing:
|
|
552
565
|
- Language codes: 2-3 lowercase letters (e.g., `zh`, `ar`, `en`)
|
|
553
|
-
- Region codes: `r` + 2 uppercase letters (e.g., `rCN`, `rUS`) — always follows a language
|
|
566
|
+
- Region codes: `r` + 2 uppercase letters (e.g., `rCN`, `rUS`) **or** `r` + 3 digits, the UN M.49 numeric region code (e.g., `r419` = Latin America) — always follows a language. HarmonyOS accepts both forms, so `values-es-r419` → `es_419/element/`. Treating the numeric form as unrecognized and stripping it merges that locale into the bare language directory (`es`), where its entries silently overwrite the other variant's — a mistranslation no existence check can see.
|
|
554
567
|
- Density: `ldpi`, `mdpi`, `hdpi`, `xhdpi`, `xxhdpi`, `xxxhdpi`, `nodpi`, `anydpi`, `tvdpi`
|
|
555
568
|
- Orientation: `land`, `port`
|
|
556
569
|
- Night mode: `night`, `notnight`
|
|
@@ -93,6 +93,27 @@ Resource references can appear in several places within Android XML files:
|
|
|
93
93
|
<color name="margin_double">@dimen/margin_base</color>
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
+
### Lottie-Specific Reference Patterns (Extended)
|
|
97
|
+
|
|
98
|
+
Lottie asset references do NOT use the generic `@type/name` syntax and are not caught by the reference regex above. They need their own extraction pass — see `references/lottie-conversion-rules.md` for the full rules. Summary:
|
|
99
|
+
|
|
100
|
+
**XML attribute patterns** (namespace prefix varies; match by local name only):
|
|
101
|
+
|
|
102
|
+
| Attribute | Points at | Migration classification |
|
|
103
|
+
|---|---|---|
|
|
104
|
+
| `lottie_fileName="Xxx.json"` | Assets-relative path under `assets/` | Local Lottie asset — migrate to `rawfile/lottie/<normalized>.json` |
|
|
105
|
+
| `lottie_rawRes="@raw/name"` | `res/raw/name.json` | Local Lottie asset — migrate to `rawfile/lottie/<normalized>.json` |
|
|
106
|
+
| `lottie_url="https://..."` | Runtime URL | Remote resource — no local target |
|
|
107
|
+
| `lottie_fallbackRes="@drawable/xxx"` | Fallback drawable | Handled by normal drawable rules; note the association in the Lottie row |
|
|
108
|
+
|
|
109
|
+
**Code patterns** (Kotlin/Java):
|
|
110
|
+
|
|
111
|
+
- Literal string / resource id arg to `LottieCompositionFactory.fromAsset(...)`, `.fromRawRes(...)`, `.fromJsonInputStream(...)`, `.fromUrl(...)`, or to `LottieAnimationView.setAnimation(...)`.
|
|
112
|
+
- `AssetManager.list(...)` results filtered by `.json` and dispatched to any factory method above — the "dynamic picker" pattern. Attribute the listed JSONs to the **detail screen** navigated to by the picker's click handler, not the picker itself.
|
|
113
|
+
- `CompositionArgs`-style data classes carrying `assetName` / `fileUri` / `url` fields consumed by a factory method — resolve to the actual factory call site.
|
|
114
|
+
|
|
115
|
+
**Host-screen binding.** For each Lottie asset, additionally record the Activity/Fragment that plays it (evidence: layout inflation, factory call site, or picker navigation target). This host attribution is required by `references/lottie-conversion-rules.md` and is consumed by the mapping document.
|
|
116
|
+
|
|
96
117
|
## Screen and Source Attribution Support
|
|
97
118
|
|
|
98
119
|
Use dependency analysis not only to resolve resources, but also to collect evidence for the mapping markdown.
|
|
@@ -294,7 +315,8 @@ Layout and menu files are **not** converted to HarmonyOS (they use ArkUI instead
|
|
|
294
315
|
|
|
295
316
|
### Skip (no meaningful dependencies)
|
|
296
317
|
|
|
297
|
-
- Raw files (`raw/`)
|
|
318
|
+
- Raw files (`raw/`) — **except** when the file is a Lottie animation JSON (identified by signature `v`+`fr`+`ip`+`op`+`layers`), which must be recorded per `references/lottie-conversion-rules.md`. The Lottie JSON itself has no `@type/name` references to resolve, but it must be inventoried and bound to a host screen.
|
|
319
|
+
- `assets/` — same rule: skip generic files, but detect and record Lottie JSON.
|
|
298
320
|
- Font files (`font/`) — unless font XML family definitions reference other fonts
|
|
299
321
|
- Image files (PNG, JPG, WEBP, etc.) — binary files, no references
|
|
300
322
|
|
|
@@ -330,7 +352,7 @@ When a dependency is satisfied, also verify qualifier coverage. If the dependent
|
|
|
330
352
|
|
|
331
353
|
## Handling Missing Library Resources
|
|
332
354
|
|
|
333
|
-
When converting from source `res/` (because
|
|
355
|
+
When converting from source `res/` (because decoding `apk_path` failed), library-provided resources will be absent from the lookup table. This section describes how to handle these gaps gracefully.
|
|
334
356
|
|
|
335
357
|
### Identifying Library Resources
|
|
336
358
|
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# Android to HarmonyOS Lottie Animation Conversion Rules
|
|
2
|
+
|
|
3
|
+
This document defines how to migrate Lottie animation resources from an Android project to a HarmonyOS project that uses `@ohos/lottie`. Read this before converting any Lottie animation. Every Lottie resource observed during conversion must produce an inventory row and a mapping row per the rules in `references/resource-mapping-rules.md`.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
1. [Scope and Runtime Context](#scope-and-runtime-context)
|
|
7
|
+
2. [How to Identify a Lottie JSON File](#how-to-identify-a-lottie-json-file)
|
|
8
|
+
3. [Source Locations in an Android Project](#source-locations-in-an-android-project)
|
|
9
|
+
4. [Target Path in the HarmonyOS Project](#target-path-in-the-harmonyos-project)
|
|
10
|
+
5. [Filename Normalization](#filename-normalization)
|
|
11
|
+
6. [Extracting Lottie References from Android Code](#extracting-lottie-references-from-android-code)
|
|
12
|
+
7. [Binding Each Lottie JSON to a HarmonyOS Page](#binding-each-lottie-json-to-a-harmonyos-page)
|
|
13
|
+
8. [Unmappable / Deferred Cases](#unmappable--deferred-cases)
|
|
14
|
+
9. [Report and Mapping Requirements](#report-and-mapping-requirements)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Scope and Runtime Context
|
|
19
|
+
|
|
20
|
+
On HarmonyOS the animation library is `@ohos/lottie` (installed via `ohpm install @ohos/lottie`). The JSON files are consumed at runtime via `lottie.loadAnimation({ path: 'lottie/animation.json', ... })`, where `path` is resolved under `<ui_module>/src/main/resources/rawfile/`. This skill handles **only** the JSON asset migration and its reference bookkeeping — it does NOT generate ArkTS UI code. The developer rewrites the hosting screen(s) in ArkUI and calls `@ohos/lottie` from that screen using the paths this skill produced.
|
|
21
|
+
|
|
22
|
+
## How to Identify a Lottie JSON File
|
|
23
|
+
|
|
24
|
+
The `.json` extension is not enough on its own — HarmonyOS projects can contain unrelated JSON. A file is a Lottie animation JSON if and only if its parsed root object contains **all** of the following top-level fields:
|
|
25
|
+
|
|
26
|
+
| Field | Meaning |
|
|
27
|
+
|---|---|
|
|
28
|
+
| `v` | Bodymovin exporter version, e.g. `"5.7.4"` |
|
|
29
|
+
| `fr` | Frame rate, number |
|
|
30
|
+
| `ip` | In-point (start frame), number |
|
|
31
|
+
| `op` | Out-point (end frame), number |
|
|
32
|
+
| `w` | Composition width, number |
|
|
33
|
+
| `h` | Composition height, number |
|
|
34
|
+
| `layers` | Array of layer objects |
|
|
35
|
+
|
|
36
|
+
If a JSON file has this signature, treat it as a Lottie animation regardless of the directory it lives in. If a JSON file is missing any of these fields, do not classify it as a Lottie asset; process it under the ordinary rules for its host directory (e.g., a raw JSON config in `res/raw/` follows the raw-file rules).
|
|
37
|
+
|
|
38
|
+
If a file cannot be parsed as JSON (broken, binary), record it as an inventory row with `Status = failed to parse` and `mapping_kind = failed conversion`.
|
|
39
|
+
|
|
40
|
+
## Source Locations in an Android Project
|
|
41
|
+
|
|
42
|
+
Android projects place Lottie JSON in one of two directories. This skill scans both:
|
|
43
|
+
|
|
44
|
+
| Android Location | Access Pattern in Android | Example |
|
|
45
|
+
|---|---|---|
|
|
46
|
+
| `app/src/main/assets/` (recursive) | `AssetManager.open(...)`, `LottieCompositionFactory.fromAsset(context, "AndroidWave.json")` | `assets/AndroidWave.json`, `assets/lottie/checkmark.json` |
|
|
47
|
+
| `app/src/main/res/raw/` | `R.raw.name`, `LottieCompositionFactory.fromRawRes(context, R.raw.lottielogo)` | `res/raw/lottielogo.json` |
|
|
48
|
+
|
|
49
|
+
`assets/` is a passthrough directory in APK decoding (unlike `res/`), so its files stay under `<decoded_output_path>/assets/` after decoding. Scan both `<decoded_output_path>/assets/` and `<decoded_output_path>/res/raw/` (or the source-fallback equivalents `app/src/main/assets/` and `app/src/main/res/raw/`).
|
|
50
|
+
|
|
51
|
+
When multi-module: scan every module's `src/*/assets/` and `src/*/res/raw/`, including flavor source sets (`src/debug/`, `src/<flavor>/`).
|
|
52
|
+
|
|
53
|
+
## Target Path in the HarmonyOS Project
|
|
54
|
+
|
|
55
|
+
All migrated Lottie animations land under a dedicated subdirectory of `rawfile/`:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
<ui_module>/src/main/resources/rawfile/lottie/<name>.json
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- The `lottie/` subdirectory keeps them separate from unrelated raw assets and matches the load-path convention used by `@ohos/lottie` examples (`path: 'common/lottie/animation.json'` style paths).
|
|
62
|
+
- Preserve the base filename (after normalization — see below). Discard any Android source subdirectory. If a project has `assets/animations/wave.json`, the target is still `rawfile/lottie/wave.json`.
|
|
63
|
+
- The load path recorded in the mapping document (for the developer to feed into `lottie.loadAnimation({ path: ... })`) is `lottie/<name>.json` — relative to `rawfile/`, no leading slash.
|
|
64
|
+
|
|
65
|
+
**Filename collision handling.** If two source files would normalize to the same target filename (e.g., `assets/logo.json` and `res/raw/logo.json` both target `lottie/logo.json`), disambiguate by prefixing the source category: `lottie/assets_logo.json` and `lottie/raw_logo.json`. Record both original paths in the mapping row's `notes`.
|
|
66
|
+
|
|
67
|
+
## Filename Normalization
|
|
68
|
+
|
|
69
|
+
HarmonyOS resource filenames must be all-lowercase, use only `[a-z0-9_]` in the base name, and only one `.` before the extension. Apply the following transforms to every Lottie source filename:
|
|
70
|
+
|
|
71
|
+
1. Strip the `.json` extension, transform the base name, then reattach `.json`.
|
|
72
|
+
2. Replace whitespace and any character in `[^a-zA-Z0-9_]` with `_`.
|
|
73
|
+
3. Collapse consecutive `_` into a single `_`.
|
|
74
|
+
4. Trim leading/trailing `_`.
|
|
75
|
+
5. Lowercase the entire base name.
|
|
76
|
+
6. If the base name starts with a digit after normalization, prefix with `lottie_`.
|
|
77
|
+
|
|
78
|
+
Examples:
|
|
79
|
+
|
|
80
|
+
| Android source filename | Normalized target under `rawfile/lottie/` |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `AndroidWave.json` | `androidwave.json` |
|
|
83
|
+
| `HamburgerArrow.json` | `hamburgerarrow.json` |
|
|
84
|
+
| `Lottie Logo 1.json` | `lottie_logo_1.json` |
|
|
85
|
+
| `Lottie Logo 2.json` | `lottie_logo_2.json` |
|
|
86
|
+
| `lottielogo.json` (from `res/raw/`) | `lottielogo.json` |
|
|
87
|
+
| `01-intro.json` | `lottie_01_intro.json` |
|
|
88
|
+
|
|
89
|
+
When a filename is renamed, record the rename in the mapping row's `notes` (`renamed from "Lottie Logo 1.json"`) and — if the reference is code-loaded via a literal string in Kotlin/Java — flag it in the report so the developer updates the code.
|
|
90
|
+
|
|
91
|
+
## Extracting Lottie References from Android Code
|
|
92
|
+
|
|
93
|
+
Lottie assets are wired to Android screens through three attribute-based reference patterns (in XML) plus a code-loaded pattern (in Kotlin/Java). Extend the reference-scanning defined in `references/dependency-analysis-rules.md` with the patterns below.
|
|
94
|
+
|
|
95
|
+
### XML attribute references
|
|
96
|
+
|
|
97
|
+
Namespace prefix varies (`app:`, `airbnb:`, custom bindings) — match by local name.
|
|
98
|
+
|
|
99
|
+
| Attribute | Reference target | Notes |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| `lottie_fileName` | Filename under `assets/` (e.g., `"AndroidWave.json"`) | Treat the string value as an assets-relative path. Match to the migrated `rawfile/lottie/<normalized>.json`. |
|
|
102
|
+
| `lottie_rawRes` | `@raw/<name>` | Match `<name>.json` inside `res/raw/`. The migrated target is `rawfile/lottie/<normalized>.json`. |
|
|
103
|
+
| `lottie_url` | Runtime URL string | No local file to migrate. Mark the reference as **remote** — `mapping_kind = remote resource (no local target)` and `source_category = 运行时远程资源`. Record the URL in `notes`. |
|
|
104
|
+
| `lottie_fallbackRes` | `@drawable/<name>` | Not a Lottie asset — falls under the normal drawable rules. Note the association in the Lottie row's `notes` because it is the fallback shown when the animation fails. |
|
|
105
|
+
|
|
106
|
+
For each XML reference, record the host layout file. That is the input to page binding (next section).
|
|
107
|
+
|
|
108
|
+
### Code-loaded Lottie
|
|
109
|
+
|
|
110
|
+
Some apps load Lottie JSON purely from code, including scanning `assets/` at runtime with `AssetManager.list("")`. Detection heuristics:
|
|
111
|
+
|
|
112
|
+
1. String literal ending in `.json` passed to `LottieCompositionFactory.fromAsset(...)`, `.fromRawRes(...)`, `.fromJsonInputStream(...)`, `.fromUrl(...)`, or to `LottieAnimationView.setAnimation(...)`.
|
|
113
|
+
2. `AssetManager.list(...)` results filtered by `.json` and passed into any of the factory methods above — this is the "dynamic dialog listing all assets" pattern (see `PreviewFragment` in the airbnb `lottie-android` sample).
|
|
114
|
+
3. Any `CompositionArgs`-like data class carrying an `assetName` / `fileUri` / `url` field consumed by a factory method.
|
|
115
|
+
|
|
116
|
+
For every code-loaded reference:
|
|
117
|
+
- If the source file is `assets/*.json` or `res/raw/*.json` → treat exactly like an XML reference in terms of migration (copy to `rawfile/lottie/<normalized>.json`).
|
|
118
|
+
- If the source is a URL or a user-picked file URI → mark as runtime remote resource, no local target.
|
|
119
|
+
- Record the Kotlin/Java source file and, if identifiable, the containing Activity/Fragment for page binding.
|
|
120
|
+
|
|
121
|
+
### Extending the reference table
|
|
122
|
+
|
|
123
|
+
Add the Lottie patterns above to whatever reference-extraction pipeline the skill runs. They are not covered by the generic `@type/name` regex in `references/dependency-analysis-rules.md`, so they need their own passes:
|
|
124
|
+
|
|
125
|
+
1. XML pass: for every layout / view XML file, extract every attribute whose local name matches `^lottie_(fileName|rawRes|url|fallbackRes)$`. Value handling is as described above.
|
|
126
|
+
2. Code pass: grep the project's Kotlin/Java sources for the factory method names listed above and, for each hit, capture the argument (literal filename, resource id, or URL) plus the enclosing class name.
|
|
127
|
+
|
|
128
|
+
## Binding Each Lottie JSON to a HarmonyOS Page
|
|
129
|
+
|
|
130
|
+
The whole point of the mapping report is telling the HarmonyOS developer **which screen in the new project plays which animation**. For every Lottie JSON, produce a *host* attribution using the same evidence sources as the standard screen-attribution rules (`references/resource-mapping-rules.md` §6, `references/dependency-analysis-rules.md` "Screen Attribution"), plus these Lottie-specific rules:
|
|
131
|
+
|
|
132
|
+
### Attribution priority
|
|
133
|
+
|
|
134
|
+
1. **XML host layout → hosting Activity/Fragment.**
|
|
135
|
+
- Find the layout XML(s) that reference the JSON via `lottie_fileName` / `lottie_rawRes`.
|
|
136
|
+
- Walk backward: which Activity/Fragment inflates that layout? Evidence includes `setContentView(R.layout.<name>)`, `AppCompatActivity(R.layout.<name>)` constructor form, `BaseFragment(R.layout.<name>)` / `Fragment.onCreateView` returning `R.layout.<name>`, and DataBinding/ViewBinding class names derived from the layout.
|
|
137
|
+
- Record `<Activity/Fragment class name>` in the `Host Activity/Fragment` column and `<layout file>` in `notes`.
|
|
138
|
+
|
|
139
|
+
2. **Code-loaded (literal filename in factory call).**
|
|
140
|
+
- Take the enclosing class as the host. If the class is a ViewModel or repository (not a screen), walk one hop up to the Fragment/Activity that observes it.
|
|
141
|
+
|
|
142
|
+
3. **Code-loaded (dynamic list from `AssetManager`).**
|
|
143
|
+
- This is the "preview / picker" pattern where the animation is rendered on whatever detail screen the user navigates to. Attribute every listed JSON to that **detail screen** (the target of the `startActivity` call from the picker), not the picker itself. The picker is the browser; the detail screen is the actual host.
|
|
144
|
+
- Example (from the airbnb `lottie-android` sample): `PreviewFragment` lists `assets/*.json` in an `AlertDialog` and calls `PlayerActivity.Companion.intent(context, CompositionArgs(asset = <name>))`. Attribute all listed JSONs to `PlayerActivity` (which in turn hosts `PlayerFragment` with the `LottieAnimationView`).
|
|
145
|
+
|
|
146
|
+
4. **Manifest / global.**
|
|
147
|
+
- If the JSON is loaded from `Application.onCreate`, a splash screen, or shared UI (nav header, tab bar), attribute to `Global` or `Launcher` per the shared-component / global rules in `resource-mapping-rules.md`.
|
|
148
|
+
|
|
149
|
+
5. **Fallback.**
|
|
150
|
+
- If no evidence exists, use `Unknown` and mention the reason in `notes` (e.g., "loaded via runtime string from unrecognized code path").
|
|
151
|
+
|
|
152
|
+
### Suggested HarmonyOS Page
|
|
153
|
+
|
|
154
|
+
Alongside the Android host, propose a HarmonyOS page name so the developer knows where to place the `@ohos/lottie` call. Use these rules:
|
|
155
|
+
|
|
156
|
+
- Strip the `Activity` / `Fragment` suffix and append `Page`. Examples: `PlayerActivity` → `PlayerPage`, `PlayerFragment` → `PlayerPage`, `LoginActivity` → `LoginPage`.
|
|
157
|
+
- Multiple hosts collapse to one page if they share the same base name (Activity + Fragment pair for the same screen).
|
|
158
|
+
- `Launcher` → `EntryAbility` (or the equivalent entry ability of the HarmonyOS project).
|
|
159
|
+
- `Global` → `App` / global scope.
|
|
160
|
+
- `Unknown` → `Unknown`.
|
|
161
|
+
|
|
162
|
+
This is a **suggestion column** — the mapping row's `notes` should say `HarmonyOS page name inferred from Android host; verify against actual ArkUI pages`.
|
|
163
|
+
|
|
164
|
+
## Unmappable / Deferred Cases
|
|
165
|
+
|
|
166
|
+
Record each of these with an explicit `mapping_kind` and keep the inventory row, never silently drop:
|
|
167
|
+
|
|
168
|
+
| Case | `mapping_kind` | Notes |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `lottie_url="https://..."` | `remote resource (no local target)` | HarmonyOS Target = `N/A`. Developer must fetch and re-render at runtime. |
|
|
171
|
+
| JSON referenced only by a runtime `Uri` from a file picker (`Intent.ACTION_GET_CONTENT`) | `remote resource (no local target)` | HarmonyOS Target = `N/A`. Developer handles user-provided files with the equivalent Picker API. |
|
|
172
|
+
| JSON present in `assets/` but not referenced anywhere in XML or code (dead asset) | `direct copy` still applies | Migrate anyway, mark screens = `Unknown`, note "no static references found — possibly loaded via string concatenation or dead asset". |
|
|
173
|
+
| JSON exists but fails Lottie signature check (missing `v`/`fr`/`layers` etc.) | `unmappable` | HarmonyOS Target = `N/A`. Notes: "JSON in assets/raw is not a valid Lottie composition; falls back to raw-file rules". |
|
|
174
|
+
| Lottie with embedded image assets (`images` field pointing to external files) | `direct copy` for the JSON + `direct copy` for each referenced image | Copy images alongside the JSON under `rawfile/lottie/images/<name>` (preserve relative structure declared by the Lottie `imagePath`). Record each image as its own inventory + mapping row. |
|
|
175
|
+
| Lottie referenced by `lottie_fallbackRes` only (used as fallback for another animation) | `direct copy` | Attribute to same host as the primary animation. Mention "fallback for @drawable/xxx or another JSON" in notes. |
|
|
176
|
+
|
|
177
|
+
## Report and Mapping Requirements
|
|
178
|
+
|
|
179
|
+
### Standard conversion report (Step 7)
|
|
180
|
+
|
|
181
|
+
Add a dedicated section:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
### Lottie Animation Resources
|
|
185
|
+
Every Lottie JSON detected in the project (via structure signature) and where it maps in HarmonyOS.
|
|
186
|
+
|
|
187
|
+
| Android Source | Target Rawfile Path | Host Activity/Fragment | Host Layout | HarmonyOS Page (suggested) | Notes |
|
|
188
|
+
|---|---|---|---|---|---|
|
|
189
|
+
| assets/AndroidWave.json | rawfile/lottie/androidwave.json | (none — XML host only) | res/layout/dynamic_activity.xml | DynamicPage | XML host inflated by DynamicActivity (inferred) |
|
|
190
|
+
| assets/Lottie Logo 1.json | rawfile/lottie/lottie_logo_1.json | PlayerActivity (via PreviewFragment picker) | N/A | PlayerPage | Renamed from "Lottie Logo 1.json" (spaces stripped). Loaded via AssetManager.list() in PreviewFragment; picker dispatches to PlayerActivity. |
|
|
191
|
+
| ... | ... | ... | ... | ... | ... |
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
Also add these summary counters to the "Summary" section:
|
|
195
|
+
- `Total Lottie animations found: <count>`
|
|
196
|
+
- `Migrated to rawfile/lottie/: <count>`
|
|
197
|
+
- `Remote / URL Lottie (no local target): <count>`
|
|
198
|
+
- `Filename collisions disambiguated: <count>`
|
|
199
|
+
|
|
200
|
+
### Mapping markdown (Step 8)
|
|
201
|
+
|
|
202
|
+
- Every Lottie JSON is one inventory row and at least one mapping row.
|
|
203
|
+
- Inventory row: `type_category = raw` (Lottie JSON lives under Android `assets/` or `res/raw/`, both of which normalize to Harmony's `rawfile/`). Explain the Lottie subtype in `notes` (`Lottie animation JSON`).
|
|
204
|
+
- Mapping row `harmony_target` values:
|
|
205
|
+
- Local migration → `<ui_module>/src/main/resources/rawfile/lottie/<normalized>.json`
|
|
206
|
+
- Remote / URL / user-picked → `N/A` with `mapping_kind = remote resource (no local target)`
|
|
207
|
+
- Mapping row `mapping_kind`:
|
|
208
|
+
- `direct copy` — for a straight assets/raw → rawfile move
|
|
209
|
+
- `rename` — used **in addition to** `direct copy` when the filename was normalized (emit as a second mapping row or record both in `notes`; the simpler option is one row with `mapping_kind = direct copy` and a `notes` entry `renamed from "<original>"`)
|
|
210
|
+
- `remote resource (no local target)` — URL / picker cases
|
|
211
|
+
- `unmappable` — JSON that fails the Lottie signature check
|
|
212
|
+
- Every Lottie mapping row MUST populate the Host Activity/Fragment (via `notes` if the mapping template does not have a dedicated column) and the suggested HarmonyOS page so the developer can wire the `@ohos/lottie` call to the correct ArkUI page.
|
|
213
|
+
- If the mapping markdown template supports extra columns, add `Host Activity/Fragment` and `Suggested HarmonyOS Page` columns to the Lottie section. Otherwise, put both facts in `notes` in the format `host=<class>; page=<PageName>`.
|
|
214
|
+
|
|
215
|
+
### Cross-check during Step 6 verification
|
|
216
|
+
|
|
217
|
+
- Verify every migrated Lottie file is present under `rawfile/lottie/` in the HarmonyOS output.
|
|
218
|
+
- Verify every `lottie_fileName` / `lottie_rawRes` reference discovered in XML has a corresponding entry in the report's Lottie table.
|
|
219
|
+
- If `harmony_project_dir` already contains ArkTS code and any `.ets` file already calls `lottie.loadAnimation({ path: 'lottie/xxx.json' })`, verify the file exists at `rawfile/lottie/xxx.json`. If missing, do **not** create a placeholder SVG (that's the drawable rule) — instead log it prominently as an unresolved Lottie reference so the developer can either drop in the JSON manually or fix the path.
|