Color By Number — Báo Cáo Audit Toàn Diện (Executive Summary)
Ngày: 2026-05-07 | Branch: main | Phiên bản: versionCode 1, versionName 1.0
Quy mô: 1 module Compose Android, 135 file .kt, 30 file .xml, ~13K LOC
Stack: Kotlin + Compose + Koin + Room + Retrofit + Coil + WorkManager + Apero FO SDK + Firebase
Báo cáo này tổng hợp 5 audit chuyên sâu (5 reviewer chạy song song). Đọc chi tiết tại:
audit-group-a-startup-architecture-260507-1130.mdaudit-group-b-compose-ui-ux-260507-1130.mdaudit-group-c-coroutines-crash-memory-260507-1130.mdaudit-group-d-data-network-db-260507-1130.mdaudit-group-e-security-ads-playstore-260507-1130.md
TL;DR
App có kiến trúc Clean + MVI bài bản, design system token-based đầy đủ, offline-first pattern implement đúng spec. Tuy nhiên KHÔNG đủ điều kiện ship Play Store vì:
- Release keystore + password committed vào git — mất toàn quyền sở hữu publisher.
- AdMob test ad units ở
appProdflavor — ship = $0 doanh thu + ban risk. - API key + Artifactory + Facebook ClientToken plaintext trong source/git history.
- Main thread blocking đa điểm —
runBlockingởattachBaseContext,ColoringViewparse SVG + region + distance transform main thread → ANR thực sự với painting nhiều region. - Dark mode hoàn toàn không hoạt động dù tokens đã định nghĩa đầy đủ.
- Schema migration
fallbackToDestructiveMigration(true)— bump v1→v2 = wipe toàn bộ progress + favorites của user.
Tổng số issue: ~115 (~24 Critical, ~37 High, ~46 Medium, ~31 Low).
Severity Distribution
| Group | Critical | High | Medium | Low | Total |
|---|---|---|---|---|---|
| A. Startup + DI + Architecture | 3 | 6 | 6 | 3 | 18 |
| B. Compose UI + UX + a11y | 6 | 12 | 13 | 15 | 46 |
| C. Coroutines + Crash + Memory | 5 | 6 | 6 | 3 | 20 |
| D. Data + Network + DB + Offline | 4 | 7 | 9 | 5 | 25 |
| E. Security + Ads + PlayStore | 5 | 6 | ~12 | ~5 | ~28 |
| Total | 23 | 37 | ~46 | ~31 | ~137 |
CRITICAL ISSUES — Chặn release (P0)
🔐 Security & Compliance (release-blocking)
| # | Issue | File | Group | Effort |
|---|---|---|---|---|
| S1 | Release keystore .jks + plaintext password commit vào git history; .gitignore không exclude |
app/build.gradle.kts:14-22, app/keystore/upload-keystore.jks |
E | 4-8h |
| S2 | API_KEY (jsonsilo) + Apero Artifactory creds (apero-terasofts:Apero@1234) hardcoded trong source + git |
core/common/AppConstants.kt:12, settings.gradle.kts:22-24, commit 450ac70 |
A, E | 3h |
| S3 | Facebook ClientToken plaintext trong AndroidManifest | AndroidManifest.xml:46 |
E | 1h |
💰 Revenue & Play Store policy (release-blocking)
| # | Issue | File | Group | Effort |
|---|---|---|---|---|
| S4 | AdMob TEST ad unit IDs (ca-app-pub-3940256099942544/...) ở cả appDev và appProd flavor + fo_config_ads.xml 28 slot |
app/build.gradle.kts:55-68, fo_config_ads.xml |
A, E | 2h + Apero coord |
| S5 | AdMob APPLICATION_ID cũng dùng test ID ở 2 flavor |
build.gradle.kts:44, 58 |
E | 30min |
| S6 | UMP/GDPR consent flow VẮNG MẶT — chỉ work nếu FO SDK handle internally (cần verify) | App init flow | E | 1-3 ngày |
| S7 | Crashlytics + Analytics auto-init trước khi user consent | ColorByNumberApplication.kt, AndroidManifest.xml |
E | 4h |
🐛 Stability & Data integrity (release-blocking)
| # | Issue | File | Group | Effort |
|---|---|---|---|---|
| D1 | fallbackToDestructiveMigration(true) ở production — bump schema = wipe coloring_progress + favorites |
core/di/LazyModule.kt |
D | 1 ngày |
| D2 | TemplateSyncWorker xóa toàn bộ catalog khi API trả empty [] (200 OK + empty body) |
data/sync/TemplateSyncWorker.kt, TemplateRepositoryImpl.kt |
C, D | 2h |
| D3 | saveThumbnail + extractZip không atomic — process kill giữa chừng để lại file partial mà existsAndNonEmpty() không phát hiện → asset stuck corrupt |
data/coloring/ColoringDownloader.kt, ColoringCache.kt |
D | 4h |
| D4 | ColoringView main-thread parse SVG Path + Region.setPath() + computePoleOfInaccessibility (Bitmap ALPHA_8 96×96 + distance transform 2-pass) → 1.5–2.5s blocking → ANR thực sự |
coloring/engine/ColoringView.kt, ColoringRegion.kt |
C | 1-2 ngày |
| D5 | revealBitmap (~16MB) + activePatternBitmap không cleanup khi setAsset gọi lại; shader giữ ref qua Paint → memory bloat / OOM |
coloring/engine/ColoringView.kt |
C | 4h |
| D6 | runBlocking { dataStore.first() } trong MainActivity.attachBaseContext — block main thread mỗi cold start + config change (~50-200ms) |
MainActivity.kt:22, PreferencesManager.kt:32 |
A, C, E | 4h |
| D7 | Zip-slip prefix check thiếu trailing separator → có thể write file out-of-cache-dir | ColoringDownloader.kt extractZip() |
C | 30min |
🎨 UX-blocking
| # | Issue | File | Group | Effort |
|---|---|---|---|---|
| U1 | Dark mode hoàn toàn không hoạt động — AppTheme hardcode LightAppColors dù có đủ DarkAppColors 49 fields |
designsystem/theme/AppTheme.kt:36-43 |
B | 1-2 ngày |
| U2 | Hardcoded Color(0xFF...) ngoài DS ở 6+ điểm (Palette, Calendar, RetryButton, ColoringScreen) → dark mode regression |
nhiều file | B | 1 ngày |
| U3 | HomeBannerPager render painter tĩnh ic_setting_share thay vì load banner.imageUrl qua AsyncImage → banner backend vô dụng |
home/component/HomeBannerPager.kt:97-103 |
B | 30min |
| U4 | Localization vi thiếu nhiều strings (Daily, Home, MyFeed, Coloring fallback English) + onboarding en/vi nội dung lệch hoàn toàn | res/values-vi/strings.xml |
B | 4h |
| U5 | MainBottomBar thiếu Role.Tab semantic, contentDescription đọc 2 lần, active state chỉ dùng alpha 0.6f |
navigation/MainBottomBar.kt |
B | 2h |
| U6 | ColoringView AndroidView state race: var view by mutableStateOf + LaunchedEffect đọc null view → completed update mất khi recompose trước factory chạy |
coloring/ColoringScreen.kt:57-86 |
B, C | 4h |
🏗️ Architecture/Init bugs
| # | Issue | File | Group | Effort |
|---|---|---|---|---|
| A1 | HomeViewModel đăng ký double trong cả EagerModule:56 + LazyModule:129 → race |
core/di/EagerModule.kt, LazyModule.kt |
A, C | 5min |
| A2 | AperoAd.init + FirstOpenSDK.init synchronous trong Application.onCreate — block trước Splash render |
ColorByNumberApplication.kt |
A | 4h |
| A3 | BaseViewModel._effect dùng Channel.BUFFERED + repeatOnLifecycle + collectLatest → ghost navigation / lost effects khi background |
presentation/base/BaseViewModel.kt |
C | 2h |
HIGH ISSUES (P1) — Cần fix trước milestone tiếp theo
Performance / Threading
snapshotBitmap()1MB (ARGB_8888 512×512) main thread mỗiON_STOP→ 30-100ms UI block + GC pressure (B-H2, C-07)- Compose
BodySection.drawBehindallocate Path mỗi frame (B-H8) MyFeedUiState.visibleItemscomputed property khôngderivedStateOf→ O(n log n) mỗi recomposition (B-H5)- HomeBannerPager
LaunchedEffectkhông dùngcollectLatest→ multipledelaypending khi swipe nhanh (B-H7) combineflow trong ViewModel khôngflowOn/distinctUntilChanged(C)
Network / Data
- OkHttp thiếu disk cache, không SSL pinning (jsonsilo HTTPS = MITM-able),
Level.BODYlog có thể leak signed URL token (D-#1) TemplateSyncWorkerthiếuConstraints(NetworkType.CONNECTED),BackoffPolicy.EXPONENTIAL, retry logic — fail thẳng cho mọi exception (D-#3)IntSetConverterparse string fragile (D-#5)- Progress save mất khi VM destroy giữa debounce 500ms (C-10)
Build / Release
- ProGuard rules trống +
isMinifyEnabled = true→ release sẽ crash mediation SDK (E) isShrinkResourceskhông bật → APK size phình (E)- backup_rules trống cho phép auto cloud backup → leak user data (E)
A11y / UX
AppTemplateCardthiếu loading state,contentDescription = nullcho painting + heart icon (B-H1)ColoringPaletteUnicode✓render rủi ro tofu, fontSize 11sp/14sp không respect font scale, touch 40dp < 48dp (B-H3)- Touch target nhiều chỗ < 48dp (B-M7, B-H3)
HomeContent.bannerNestedScrollConnectionthrash AnimatedVisibility khi fling (B-H4)MyFeedRowchunked(2) → row cuối lệch alignment (B-H6)MyFeedTabBaroverflow risk khi text vi dài (B-H12)ColorTemplateBottomSheetkhông có hide animation khi clear state (B-H9)Calendar.getDisplayName(Locale.ENGLISH)cứng → user vi vẫn thấy "January" (B-H11)ColoringScreenbackgroundColor.White+ strings hardcode English (B-H10)ALL_CATEGORYsemantics bug: prepend pseudo-row vào empty list → UI lừa user "có category" (D-#8)
App init
- Eager DataStore init kéo theo
runBlocking(A-H6) applyLocaleAndPopdùng deprecatedupdateConfiguration+ không re-trigger ComposestringResource(A-H9)- FO SDK splash không có local timeout (A-M13)
MEDIUM / LOW (P2-P3)
Tham khảo từng group report. Tóm lược:
- Recomposition:
HomePainting/MyFeedItem/HomeBannerthiếu@Stable/@Immutable; AppRaisedSurface.letchain phá modifier stability. - Design system gaps:
AppTextkhông exposelineHeight/fontWeight;themes.xmlparentTheme.MaterialComponents.DayNight.NoActionBardư thừa cho 100% Compose. - A11y: Nhiều component thiếu
Role.Button/RadioButton/Tab,progressSemantics, heading semantics. - Code smell:
ColoringScreen.ErrorStateSpacer(Modifier.width(0.dp))dead code; duplicate Modifier.width(102.dp).size(102.dp, 6.dp);colors.xmlcòn purple_200/500/700 template defaults. - Future-proof: targetSdk 36 yêu cầu Predictive Back, edge-to-edge mặc định, foreground service type rõ ràng — chưa thấy implement đầy đủ.
Cross-Cutting Themes
🔁 Anti-pattern lặp lại
runBlockingtrên main thread: 3 chỗ (MainActivity.attachBaseContext, PreferencesManager, sync paths). Pattern xử lý locale bootstrap cần redesign — dùng AtomicReference cache hoặc legacy SharedPreferences boot file thay DataStore.- Hardcoded magic values: API_KEY, keystore password, Artifactory creds, Facebook ClientToken, AdMob test IDs, Color(0xFF...) ngoài DS,
Locale.ENGLISHcứng — toàn bộ phải externalize. - Main-thread heavy work: ColoringView SVG parse + Region + distance transform + snapshot bitmap; thiếu
withContext(Dispatchers.Default)ở use case layer. - State race trong Compose ↔ AndroidView interop:
var view by remember { mutableStateOf }+ side-effect đọc state — không idiomatic. DùngAndroidView(factory, update)chuẩn. - Lack of consent gating: Crashlytics, Analytics, AdMob auto-init trước user consent → GDPR/CCPA risk.
✅ Điểm mạnh đáng ghi nhận
- Clean architecture (data/domain/presentation) + MVI Contract sạch.
- Offline-first pattern implement đúng theo doc: separated
observe*/triggerSync/observeSyncStatus,replaceAll1 transaction,formatVersionfilter cho schema drift, splash trigger sync, layered DTO→Entity→Domain mappers không leak Room types. - Design system token-based (49 color fields, CompositionLocal pattern chuẩn).
collectAsStateWithLifecycleconsistent (không thấycollectAsState).keyparameter dùng đúng trong tất cả LazyColumn/LazyGrid.CancellationExceptionre-throw chuẩn ở đa số try/catch.- Pole-of-Inaccessibility 2-pass distance transform — kỹ thuật tốt (chỉ sai chỗ chạy main thread).
- Type-safe Compose Navigation qua kotlinx.serialization.
Top 10 Priority Fixes (Action Plan)
| Priority | Issue | Effort | Owner | Blocker for |
|---|---|---|---|---|
| 1 | S1: Rotate keystore + git history cleanup + Play Console "Reset upload key" | 4-8h | DevOps | Release |
| 2 | S4 + S5: Swap test → real AdMob unit IDs cho appProd + APPLICATION_ID + CI gate chặn keyword 3940256099942544 |
2h | Dev + Apero | Revenue |
| 3 | S2: Rotate API_KEY + Apero Artifactory creds qua secret manager + local.properties |
3h | DevOps | Security |
| 4 | D1: Implement Room Migration objects + exportSchema = true + xóa fallbackToDestructiveMigration |
1 ngày | Backend dev | Data integrity |
| 5 | D4: Move ColoringView SVG parse + Region precompute + label geometry → Dispatchers.Default trong PrepareColoringUseCase |
1-2 ngày | Senior dev | ANR / UX |
| 6 | U1 + U2: Wire dark mode (if (darkTheme) DarkAppColors else LightAppColors) + promote tất cả hardcoded Color() vào AppColors |
1-2 ngày | UI dev | UX |
| 7 | D2 + D3: Guard TemplateSyncWorker chống empty + atomic file write (tmp+rename) + retry/backoff |
6h | Backend dev | Data integrity |
| 8 | D5: Bitmap lifecycle cleanup + recycle thumbnail sau save | 4h | Senior dev | Memory / OOM |
| 9 | D6 + A2: Cache locale qua AtomicReference; defer FO SDK / AperoAd init khỏi main thread | 6h | Senior dev | Cold start |
| 10 | S6: Verify FO SDK UMP handling; nếu thiếu, tích hợp Google UMP SDK trước Crashlytics/Analytics init | 1-3 ngày | Senior dev | GDPR / Compliance |
Estimated total for P0 fixes: 8-15 ngày dev (1.5-3 tuần với 1 senior dev).
Testing & Verification Recommendations
App hiện không có test (app/src/test trống). Khuyến nghị:
- DAO tests với in-memory Room cho FavoriteDao, ColoringProgressDao, PaintingCatalogDao.
- Mapper tests cho IntSetConverter, PaintingEntityMapper, ColoringProgressEntityMapper, ColoringAssetMapper.
- Worker tests với
TestListenableWorkerBuilderchoTemplateSyncWorker+ edge case "200 OK + empty body". - ViewModel tests với
MainDispatcherRule+kotlinx-coroutines-testcho HomeViewModel, ColoringViewModel, MyFeedViewModel. - Compose UI tests cho Bottom navigation, Coloring tap flow, Banner pager.
- Macrobenchmark cho cold start + frame time của ColoringView render.
- TalkBack manual test cho a11y sweep.
- Smoke test
assembleAppProdReleasetrước mọi merge để bắt ProGuard/R8 issue sớm.
Unresolved Questions (cần PM/Tech Lead trả lời)
- FO SDK & UMP: FO SDK có handle UMP/GDPR consent internally không? Nếu không, kế hoạch tích hợp Google UMP SDK ra sao?
- Apero ad units production: bao giờ nhận từ Apero account? Có thể CI gate hard-fail nếu thấy test ID trong appProd?
- Apero
apiKey: đã bind đúng applicationId production (com.colorbynumber.coloringgames.numberpaint) chưa? - Target user: có bao gồm trẻ em < 13 (COPPA / Families policy) không? Nếu có, ad mediation cần cấu hình child-directed flag.
- Play App Signing: đang dùng hay không? Nếu có, key trong repo chỉ là upload key — vẫn cần rotate nhưng impact thấp hơn.
- Painting max size: thực tế max bao nhiêu region trong painting production? Quyết định severity của D4 (ColoringView ANR).
- Module split timing: khi nào bắt đầu tách module (
:core:designsystem,:feature:home, …)? Hiện chưa cấp bách nhưng sẽ thành bottleneck khi team scale. - Schema migration strategy: dùng
Migrationobjects truyền thống hayformatVersionfilter ở mapper layer (hiện đang có sẵn cho catalog)? - Asset cache invalidation: TTL hay checksum? Hiện chưa có policy rõ.
- HomeBanner backend: API đã trả URL hợp lệ chưa? Cần fallback graceful nếu chưa.
- Onboarding nội dung: en "Tap Numbers, Bring Art to Life" vs vi "Chọn bức tranh bạn thích" — phiên bản nào đúng intent? PM xác nhận.
- Tablet/Foldable: có yêu cầu adaptive UI cho large screens không?
- Crashlytics PII policy: được phép log gì, không log gì?
- Dark mode design tokens: có Figma reference cho dark mode không?
DarkAppColorshiện đang đoán dựa Material defaults.
Recommended Roadmap
Phase 0 — Pre-release blockers (1-2 tuần)
- Top 10 priorities ở trên.
- Verify FO SDK UMP behavior.
- Smoke test release build.
Phase 1 — Stability & Compliance (1-2 tuần)
- Tất cả High issues group C (coroutine/memory/lifecycle).
- High issues group D (network resilience, atomic writes).
- High issues group E (ProGuard rules, isShrinkResources, backup rules).
- Test coverage tối thiểu (DAO, Mapper, Worker).
Phase 2 — UX polish (1-2 tuần)
- Dark mode hoàn thiện + Preview NIGHT_YES coverage.
- Localization vi đầy đủ + sync onboarding.
- A11y sweep (Roles, touch targets, contentDescription).
- Compose recomposition optimization (@Immutable, derivedStateOf).
- Tablet/Foldable adaptive UI (nếu yêu cầu).
Phase 3 — Architecture maturity (sau 1.0)
- Module split khi đạt threshold (15+ feature screens hoặc team >3 dev).
- Macrobenchmark + Baseline Profile.
- Spatial index cho ColoringView nếu painting > 500 region.
Tài liệu tham khảo
first-open-sdk-integration-guide.md(project root) — FO SDK init flow.offline-first-reactive-cache-pattern.md(project root) — design intent của data layer.PROJECT_BASE_PROMPT.md(project root) — project specs.- 5 group reports trong
plans/reports/audit-group-{a..e}-*-260507-1130.md.