Belum ada ide. Klik Edit untuk menambahkan.
{
"idPaket": "com.zomet.zxparentingkids",
"versiNama": "2.5.0",
"versiKode": 32,
"beranda": "https://zxparenting.zomet.my.id",
"api": "https://zxparenting.zomet.my.id/api/agent",
"repoRilis": "djvpri/ZX-Parenting-agent"
}{
"idPaket": "com.zomet.zxparentingkids",
"versiNama": "2.5.0",
"versiKode": 32,
"beranda": "https://zxparenting.zomet.my.id",
"api": "https://zxparenting.zomet.my.id/api/agent",
"repoRilis": "djvpri/ZX-Parenting-agent"
}
## Config build.gradle.kts - **namespace**: `com.zxparenting.agent` - **compileSdk**: 36 - **targetSdk**: 36 (wajib untuk app baru Okt 2026) - **minSdk**: 24 - **Java**: 17 - **Compose**: BOM 2024.12.01 - **isMinifyEnabled**: false (no R8/ProGuard) - **No native code** (no NDK/CMake/.so) ## Dependencies (15) - Compose (ui, material3, material-icons-extended) - AppCompat 1.7.0 - Retrofit 2.11.0 + Gson converter - OkHttp (logging interceptor) - Lifecycle (runtime-compose, viewmodel-compose) - buildConfig = true (BuildConfig.API, BuildConfig.VERSI_NAMA, BuildConfig.VERSI_KODE) ## Rilis - CI: `assembleDebug` per push to main - Rilis: `workflow_dispatch` β AAB + APK + GitHub Release - Keystore: GitHub Secrets (KEYSTORE_B64, KEYSTORE_PASSWORD, KEYSTORE_ALIAS, KEYSTORE_KEY_PASSWORD) - Tag: `git tag -f` + `push --force` (re-rilis versi sama OK)
com.zxparenting.agentassembleDebug per push to mainworkflow_dispatch β AAB + APK + GitHub Releasegit tag -f + push --force (re-rilis versi sama OK)App Android parental control untuk HP anak. Konsumsi API /api/agent dari web ZX Parenting. Dual fungsi:
1. Agent parental control β foreground service ping loop, blokir layar, GPS, app blocking
2. Edu-gamification β login anak, kuis AI (Gemini), tugas, konten official, hadiah
app/src/main/java/com/zxparenting/agent/ MainActivity.kt # Entry point, nav bottom bar (Beranda/Kuis/Tugas/Hadiah/Saya) PasangActivity.kt # Layar pairing device ortu StatusActivity.kt # Status koneksi device BlokirActivity.kt # Layar blokir fullscreen (sesi habis/curfew) AgentService.kt # Foreground service, ping loop 5 menit AksesService.kt # Accessibility service, deteksi app depan AdminReceiver.kt # Device admin receiver Api.kt # Retrofit interface (edu API: login, kuis, tugas, hadiah) β 359 lines ApiAgent.kt # HttpURLConnection client (agent API: ping, lokasi, activity) β 68 lines Simpanan.kt # SharedPreferences: token, JWT, profil anak Kebijakan.kt # Parser aturan server + keputusan blokir Blokir.kt # Status blokir aktif + daftar app diblokir Lokasi.kt # GPS on-demand, lastKnownLocation Pemakaian.kt # UsageStatsManager, laporan app usage ManajerBahasa.kt # i18n: EN default + ID, AppCompatDelegate PemeriksaPembaruan.kt # Auto-update APK dari GitHub releases Pemberitahuan.kt # Notifikasi pengingat Ctx.kt # Global context helper DataDemo.kt # Sample data untuk mode demo (reviewer Play Console) LaporError.kt # Error reporting ke backend (POST /api/agent lapor-error) LayarBeranda.kt # Feed konten official (kuis/artikel) LayarMasuk.kt # Login anak (username + PIN) + Mode Demo LayarSaya.kt # Profil anak, bahasa, device admin status LayarTugas.kt # Daftar tugas anak LayarKuisAI.kt # Buat kuis AI (Gemini on-demand) LayarKuisDetail.kt # Detail kuis official LayarKuisSession.kt # Sesi kuis: jawab per-soal, lock, review LayarHadiah.kt # Katalog hadiah + tukar token β waktu main tema/Palet.kt # Color palette tema/WarnaAktif.kt # Dynamic color scheme
app/src/main/java/com/zxparenting/agent/
MainActivity.kt # Entry point, nav bottom bar (Beranda/Kuis/Tugas/Hadiah/Saya)
PasangActivity.kt # Layar pairing device ortu
StatusActivity.kt # Status koneksi device
BlokirActivity.kt # Layar blokir fullscreen (sesi habis/curfew)
AgentService.kt # Foreground service, ping loop 5 menit
AksesService.kt # Accessibility service, deteksi app depan
AdminReceiver.kt # Device admin receiver
Api.kt # Retrofit interface (edu API: login, kuis, tugas, hadiah) β 359 lines
ApiAgent.kt # HttpURLConnection client (agent API: ping, lokasi, activity) β 68 lines
Simpanan.kt # SharedPreferences: token, JWT, profil anak
Kebijakan.kt # Parser aturan server + keputusan blokir
Blokir.kt # Status blokir aktif + daftar app diblokir
Lokasi.kt # GPS on-demand, lastKnownLocation
Pemakaian.kt # UsageStatsManager, laporan app usage
ManajerBahasa.kt # i18n: EN default + ID, AppCompatDelegate
PemeriksaPembaruan.kt # Auto-update APK dari GitHub releases
Pemberitahuan.kt # Notifikasi pengingat
Ctx.kt # Global context helper
DataDemo.kt # Sample data untuk mode demo (reviewer Play Console)
LaporError.kt # Error reporting ke backend (POST /api/agent lapor-error)
LayarBeranda.kt # Feed konten official (kuis/artikel)
LayarMasuk.kt # Login anak (username + PIN) + Mode Demo
LayarSaya.kt # Profil anak, bahasa, device admin status
LayarTugas.kt # Daftar tugas anak
LayarKuisAI.kt # Buat kuis AI (Gemini on-demand)
LayarKuisDetail.kt # Detail kuis official
LayarKuisSession.kt # Sesi kuis: jawab per-soal, lock, review
LayarHadiah.kt # Katalog hadiah + tukar token β waktu main
tema/Palet.kt # Color palette
tema/WarnaAktif.kt # Dynamic color scheme
## Stats - 30 Kotlin files, ~4764 LOC - 143 strings (EN), 143 strings (ID) - 5 tab: Beranda, Kuis, Tugas, Hadiah, Saya
<!-- Permissions -->
INTERNET
ACCESS_NETWORK_STATE
REQUEST_INSTALL_PACKAGES <!-- auto-update APK -->
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
FOREGROUND_SERVICE
FOREGROUND_SERVICE_LOCATION
POST_NOTIFICATIONS
PACKAGE_USAGE_STATS <!-- tools:ignore=ProtectedPermissions -->
<!-- Components -->
MainActivity (LAUNCHER)
StatusActivity (exported=false)
PasangActivity (exported=false)
BlokirActivity (singleInstance, showOnLockScreen, excludeFromRecents)
AgentService (foregroundServiceType=location)
AksesService (BIND_ACCESSIBILITY_SERVICE)
AdminReceiver (BIND_DEVICE_ADMIN)
FileProvider (${applicationId}.fileprovider)<!-- Permissions -->
INTERNET
ACCESS_NETWORK_STATE
REQUEST_INSTALL_PACKAGES <!-- auto-update APK -->
ACCESS_FINE_LOCATION
ACCESS_COARSE_LOCATION
FOREGROUND_SERVICE
FOREGROUND_SERVICE_LOCATION
POST_NOTIFICATIONS
PACKAGE_USAGE_STATS <!-- tools:ignore=ProtectedPermissions -->
<!-- Components -->
MainActivity (LAUNCHER)
StatusActivity (exported=false)
PasangActivity (exported=false)
BlokirActivity (singleInstance, showOnLockScreen, excludeFromRecents)
AgentService (foregroundServiceType=location)
AksesService (BIND_ACCESSIBILITY_SERVICE)
AdminReceiver (BIND_DEVICE_ADMIN)
FileProvider (${applicationId}.fileprovider)
## Permission Flow 1. User install app β login anak (username + PIN) 2. Pairing device ortu (deviceToken dari dashboard) 3. Request: location, usage access, device admin, accessibility 4. AgentService start foreground β ping loop 5 menit
1. User install app β login anak (username + PIN)
2. Pairing device ortu (deviceToken dari dashboard)
3. Request: location, usage access, device admin, accessibility
4. AgentService start foreground β ping loop 5 menit
START_STICKY (Android restart saat sistem matikan).
PERIODE_MS = 5 * 60_000L β default interval, tidak boros baterai.
hitungPeriode() cek Kebijakan.menitKeCurfew(jam, menit, mulai, selesai):
menitKeCurfew() support curfew lintas tengah malam (mis 21:00-05:00):
Menggantikan Thread.sleep loop β lebih responsif, tidak block thread.
1. ApiAgent.ping(context) β POST /api/agent {aksi: "ping"} dengan header x-device-token
2. Simpan timestamp ping terakhir
3. Parse respons β Aturan (sessionLimit, cooldown, dailyLimit, curfew, appBlokir)
4. Simpan aturan via Kebijakan.simpan()
5. Simpan daftar app diblokir via Blokir.simpanDaftarApp()
6. Catat sesi_main bila sesiTerakhir == null && !masterUnlock
7. GPS on-demand: Lokasi.kirimBilaDiminta() bila server minta
8. Evaluasi blokir: Kebijakan.evaluasi(aturan, jam, menit) β tampilkan/sembunyikan BlokirActivity
9. β
Cek accessibility: AksesService.instance == null β notifikasi + log "accessibility_off"
10. Tiap 12 ping (~1 jam): kirim laporan usage stats bila izin ada
Channel agent (IMPORTANCE_LOW), ongoing notification "ZX Agent active" + "Device connected to parent".
Channel alert (IMPORTANCE_HIGH) untuk notifikasi accessibility off.
Api.ping() β JSONObject respons atau nullApi.kirimLokasi(lat, lng, akurasi) β BooleanApi.kirimAktivitas(tipe, detail?) β BooleanApi.laporError(screen, message, extra?) β BooleanBearer JWT auth. Endpoint:
POST auth/native-anak β login (username + PIN) β JWT + AnakInfoGET anak/official β feed konten (kuis harian, tantangan, artikel) + officialMeta nestedGET anak/official/{id} β detail kuis + soalPOST anak/official/{id}/complete β submit jawaban β skor + token rewardPOST anak/kuis-ai/start β generate kuis AI (Gemini) β attemptId + soalPOST anak/kuis-ai/{attemptId}/submit β submit jawaban β hasil + explainGET anak/tugas β daftar tugas anakPOST anak/tugas/{id}/session/start β mulai sesi kuis tugasPOST anak/tugas/session/{id}/submit β submit jawaban sesiGET anak/tugas/session/{id}/review β review jawaban + pembahasanGET anak/saldo β saldo token anakGET anak/streak β streak harian + terbaikGET anak/challenge β challenge hari ini + status selesaiGET anak/hadiah β katalog hadiahPOST anak/hadiah/{id}/tukar β tukar token β hadiahLoginAnakReq/Res, OfficialItem/Meta/Feed, TugasAnak, Soal, OfficialDetail, CompleteReq/Res, KuisAIReq/Res, SubmitKuisReq/Res, HasilSoal, KuisSessionStartReq/Res, SoalSession, JawabanLocked, KuisSessionSubmitReq/Res, ReviewSoal, KuisSessionReviewRes, SaldoRes, StreakRes, ChallengeRes, HadiahItem, TukarReq/Res
GET /api/diagnostik?token=<CRON_TOKEN> β health snapshot (DB, env, kuis harian, errors, tugas)GET /api/error-log?token=<CRON_TOKEN> β error log machine-readablescripts/smoke-test.sh β E2E smoke test (login β feed β saldo β streak β challenge β tugas β diagnostik)Parser aturan server + evaluator blokir.
data class Aturan(
sessionLimit: Int, // menit per sesi
cooldown: Int, // menit jeda antar sesi
dailyLimit: Int, // menit total per hari
curfewMulai: String?, // "21:00"
curfewSelesai: String?, // "05:00"
masterUnlock: Boolean, // ortu override
sesiTerpakaiHariIni: Int,
sesiTerakhir: Long?, // epoch ms
gpsMinta: Boolean,
appBlokir: List<String>, // daftar paket diblokir
)data class Aturan(
sessionLimit: Int, // menit per sesi
cooldown: Int, // menit jeda antar sesi
dailyLimit: Int, // menit total per hari
curfewMulai: String?, // "21:00"
curfewSelesai: String?, // "05:00"
masterUnlock: Boolean, // ortu override
sesiTerpakaiHariIni: Int,
sesiTerakhir: Long?, // epoch ms
gpsMinta: Boolean,
appBlokir: List<String>, // daftar paket diblokir
)
### AlasanBlokir enum `JAM_TIDUR` | `DAILY_HABIS` | `COOLDOWN` ### Kebijakan.evaluasi(aturan, jam, menit) β AlasanBlokir? - Cek curfew (support lintas tengah malam, mis 21:00-05:00) - Cek daily limit terpakai - Cek cooldown ## Blokir.kt (46 lines) Status blokir di SharedPreferences (bertahan walau service dibunuh). - `nyalakan(alasan)` / `matikan()` / `aktif()` / `alasan()` - `simpanDaftarApp(daftar)` / `daftarApp()` β JSON array - `overlayAppAktif` β volatile flag untuk AksesService overlay ## BlokirActivity.kt (94 lines) Fullscreen, `singleInstance`, `showOnLockScreen`, `excludeFromRecents`. - Tampilkan pesan blokir (bedtime/daily/cooldown) - Tombol "Minta Izin Ortu" - `selesaiJikaAda()` β static method dismiss
JAM_TIDUR | DAILY_HABIS | COOLDOWN
Status blokir di SharedPreferences (bertahan walau service dibunuh).
nyalakan(alasan) / matikan() / aktif() / alasan()simpanDaftarApp(daftar) / daftarApp() β JSON arrayoverlayAppAktif β volatile flag untuk AksesService overlayFullscreen, singleInstance, showOnLockScreen, excludeFromRecents.
selesaiJikaAda() β static method dismissGPS on-demand β server minta (gps.minta=true di ping) β kirim posisi.
1. Cek izin ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION
2. Ambil lastKnownLocation dari GPS/NETWORK/PASSIVE provider β pilih terbaru
3. Kirim via Api.kirimLokasi(lat, lng, akurasi)
Hanya posisi terakhir dikirim, tanpa histori. Bila lokasi terlalu tua (>= 10 menit), requestSingleUpdate dengan timeout singkat.
Fase 5: blokir per-app. Deteksi app yang pindah ke depan via TYPE_WINDOW_STATE_CHANGED.
1. Event TYPE_WINDOW_STATE_CHANGED β e.packageName
2. Skip app sendiri + launcher sistem
3. Cek Blokir.daftarApp() β bila paket ada di daftar β tampilkan BlokirActivity overlay
4. Anak pindah ke app tidak diblokir β lepas overlay (Blokir.overlayAppAktif = false)
accessibilityEventTypes: typeWindowStateChanged accessibilityFeedbackType: feedbackGeneric accessibilityFlags: flagIncludeNotImportantViews notificationTimeout: 100
accessibilityEventTypes: typeWindowStateChanged
accessibilityFeedbackType: feedbackGeneric
accessibilityFlags: flagIncludeNotImportantViews
notificationTimeout: 100
Laporan pemakaian app via UsageStatsManager. Butuh izin PACKAGE_USAGE_STATS (user setujui di Settings).
Kirim ke server: Api.kirimAktivitas(c, "app_usage", {per_app, jam}). Dipanggil tiap 12 ping (~1 jam).
Cek via AppOpsManager.unsafeCheckOpNoThrow(OPSTR_GET_USAGE_STATS)
| Screen | File | Fungsi |
|--------|------|--------|
| Beranda | LayarBeranda.kt | Feed konten official (kuis harian, tantangan, artikel) |
| Kuis AI | LayarKuisAI.kt | Buat kuis custom via Gemini (topik + level + jumlah) |
| Kuis Detail | LayarKuisDetail.kt | Detail kuis official + mulai sesi |
| Kuis Session | LayarKuisSession.kt | Sesi kuis: jawab per-soal, lock jawaban, review |
| Tugas | LayarTugas.kt | Daftar tugas anak + mulai sesi kuis tugas |
| Hadiah | LayarHadiah.kt | Katalog hadiah + tukar token β waktu main |
| Saya | LayarSaya.kt | Profil anak, bahasa, device admin status |
| Login | LayarMasuk.kt | Login anak (username + PIN) + Mode Demo |
| Blokir | BlokirActivity.kt | Layar blokir fullscreen (sesi habis/curfew) |
| Pasang | PasangActivity.kt | Pairing device ortu |
| Status | StatusActivity.kt | Status koneksi device |
tema/Palet.kt β color palettetema/WarnaAktif.kt β dynamic color schemeres/values/strings.xml β 143 strings (English default)res/values-in/strings.xml β 143 strings (Indonesian)ManajerBahasa.kt β AppCompatDelegate.setApplicationLocalesCek GitHub releases latest, banding versi (tuple angka [major, minor, patch]), unduh APK, buka installer via FileProvider.
versiNama dibandingkan sebagai tuple, BUKAN versiKode. Contoh: 1.9.0 β [1, 9, 0].
1. rilisTerbaru() β GET api.github.com/repos/djvpri/ZX-Parenting-agent/releases/latest
2. Banding tag vs BuildConfig.VERSI_NAMA
3. Cari asset .apk di release
4. unduh(url, ctx) β simpan ke getExternalFilesDir(null)/updates/zx-agent-update.apk
5. Buka installer via FileProvider.getUriForFile() + ACTION_INSTALL_PACKAGE
Device admin receiver β force-lock policy. Memungkinkan lockNow() untuk kunci layar saat sesi habis.
<uses-policies>
<force-lock />
</uses-policies><uses-policies>
<force-lock />
</uses-policies>
## Simpanan.kt (64 lines) SharedPreferences (`zx_agent`): - `token` β device pairing token (x-device-token) - `terakhir_ping` β timestamp ping terakhir - `jwt_anak` β JWT bearer anak (Authorization) - `nama_anak`, `anak_id`, `umur_anak` β profil anak Methods: `token()`, `simpanToken()`, `hapusToken()`, `catatPing()`, `pingTerakhir()`, `jwt()`, `simpanAnak()`, `namaAnak()`, `anakId()`, `umurAnak()`, `hapusAnak()`
SharedPreferences (zx_agent):
token β device pairing token (x-device-token)terakhir_ping β timestamp ping terakhirjwt_anak β JWT bearer anak (Authorization)nama_anak, anak_id, umur_anak β profil anakMethods: token(), simpanToken(), hapusToken(), catatPing(), pingTerakhir(), jwt(), simpanAnak(), namaAnak(), anakId(), umurAnak(), hapusAnak()
assembleDebug β build check, no artifactversi (mis. 2.5.0)versiKode (+1) di alamat.jsongit tag -f v{VERSI} + push --force (re-rilis OK)App butuh pairing device ortu untuk full functionality.
Demo mode tersedia: buka app β tombol "Mode Demo" di layar login.
Demo menampilkan sample kuis, tugas, hadiah, dan UI tanpa device ortu.
git tag -f + push --force| Method | Endpoint | Body | Response |
|--------|----------|------|----------|
| POST | /api/auth/native-anak | {username, pin} | {token: JWT, anak: {id, nama, umur}} |
| Method | Endpoint | Response |
|--------|----------|----------|
| GET | /api/anak/official | {items: [{id, tipe, judul, officialMeta: {tokenReward, levelKesulitan, views}, selesai}], total} |
| GET | /api/anak/official/{id} | detail kuis + soal |
| POST | /api/anak/official/{id}/complete | skor + token reward |
| POST | /api/anak/kuis/start | {attemptId, soal: [{q, options}]} |
| GET | /api/anak/tugas | [{id, judul, tokenReward, status, deadline}] |
| GET | /api/anak/saldo | {saldo: int} |
| GET | /api/anak/streak | {current, terbaik, terakhir} |
| GET | /api/anak/challenge | {topik, tokenReward, selesai} |
| Method | Endpoint | Body |
|--------|----------|------|
| POST | /api/agent | {aksi: "ping", "activity", "lapor-error", "lokasi"} |
| Method | Endpoint | Response |
|--------|----------|----------|
| GET | /api/diagnostik?token= | health snapshot JSON |
| GET | /api/error-log?token= | error log JSON |
| # | Fitur | Web File | API Endpoint | App Android |
|---|-------|----------|--------------|-------------|
| 1 | Tukar Token β Waktu Main | TukarToken.tsx | POST /api/anak/sesi-main | β belum |
| 2 | Katalog Hadiah (minta dari ortu) | KatalogHadiah.tsx | GET/POST /api/anak/hadiah | β belum |
| 3 | Quest Keluarga (status quest) | QuestAnak.tsx | GET /api/quests-saya | β belum |
| 4 | Challenge Harian (progress) | ChallengeKartu.tsx | challenge API | β belum |
| 5 | Pinjam Waktu (token darurat) | blueprint ekonomi-token | GET /api/pinjam | β belum |
| 6 | Riwayat Token (ledger mutasi) | blueprint ekonomi-token | GET /api/token-ledger | β belum |
| # | Fitur | App File | Status |
|---|-------|----------|--------|
| 1 | Login anak (PIN) | LayarMasuk.kt | β
|
| 2 | Feed official content | LayarBeranda.kt | β
|
| 3 | Kuis AI (Gemini) | LayarKuisAI.kt | β
|
| 4 | Detail kuis official | LayarKuisDetail.kt | β
|
| 5 | Sesi kuis tugas | LayarKuisSession.kt | β
|
| 6 | Daftar tugas | LayarTugas.kt | β
|
| 7 | Profil + bahasa | LayarSaya.kt | β
|
| 8 | Ping loop + blokir + GPS | AgentService.kt | β
|
| 9 | Auto-update APK | PemeriksaPembaruan.kt | β
|
Beranda / Kuis AI / Tugas / Saya
Gabung Tukar Token + Katalog Hadiah + Quest + Pinjam Waktu + Riwayat Token. Semua API backend sudah live.
Anak tukar token (1 token = 1 menit main) β waktu main aktif. Saldo berkurang, sesi main bertambah.
/api/anak/sesi-main {menit: N}{menit, saldo} β sukses pesan + saldo baruPOST /api/anak/sesi-main
Header: Authorization: Bearer <jwt>
Body: {menit: 15}
Response: {menit: 15, saldo: 305}
Error: {error: "Saldo tidak cukup"}POST /api/anak/sesi-main
Header: Authorization: Bearer <jwt>
Body: {menit: 15}
Response: {menit: 15, saldo: 305}
Error: {error: "Saldo tidak cukup"}
## Implementasi app Android
// Api.kt β tambah endpoint
@POST("anak/sesi-main")
suspend fun tukarToken(
@Header("Authorization") bearer: String,
@Body req: TukarTokenReq
): Response<TukarTokenRes>
data class TukarTokenReq(val menit: Int)
data class TukarTokenRes(val menit: Int, val saldo: Int)// Api.kt β tambah endpoint
@POST("anak/sesi-main")
suspend fun tukarToken(
@Header("Authorization") bearer: String,
@Body req: TukarTokenReq
): Response<TukarTokenRes>
data class TukarTokenReq(val menit: Int)
data class TukarTokenRes(val menit: Int, val saldo: Int)
## UI: di dalam tab "Hadiah" - Card "Tukar Token β Waktu Main" - Dropdown pilihan menit - Tombol "Tukar" (hijau) - Pesan sukses/error - Minimal 5 token untuk tukar
Ortu buat hadiah (mainan, jajanan, dll) dengan harga coin. Anak lihat katalog β klik "Minta" β ortu approve β dibelikan.
/api/anak/hadiah β daftar hadiah {id, judul, deskripsi, hargaCoin, stok, sudahMinta}/api/anak/hadiah {hadiahId}GET /api/anak/hadiah
Header: Authorization: Bearer <jwt>
Response: {katalog: [{id, judul, deskripsi, hargaCoin, stok, sudahMinta}]}
POST /api/anak/hadiah
Body: {hadiahId: "xxx"}
Response: {ok: true}
Error: {error: "Sudah diminta" | "Stok habis"}GET /api/anak/hadiah
Header: Authorization: Bearer <jwt>
Response: {katalog: [{id, judul, deskripsi, hargaCoin, stok, sudahMinta}]}
POST /api/anak/hadiah
Body: {hadiahId: "xxx"}
Response: {ok: true}
Error: {error: "Sudah diminta" | "Stok habis"}
## Implementasi app Android
@GET("anak/hadiah")
suspend fun hadiahList(@Header("Authorization") bearer: String): Response<HadiahListRes>
@POST("anak/hadiah")
suspend fun hadiahMinta(
@Header("Authorization") bearer: String,
@Body req: HadiahMintaReq
): Response<HadiahMintaRes>
data class HadiahItem(
val id: String, val judul: String,
val deskripsi: String?, val hargaCoin: Int,
val stok: Int, val sudahMinta: Boolean
)
data class HadiahListRes(val katalog: List<HadiahItem>)
data class HadiahMintaReq(val hadiahId: String)@GET("anak/hadiah")
suspend fun hadiahList(@Header("Authorization") bearer: String): Response<HadiahListRes>
@POST("anak/hadiah")
suspend fun hadiahMinta(
@Header("Authorization") bearer: String,
@Body req: HadiahMintaReq
): Response<HadiahMintaRes>
data class HadiahItem(
val id: String, val judul: String,
val deskripsi: String?, val hargaCoin: Int,
val stok: Int, val sudahMinta: Boolean
)
data class HadiahListRes(val katalog: List<HadiahItem>)
data class HadiahMintaReq(val hadiahId: String)
## UI: di dalam tab "Hadiah" - Section "Katalog Hadiah" - LazyColumn kartu hadiah - Badge harga coin + status stok - Tombol "Minta" per item
Ortu buat quest keluarga (mis: "Bantu Mama bersih-bersih"). Anak lihat status quest β ortu tandai selesai β anak dapat token reward.
/api/quests-saya β daftar quest{id, judul, deskripsi, tokenReward, deadline, anggota: [{anakId, selesai}]}GET /api/quests-saya
Header: Authorization: Bearer <jwt>
Response: [{id, judul, deskripsi, tokenReward, deadline, anggota: [{anakId, selesai}]}]GET /api/quests-saya
Header: Authorization: Bearer <jwt>
Response: [{id, judul, deskripsi, tokenReward, deadline, anggota: [{anakId, selesai}]}]
## Implementasi app Android
@GET("quests-saya")
suspend fun questSaya(@Header("Authorization") bearer: String): Response<List<QuestAnak>>
data class QuestAnak(
val id: String, val judul: String,
val deskripsi: String?, val tokenReward: Int,
val deadline: String?,
val anggota: List<QuestMember>
)
data class QuestMember(val anakId: String, val selesai: Boolean)@GET("quests-saya")
suspend fun questSaya(@Header("Authorization") bearer: String): Response<List<QuestAnak>>
data class QuestAnak(
val id: String, val judul: String,
val deskripsi: String?, val tokenReward: Int,
val deadline: String?,
val anggota: List<QuestMember>
)
data class QuestMember(val anakId: String, val selesai: Boolean)
## UI: di dalam tab "Hadiah" atau "Beranda" - Section "Quest Keluarga" - Card per quest: judul + token reward + deadline + status - Hijau jika selesai, putih jika menunggu - Pesan: "Ortu akan menandai selesai setelah dicek"
Challenge harian per anak β target aktivitas (mis: "Kerjakan 3 kuis hari ini"). Progress tracking, reward token saat complete.
ChallengeProgress β challenge harian per anakChallengeProgress β challenge harian per anak. Terhubung dengan OfficialContent (TANTANGAN_MINGGUAN).
GET /api/anak/challenge
Header: Authorization: Bearer <jwt>
Response: [{id, judul, target, progress, tokenReward, selesai}]GET /api/anak/challenge
Header: Authorization: Bearer <jwt>
Response: [{id, judul, target, progress, tokenReward, selesai}]
## Implementasi app Android
@GET("anak/challenge")
suspend fun challengeAktif(@Header("Authorization") bearer: String): Response<List<ChallengeItem>>
data class ChallengeItem(
val id: String, val judul: String,
val target: Int, val progress: Int,
val tokenReward: Int, val selesai: Boolean
)@GET("anak/challenge")
suspend fun challengeAktif(@Header("Authorization") bearer: String): Response<List<ChallengeItem>>
data class ChallengeItem(
val id: String, val judul: String,
val target: Int, val progress: Int,
val tokenReward: Int, val selesai: Boolean
)
## UI: di tab "Beranda" (card di atas feed) - Progress bar: progress/target - Badge token reward - Status: aktif / selesai
Anak kehabisan token β pinjam waktu darurat. Hutang token harus dibayar (dari reward berikutnya). Ortu lihat hutang anak.
PINJAM β pinjam waktu daruratBAYAR_PINJAM β pembayaran pinjamanLayarPinjam.kt β anak pinjam token, bayar hutangGET /api/pinjam?anakId=X
Header: Authorization: Bearer <jwt>
Response: {hutang: 10, riwayat: [{id, jumlah, status, createdAt}]}
POST /api/pinjam (estimasi)
Body: {jumlah: 15}
Response: {ok: true, hutang: 15}GET /api/pinjam?anakId=X
Header: Authorization: Bearer <jwt>
Response: {hutang: 10, riwayat: [{id, jumlah, status, createdAt}]}
POST /api/pinjam (estimasi)
Body: {jumlah: 15}
Response: {ok: true, hutang: 15}
## Implementasi app Android
@GET("pinjam")
suspend fun pinjamList(@Header("Authorization") bearer: String): Response<PinjamRes>
data class PinjamRes(val hutang: Int, val riwayat: List<PinjamItem>)
data class PinjamItem(val id: String, val jumlah: Int, val status: String, val createdAt: String)@GET("pinjam")
suspend fun pinjamList(@Header("Authorization") bearer: String): Response<PinjamRes>
data class PinjamRes(val hutang: Int, val riwayat: List<PinjamItem>)
data class PinjamItem(val id: String, val jumlah: Int, val status: String, val createdAt: String)
## UI: di dalam tab "Hadiah" - Card "Pinjam Waktu Darurat" - Tampilkan hutang aktif - Tombol "Pinjam" (hanya jika tidak ada hutang aktif) - Riwayat pinjam/bayar
Anak lihat riwayat mutasi token: dari mana (kuis, tugas, challenge), untuk apa (tukar main, pinjam). Transparansi ekonomi token.
TokenLedger β mutasi: tipe, jumlah (+/-), catatan, refIdGET /api/token-ledger?anakId=X
Header: Authorization: Bearer <jwt>
Response: [{id, tipe, jumlah, catatan, refId, createdAt}]GET /api/token-ledger?anakId=X
Header: Authorization: Bearer <jwt>
Response: [{id, tipe, jumlah, catatan, refId, createdAt}]
## Implementasi app Android
@GET("token-ledger")
suspend fun tokenLedger(@Header("Authorization") bearer: String): Response<List<LedgerItem>>
data class LedgerItem(
val id: String, val tipe: String,
val jumlah: Int, val catatan: String?,
val refId: String?, val createdAt: String
)@GET("token-ledger")
suspend fun tokenLedger(@Header("Authorization") bearer: String): Response<List<LedgerItem>>
data class LedgerItem(
val id: String, val tipe: String,
val jumlah: Int, val catatan: String?,
val refId: String?, val createdAt: String
)
## UI: di dalam tab "Hadiah" - Section "Riwayat Token" - LazyColumn: item per mutasi - Hijau (+) untuk masuk, merah (-) untuk keluar - Badge tipe (KUIS, TUGAS, PEMAKAIAN, dll)
1. Beranda β feed official + challenge harian
2. Kuis AI β buat kuis AI
3. Tugas β daftar tugas
4. Hadiah (BARU) β tukar token + katalog + quest + pinjam + riwayat
5. Saya β profil + bahasa + device admin
βββββββββββββββββββββββββββββββ β Saldo Token: 320 β β header card (hijau) β = 320 menit waktu main β β Streak: 7 hari π₯ β βββββββββββββββββββββββββββββββ€ β Tukar Token β Waktu Main β β card tukar β [15 menit βΎ] [Tukar] β βββββββββββββββββββββββββββββββ€ β Pinjam Waktu Darurat β β card pinjam β Hutang: 0 token β β [Pinjam 15 menit] β βββββββββββββββββββββββββββββββ€ β Quest Keluarga β β section quest β βΈ Bantu Mama bersih... β³ β β βΈ Baca buku 30 menit β β βββββββββββββββββββββββββββββββ€ β Katalog Hadiah β β section hadiah β βΈ Mainan Lego (50 coin) β β [Minta] β β βΈ Es Krim (20 coin) β β [Sudah diminta] β βββββββββββββββββββββββββββββββ€ β Riwayat Token β β section ledger β +15 KUIS | -30 PEMAKAIAN β β +50 CHALLENGE | +25 TUGAS β βββββββββββββββββββββββββββββββ
βββββββββββββββββββββββββββββββ
β Saldo Token: 320 β β header card (hijau)
β = 320 menit waktu main β
β Streak: 7 hari π₯ β
βββββββββββββββββββββββββββββββ€
β Tukar Token β Waktu Main β β card tukar
β [15 menit βΎ] [Tukar] β
βββββββββββββββββββββββββββββββ€
β Pinjam Waktu Darurat β β card pinjam
β Hutang: 0 token β
β [Pinjam 15 menit] β
βββββββββββββββββββββββββββββββ€
β Quest Keluarga β β section quest
β βΈ Bantu Mama bersih... β³ β
β βΈ Baca buku 30 menit β β
βββββββββββββββββββββββββββββββ€
β Katalog Hadiah β β section hadiah
β βΈ Mainan Lego (50 coin) β
β [Minta] β
β βΈ Es Krim (20 coin) β
β [Sudah diminta] β
βββββββββββββββββββββββββββββββ€
β Riwayat Token β β section ledger
β +15 KUIS | -30 PEMAKAIAN β
β +50 CHALLENGE | +25 TUGAS β
βββββββββββββββββββββββββββββββ
## NavItem enum update
enum class NavItem(@StringRes labelRes: Int, icon: ImageVector) {
Beranda(R.string.nav_home, Icons.Default.Home),
Kuis(R.string.quiz_ai, Icons.Default.AutoAwesome),
Tugas(R.string.task_title, Icons.Default.Task),
Hadiah(R.string.nav_reward, Icons.Default.CardGiftcard),
Saya(R.string.nav_profile, Icons.Default.Person),
}enum class NavItem(@StringRes labelRes: Int, icon: ImageVector) {
Beranda(R.string.nav_home, Icons.Default.Home),
Kuis(R.string.quiz_ai, Icons.Default.AutoAwesome),
Tugas(R.string.task_title, Icons.Default.Task),
Hadiah(R.string.nav_reward, Icons.Default.CardGiftcard),
Saya(R.string.nav_profile, Icons.Default.Person),
}
## File baru - `LayarHadiah.kt` β tab Hadiah (scrollable column: saldo + tukar + pinjam + quest + katalog + riwayat) - DTO di `Api.kt`: TukarTokenReq/Res, HadiahItem/ListRes/MintaReq, QuestAnak/Member, PinjamRes/Item, LedgerItem ## API endpoints baru ( Retrofit) - `POST anak/sesi-main` β tukar token - `GET anak/hadiah` β katalog hadiah - `POST anak/hadiah` β minta hadiah - `GET quests-saya` β quest keluarga - `GET pinjam` β riwayat pinjam - `GET token-ledger` β riwayat token ## Strings baru (EN + ID) - `nav_reward` = "Hadiah" / "Rewards" - `token_balance` = "Saldo Token" / "Token Balance" - `exchange_token` = "Tukar Token" / "Exchange Token" - `borrow_time` = "Pinjam Waktu" / "Borrow Time" - `family_quest` = "Quest Keluarga" / "Family Quest" - `gift_catalog` = "Katalog Hadiah" / "Gift Catalog" - `token_history` = "Riwayat Token" / "Token History"
LayarHadiah.kt β tab Hadiah (scrollable column: saldo + tukar + pinjam + quest + katalog + riwayat)Api.kt: TukarTokenReq/Res, HadiahItem/ListRes/MintaReq, QuestAnak/Member, PinjamRes/Item, LedgerItemPOST anak/sesi-main β tukar tokenGET anak/hadiah β katalog hadiahPOST anak/hadiah β minta hadiahGET quests-saya β quest keluargaGET pinjam β riwayat pinjamGET token-ledger β riwayat tokennav_reward = "Hadiah" / "Rewards"token_balance = "Saldo Token" / "Token Balance"exchange_token = "Tukar Token" / "Exchange Token"borrow_time = "Pinjam Waktu" / "Borrow Time"family_quest = "Quest Keluarga" / "Family Quest"gift_catalog = "Katalog Hadiah" / "Gift Catalog"token_history = "Riwayat Token" / "Token History"App ortu = 20 layar, 5 tab + 14 sub-layar, ~40 API endpoints (v2.9.7).
App anak = 16 layar, 4 tab (v1.9.0).
| Fitur | App Ortu (parent side) | App Anak (child side) | API | Status |
|-------|------------------------|----------------------|-----|--------|
| Tugas | LayarTugas (tab 2) β buat tugas, validasi selesai/tolak | LayarTugas.kt β daftar tugas, kerjakan kuis | GET/POST/PUT /api/tugas | β
nyambung |
| Aturan Device | LayarAturan (sub 1) β sessionLimit, cooldown, curfew, appBlokir | AgentService.kt β terima & terapkan aturan dari backend | GET/PATCH /api/kebijakan | β
nyambung |
| Lokasi GPS | LayarLokasi (sub 2) β GPS toggle, geo-fence, lastSeen | Lokasi.kt β kirim GPS ke backend | POST /api/lokasi | β
nyambung |
| Laporan Aktivitas | LayarLaporan (sub 3) β activity log per anak, ringkasan 7 hari | Pemakaian.kt β kirim usage stats ke backend | POST /api/pemakaian | β
nyambung |
| Blokir App | LayarAturan β appBlokir list | Blokir.kt β enforce blokir pakai UsageStatsManager | GET /api/blokir | β
nyambung |
| Ping/Heartbeat | (tidak ada UI β ortu lihat device online status) | AgentService.kt β ping loop 5 menit | POST /api/agent/ping | β
nyambung |
| Auto-update | (tidak relevan ortu) | PemeriksaPembaruan.kt β cek GitHub release | GitHub Releases API | β
jalan |
| # | Fitur | App Ortu (parent side) | App Anak (child side) | API | Status |
|---|-------|------------------------|----------------------|-----|--------|
| 1 | Katalog Hadiah | LayarMarketplace (tab 3) β CRUD hadiah, proses pesanan beli/tolak, saldo coin | BELUM β anak belum bisa lihat katalog/minta hadiah | GET/POST /api/anak/hadiah | β putus |
| 2 | Quest Keluarga | LayarQuest (sub 5) β buat quest, tandai selesai β anak dapat token | BELUM β anak belum bisa lihat status quest | GET /api/quests-saya | β putus |
| 3 | Challenge Harian | LayarChallenges (sub 13) β daily challenges + claim | BELUM β anak belum bisa lihat progress | GET /api/anak/challenge | β putus |
| 4 | Pinjam Waktu | LayarPinjam (sub 10) β lihat hutang anak | BELUM β anak belum bisa pinjam dari app | GET/POST /api/pinjam | β putus |
| 5 | Riwayat Token | LayarCoin (sub 4) β saldo ZX Coin, ledger, topup/penyesuaian | BELUM β anak belum bisa lihat mutasi token | GET /api/token-ledger | β putus |
| 6 | Tukar Token β Waktu Main | (ortu tidak tukar β ortu topup) | BELUM β anak belum bisa tukar token β sesi main | POST /api/anak/sesi-main | β putus |
LayarInsights (sub 9) β AI insights per anak (Gemini/rule-based)LayarForum (sub 6) β forum posts, detail + komentarLayarLangganan (sub 7) β tier, checkout MidtransLayarPesan (sub 8) β direct chat (anak belum butuh)LayarReferral (sub 11) β kode referralLayarLeaderboard (sub 12) β ranking token mingguanLayarAturan β masterUnlock, device admin activation| Endpoint | Method | App File | Status |
|----------|--------|----------|--------|
| /api/agent/ping | POST | AgentService.kt | β
|
| /api/lokasi | POST | Lokasi.kt | β
|
| /api/kebijakan | GET | Kebijakan.kt | β
|
| /api/blokir | GET | Blokir.kt | β
|
| /api/pemakaian | POST | Pemakaian.kt | β
|
| /api/agent/edu (kuis) | GET | LayarKuisAI.kt | β
|
| /api/agent/edu (tugas) | GET | LayarTugas.kt | β
|
| /api/agent/edu/kuis/detail | GET | LayarKuisDetail.kt | β
|
| /api/agent/edu/kuis/sesi | POST | LayarKuisSession.kt | β
|
| Endpoint | Method | Untuk Fitur | Prioritas |
|----------|--------|-------------|-----------|
| /api/anak/sesi-main | POST | Tukar Token β Waktu Main | TINGGI |
| /api/anak/hadiah | GET | Katalog Hadiah (list) | TINGGI |
| /api/anak/hadiah | POST | Katalog Hadiah (minta) | TINGGI |
| /api/quests-saya | GET | Quest Keluarga (status) | TINGGI |
| /api/anak/challenge | GET | Challenge Harian (progress) | SEDANG |
| /api/pinjam | GET | Pinjam Waktu (riwayat) | SEDANG |
| /api/pinjam | POST | Pinjam Waktu (ajukan) | SEDANG |
| /api/token-ledger | GET | Riwayat Token (mutasi) | SEDANG |
| /api/anak/saldo | GET | Saldo token real-time | TINGGI |
~30 endpoint: auth ortu, CRUD anak, CRUD tugas, device control, marketplace CRUD, quest CRUD, forum, pesan, insights, langganan, referral, leaderboard, challenges CRUD
/api/agent/loginAuthorization: Bearer <jwt> untuk semua endpoint anakZxVm (757 lines), single UiState data class (20+ fields)MutableStateFlow<UiState>, collectAsState()ApiZx interface (~40 endpoints)mutableIntStateOf (tab + subLayar), no Navigation Composeremember + mutableStateOf per layarApiAgent (HttpURLConnection) + Api (Retrofit)mutableIntStateOf (tab)App ortu lebih mature:
muatSemua() β 8 parallel fetchApp anak procedural:
LayarHadiah.kt dengan pattern yang konsisten dengan LayarBeranda.kt| Fitur | Effort | API ready | Dampak |
|-------|--------|-----------|--------|
| Tukar Token β Waktu Main | Kecil | β
POST /api/anak/sesi-main | Anak bisa pakai token |
| Katalog Hadiah (lihat + minta) | Kecil | β
GET/POST /api/anak/hadiah | Loop ortuβanak jalan |
| Quest Keluarga (lihat status) | Kecil | β
GET /api/quests-saya | Anak tahu target |
| Saldo token real-time | Kecil | β
est. GET /api/anak/saldo | Transparansi |
| Fitur | Effort | API ready | Dampak |
|-------|--------|-----------|--------|
| Challenge Harian | Sedang | est. GET /api/anak/challenge | Gamifikasi |
| Riwayat Token (ledger) | Kecil | est. GET /api/token-ledger | Transparansi mutasi |
| Fitur | Effort | API ready | Dampak |
|-------|--------|-----------|--------|
| Pinjam Waktu | Sedang | est. GET/POST /api/pinjam | Token darurat |
LayarHadiah.kt + DTO di Api.kt)LayarHadiah.kt)1. Bump targetSdk 35β36 (wajib Play Console)
2. Tambah LayarHadiah.kt (Prioritas 1)
3. Tambah challenge + ledger (Prioritas 2)
4. Tambah pinjam (Prioritas 3)
5. Update bottom nav 4β5 tab
6. Update strings.xml (EN + ID)
7. Build AAB, release v2.0.0
Saat Kebijakan.evaluasi() return JAM_TIDUR / DAILY_HABIS / COOLDOWN:
AgentService.proses() β Blokir.nyalakan() β start BlokirActivityFLAG_SHOW_WHEN_LOCKED + FLAG_TURN_SCREEN_ON + FLAG_KEEP_SCREEN_ONmasterUnlockminta_bukaFile: BlokirActivity.kt (90 lines), AgentService.kt baris 65-78, Kebijakan.kt evaluasi()
Accessibility service deteksi TYPE_WINDOW_STATE_CHANGED:
appBlokir (dari server) β overlay BlokirActivity menutupi appFile: AksesService.kt (50 lines), Blokir.kt simpanDaftarApp() / daftarApp()
device_admin.xml: <force-lock /> policy dideklarasi β
StatusActivity.kt: ACTION_ADD_DEVICE_ADMIN β ortu/teknisi aktifkan sekali saat pairingdpm.removeActiveAdmin() + clear tokenFile: AdminReceiver.kt, StatusActivity.kt baris 103-107, device_admin.xml
device_admin.xml: <force-lock /> β
policy ada
AdminReceiver.kt: "lockNow() + anti-uninstall" β hanya komentar
StatusActivity.kt: dpm.isAdminActive() β
cek status
dpm.removeActiveAdmin() β
lepas saat disconnect
dpm.lockNow() β TIDAK ADA β tidak pernah dipanggildevice_admin.xml: <force-lock /> β policy ada
AdminReceiver.kt: "lockNow() + anti-uninstall" β hanya komentar
StatusActivity.kt: dpm.isAdminActive() β cek status
dpm.removeActiveAdmin() β lepas saat disconnect
dpm.lockNow() β TIDAK ADA β tidak pernah dipanggil
## Kelemahan overlay vs lockNow | Aspek | Overlay (sekarang) | `lockNow()` (belum) | |-------|--------------------|---------------------| | Back button | Diblok di kode, bisa di-bypass (notification bar, recent apps) | HP benar-benar terkunci, butuh PIN/pattern | | Force stop service | Anak bisa kill app dari Settings β overlay hilang | Device Admin survive kill (system level) | | Uninstall app | Diblok (Device Admin aktif) | Sama β anti-uninstall jalan | | Notification bar | Bisa di-tarik dari atas | Tidak bisa | | Recent apps | Bisa switch ke app lain | Layar terkunci penuh | | Lock screen | Overlay di atas app, bukan lock screen system | Lock screen system Android | ## Konfigurasi ortu yang trigger blokir Ortu set di `LayarAturan` (app ortu sub-layar 1): - `sessionLimit` β menit per sesi (default 120) - `cooldown` β menit jeda antar sesi (default 30) - `dailyLimit` β menit total per hari (default 180) - `curfewMulai` / `curfewSelesai` β jam tidur (mis. "21:00"-"05:00") - `masterUnlock` β ortu override (buka paksa) - `appBlokir` β daftar paket diblokir per-app Backend β `GET /api/agent/ping` response β `aturan` object β `AgentService.proses()` β `Kebijakan.evaluasi()`
| Aspek | Overlay (sekarang) | lockNow() (belum) |
|-------|--------------------|---------------------|
| Back button | Diblok di kode, bisa di-bypass (notification bar, recent apps) | HP benar-benar terkunci, butuh PIN/pattern |
| Force stop service | Anak bisa kill app dari Settings β overlay hilang | Device Admin survive kill (system level) |
| Uninstall app | Diblok (Device Admin aktif) | Sama β anti-uninstall jalan |
| Notification bar | Bisa di-tarik dari atas | Tidak bisa |
| Recent apps | Bisa switch ke app lain | Layar terkunci penuh |
| Lock screen | Overlay di atas app, bukan lock screen system | Lock screen system Android |
Ortu set di LayarAturan (app ortu sub-layar 1):
sessionLimit β menit per sesi (default 120)cooldown β menit jeda antar sesi (default 30)dailyLimit β menit total per hari (default 180)curfewMulai / curfewSelesai β jam tidur (mis. "21:00"-"05:00")masterUnlock β ortu override (buka paksa)appBlokir β daftar paket diblokir per-appBackend β GET /api/agent/ping response β aturan object β AgentService.proses() β Kebijakan.evaluasi()
Device Admin + <force-lock /> sudah siap. Tinggal panggil.
// Tambah import
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
// Tambah property di class AgentService
private val dpm get() = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
private val adminComp get() = ComponentName(this, AdminReceiver::class.java)
private val adminAktif get() = dpm.isAdminActive(adminComp)
// Di proses(), setelah Blokir.nyalakan(this, blokir):
if (blokir != null && !sedangBlokir) {
Blokir.nyalakan(this, blokir)
// Kunci layar system β tidak bisa di-bypass notification bar / recent apps
if (adminAktif) {
dpm.lockNow()
}
startActivity(
Intent(this, BlokirActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
)
}// Tambah import
import android.app.admin.DevicePolicyManager
import android.content.ComponentName
// Tambah property di class AgentService
private val dpm get() = getSystemService(Context.DEVICE_POLICY_SERVICE) as DevicePolicyManager
private val adminComp get() = ComponentName(this, AdminReceiver::class.java)
private val adminAktif get() = dpm.isAdminActive(adminComp)
// Di proses(), setelah Blokir.nyalakan(this, blokir):
if (blokir != null && !sedangBlokir) {
Blokir.nyalakan(this, blokir)
// Kunci layar system β tidak bisa di-bypass notification bar / recent apps
if (adminAktif) {
dpm.lockNow()
}
startActivity(
Intent(this, BlokirActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)
)
}
### Kenapa tetap pakai BlokirActivity + lockNow? 1. `lockNow()` kunci layar system β HP benar terkunci, butuh PIN/pattern 2. `BlokirActivity` tetap tampil di lock screen (`FLAG_SHOW_WHEN_LOCKED`) β anak lihat pesan "HP terkunci sampai jam tidur berakhir" 3. Saat anak buka HP (unlock PIN), `BlokirActivity` masih ada β tetap blokir 4. Double layer: system lock + app overlay ### Anti-bypass tambahan (opsional, fase lanjut)
1. lockNow() kunci layar system β HP benar terkunci, butuh PIN/pattern
2. BlokirActivity tetap tampil di lock screen (FLAG_SHOW_WHEN_LOCKED) β anak lihat pesan "HP terkunci sampai jam tidur berakhir"
3. Saat anak buka HP (unlock PIN), BlokirActivity masih ada β tetap blokir
4. Double layer: system lock + app overlay
// 1. Sembunyikan notification bar β BlokirActivity sudah fullscreen
// tambah di onCreate:
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
// 2. Block recent apps β override onPause
override fun onPause() {
super.onPause()
// bila masih blokir, restart activity
if (Blokir.aktif(this)) {
startActivity(Intent(this, BlokirActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP))
}
}
// 3. Device Admin anti-uninstall sudah jalan β tidak bisa uninstall tanpa deactivate// 1. Sembunyikan notification bar β BlokirActivity sudah fullscreen
// tambah di onCreate:
window.setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,
WindowManager.LayoutParams.FLAG_FULLSCREEN
)
// 2. Block recent apps β override onPause
override fun onPause() {
super.onPause()
// bila masih blokir, restart activity
if (Blokir.aktif(this)) {
startActivity(Intent(this, BlokirActivity::class.java)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP))
}
}
// 3. Device Admin anti-uninstall sudah jalan β tidak bisa uninstall tanpa deactivate
### Limitasi `lockNow()` di Android modern - Android 9+ (API 28+): `lockNow()` tetap jalan dengan Device Admin β - Android 14+ (API 34): Device Admin `force-lock` tidak deprecated β - Tidak butuh additional permission β `<force-lock />` policy sudah dideklarasi - Tidak konflik dengan accessibility service β keduanya independent ## Tidak butuh dependency baru - `DevicePolicyManager` = Android framework API - `ComponentName` = Android framework API - Tidak ada library tambahan
lockNow() tetap jalan dengan Device Admin β
force-lock tidak deprecated β
<force-lock /> policy sudah dideklarasiDevicePolicyManager = Android framework APIComponentName = Android framework API1. Bump targetSdk 35β36 (wajib new apps Okt 2026)
2. Build AAB, release v2.0.0-beta1
3. Tambah dpm.lockNow() di AgentService.proses() saat blokir
4. Test: set curfew dari app ortu β HP anak harus terkunci
5. Commit: feat: lockNow() saat blokir sesi/curfew
6. Buat LayarHadiah.kt β saldo + tukar token + katalog hadiah + quest
7. Tambah DTO di Api.kt: TukarTokenReq/Res, HadiahItem, QuestAnak
8. Tambah endpoint Retrofit: anak/sesi-main, anak/hadiah, quests-saya
9. Update bottom nav 4β5 tab
10. Update strings.xml (EN + ID): nav_reward, token_balance, exchange_token
11. Commit: feat: tab Hadiah β tukar token + katalog + quest
12. Tambah challenge harian (section di Beranda)
13. Tambah riwayat token (section di Hadiah)
14. Tambah pinjam waktu (section di Hadiah)
15. Commit: feat: challenge + ledger + pinjam waktu
16. Build AAB v2.0.0
17. Release ke GitHub + Play Console
18. Store listing app anak
| Fase | Lines | File baru | Dependency |
|------|-------|-----------|------------|
| 1 β targetSdk 36 | ~5 (build.gradle) | 0 | 0 |
| 2 β lockNow | ~10 | 0 | 0 |
| 3 β Tab Hadiah | ~250-300 | 1 (LayarHadiah.kt) | 0 |
| 4 β Engagement | ~150 | 0 | 0 |
| Total | ~415-465 | 1 | 0 |
| Endpoint | Method | Auth | Response Shape | Status |
|----------|--------|------|----------------|--------|
| /api/anak/sesi-main | POST | sesiAnak β
| {ok, menit, saldo} | β
READY |
| /api/anak/hadiah | GET | sesiAnak β
| {katalog: [{id, judul, deskripsi, hargaCoin, stok, sudahMinta}]} | β
READY |
| /api/anak/hadiah | POST | sesiAnak β
| {ok} (201) | β
READY |
| /api/quests-saya | GET | sesiAnak β
| [{id, judul, deskripsi, tokenReward, deadline, anggota:[{anakId, selesai}]}] | β
READY |
| /api/anak/challenge | GET | sesiAnak β
| {id, judul, topik, tokenReward, selesai} | β
READY |
| /api/anak/streak | GET | sesiAnak β
| {current, terbaik, terakhir} | β
READY (bonus) |
| Endpoint | Method | Auth | Kenapa | Status |
|----------|--------|------|--------|--------|
| /api/pinjam | GET | sesiOrtu β | Cek ortuId match, bukan anak login | β BLOCKED |
| /api/pinjam | POST | sesiOrtu β | Config pinjam (limitPinjam, bunga) β ortu set | β BLOCKED |
| /api/token-ledger | GET | sesiOrtu β | Cek ortuId match, butuh anakId param | β BLOCKED |
| /api/coin | GET | sesiOrtu β | Saldo ZX Coin ORTU (bukan token anak) | β BLOCKED |
| Endpoint | Status | Catatan |
|----------|--------|---------|
| /api/anak/saldo | β TIDAK ADA | Saldo anak hanya dapat dari response sesi-main (setelah tukar) atau token-ledger (ortu-only) |
| Child-side pinjam | β TIDAK ADA | Pinjam hanya ortu-side: lihat hutang + set config. Anak belum bisa ajukan pinjam |
| Child-side token-ledger | β TIDAK ADA | Ledger hanya ortu-side. Anak belum bisa lihat riwayat sendiri |
AKTUAL: {id, judul, topik, tokenReward, selesai}
target / progress fieldtopik (string, mis. "Matematika perkalian")selesai = boolean (dari ChallengeProgress)data class ChallengeItem(
val id: String,
val judul: String,
val topik: String, // BUKAN target: Int
val tokenReward: Int,
val selesai: Boolean
// TIDAK ADA progress: Int
)data class ChallengeItem(
val id: String,
val judul: String,
val topik: String, // BUKAN target: Int
val tokenReward: Int,
val selesai: Boolean
// TIDAK ADA progress: Int
)
### Pinjam β BP bilang anak bisa `GET/POST /api/pinjam`
**AKTUAL:** pinjam = ortu-only. POST = config (limitPinjam, bunga), bukan ajukan pinjam.
Anak TIDAK BISA pinjam dari app. Butuh endpoint baru.
### Token-ledger β BP bilang anak bisa `GET /api/token-ledger`
**AKTUAL:** ortu-only. Butuh `?anakId=X` + `sesiOrtu()`.
Anak TIDAK BISA lihat ledger sendiri. Butuh endpoint baru.
### Saldo β BP bilang `GET /api/anak/saldo` (estimasi)
**AKTUAL:** endpoint TIDAK ADA. Saldo anak hanya dari response `POST /api/anak/sesi-main` (`{ok, menit, saldo}`).
Butuh endpoint baru `GET /api/anak/saldo` atau `GET /api/anak/profil` yang return saldo + streak + nama.
## Yang perlu DIBUAT di backend (3 endpoint baru)
### 1. `GET /api/anak/saldo` β saldo token anak real-timeAKTUAL: pinjam = ortu-only. POST = config (limitPinjam, bunga), bukan ajukan pinjam.
Anak TIDAK BISA pinjam dari app. Butuh endpoint baru.
AKTUAL: ortu-only. Butuh ?anakId=X + sesiOrtu().
Anak TIDAK BISA lihat ledger sendiri. Butuh endpoint baru.
AKTUAL: endpoint TIDAK ADA. Saldo anak hanya dari response POST /api/anak/sesi-main ({ok, menit, saldo}).
Butuh endpoint baru GET /api/anak/saldo atau GET /api/anak/profil yang return saldo + streak + nama.
// src/app/api/anak/saldo/route.ts
export async function GET() {
const anak = await sesiAnak()
if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })
const saldo = await prisma.tokenBalance.findUnique({
where: { anakId: anak.id },
select: { balance: true },
})
return NextResponse.json({ saldo: saldo?.balance ?? 0 })
}// src/app/api/anak/saldo/route.ts
export async function GET() {
const anak = await sesiAnak()
if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })
const saldo = await prisma.tokenBalance.findUnique({
where: { anakId: anak.id },
select: { balance: true },
})
return NextResponse.json({ saldo: saldo?.balance ?? 0 })
}
### 2. `GET /api/anak/riwayat-token` β ledger anak (child-side)
// src/app/api/anak/riwayat-token/route.ts
export async function GET() {
const anak = await sesiAnak()
if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })
const ledger = await prisma.tokenLedger.findMany({
where: { anakId: anak.id },
orderBy: { createdAt: 'desc' },
take: 50,
select: { id: true, tipe: true, jumlah: true, catatan: true, createdAt: true },
})
return NextResponse.json({ ledger })
}// src/app/api/anak/riwayat-token/route.ts
export async function GET() {
const anak = await sesiAnak()
if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })
const ledger = await prisma.tokenLedger.findMany({
where: { anakId: anak.id },
orderBy: { createdAt: 'desc' },
take: 50,
select: { id: true, tipe: true, jumlah: true, catatan: true, createdAt: true },
})
return NextResponse.json({ ledger })
}
### 3. `POST /api/anak/pinjam` β anak ajukan pinjam (child-side)
// src/app/api/anak/pinjam/route.ts
export async function POST(req: Request) {
const anak = await sesiAnak()
if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })
const { jumlah } = await req.json()
// cek hutang aktif, cek limit, catat ledger PINJAM
// ...
}// src/app/api/anak/pinjam/route.ts
export async function POST(req: Request) {
const anak = await sesiAnak()
if (!anak) return NextResponse.json({ error: 'Akses ditolak' }, { status: 401 })
const { jumlah } = await req.json()
// cek hutang aktif, cek limit, catat ledger PINJAM
// ...
}
Catatan: logic pinjam anak butuh cek limit config (disimpan ortu via `/api/pinjam` POST sebagai catatan di ledger). Perlu parse catatan `pinjam-config:limit=X,bunga=Y`. ## Impact ke Roadmap | Fitur | Status Sebelum | Status Sekarang | Aksi | |-------|----------------|-----------------|------| | Tukar Token | API ready | β CONFIRMED ready | Langsung kode | | Katalog Hadiah | API ready | β CONFIRMED ready | Langsung kode | | Quest Keluarga | API ready | β CONFIRMED ready | Langsung kode | | Challenge Harian | API "estimasi" | β EXISTS, shape beda | Fix DTO, langsung kode | | Streak | (tidak ada di BP) | β BONUS β ada endpoint | Tambah ke UI | | Saldo real-time | API "estimasi" | β TIDAK ADA endpoint | Buat backend dulu | | Pinjam Waktu | API "estimasi" | β ORTU-ONLY, no child endpoint | Buat backend dulu | | Riwayat Token | API "estimasi" | β ORTU-ONLY, no child endpoint | Buat backend dulu | ## Revisi prioritas - **Prioritas 1 (langsung kode app):** Tukar Token, Katalog Hadiah, Quest, Challenge, Streak, Saldo (dari sesi-main response) - **Prioritas 2 (butuh backend baru dulu):** Saldo endpoint, Riwayat Token endpoint, Pinjam endpoint - Backend = ~60 lines TypeScript (3 route files). App anak tetap ~465 lines.
Catatan: logic pinjam anak butuh cek limit config (disimpan ortu via /api/pinjam POST sebagai catatan di ledger). Perlu parse catatan pinjam-config:limit=X,bunga=Y.
| Fitur | Status Sebelum | Status Sekarang | Aksi |
|-------|----------------|-----------------|------|
| Tukar Token | API ready | β CONFIRMED ready | Langsung kode |
| Katalog Hadiah | API ready | β CONFIRMED ready | Langsung kode |
| Quest Keluarga | API ready | β CONFIRMED ready | Langsung kode |
| Challenge Harian | API "estimasi" | β EXISTS, shape beda | Fix DTO, langsung kode |
| Streak | (tidak ada di BP) | β BONUS β ada endpoint | Tambah ke UI |
| Saldo real-time | API "estimasi" | β TIDAK ADA endpoint | Buat backend dulu |
| Pinjam Waktu | API "estimasi" | β ORTU-ONLY, no child endpoint | Buat backend dulu |
| Riwayat Token | API "estimasi" | β ORTU-ONLY, no child endpoint | Buat backend dulu |
App anak punya dua API client pattern berbeda:
x-device-tokenJSONObjectAuthorization: Bearer <jwt>ApiAgent dibuat duluan (fase 1 β ping loop simple). Api (Retrofit) ditambah belakangan (fase 2 β edu API). Tidak pernah di-unify.
Api.kt. JANGAN tambah ke ApiAgent.LayarHadiah.kt semua endpoint baru β Retrofit Api.kt (Bearer JWT, sesiAnak)AgentService ping tiap 5 menit (PERIODE_MS = 5 * 60_000L). Blokir dievaluasi hanya saat ping response diterima.
1. Ortu set curfew 21:00 dari app ortu β tersimpan di DB
2. Ping terakhir 20:55 β response dievaluasi β belum curfew
3. Ping berikutnya 21:00 β response dievaluasi β curfew aktif β blokir
4. Lag: sampai 5 menit antara curfew mulai dan blokir aktif
Blueprint Opsi B implemented: dynamic ping interval.
Kebijakan.menitKeCurfew(jam, menit, mulai, selesai) β hitung menit sampai curfew mulai (support cross-midnight, mis 21:00-05:00)AgentService.hitungPeriode() β ping 1 menit saat < 10 menit sebelum curfew, 5 menit normalHandler.postDelayed menggantikan sleep loop β lebih responsifkeystore/rilis.keystore + keystore/rilis.keystore.pw committed langsung ke git repo.
Siapa pun akses repo (public GitHub) = bisa:
1. Download keystore + password
2. Sign APK malicious dengan signature sama
3. Replace app di HP anak (auto-update dari GitHub releases!)
4. Akses semua data anak (agent service, lokasi, usage stats)
Attack vector nyata: auto-update (PemeriksaPembaruan.kt) download APK dari GitHub releases. Attacker sign APK malicious dengan keystore curian β upload ke GitHub releases dengan tag versi lebih tinggi β app anak auto-update β install APK malicious. Signature cocok β Android terima.
1. Generate keystore BARU β RSA 2048, alias zxagent, validitas 10000 hari
2. Simpan di GitHub Secrets: KEYSTORE_B64 (base64), KEYSTORE_PASSWORD, KEYSTORE_ALIAS, KEYSTORE_KEY_PASSWORD β semua via GitHub API
3. rilis.yml Secrets-first logic: decode dari Secret, fallback ke repo file (zero-downtime transition)
4. Hapus keystore dari repo β git rm --cached + .gitignore patterns
5. README update β dokumentasi rotasi
24c114e) tetap di history β anggap compromised permanentlyApp anak punya 2 mekanisme update yang akan konflik:
1. PemeriksaPembaruan.kt β GitHub releases (bypass Play Store)
2. Play Store in-app update β AppUpdateManager FLEXIBLE type
PemeriksaPembaruan.kt sekarang cek installer:
private fun apasangDariPlayStore(c: Context): Boolean {
val installer = c.packageManager.getInstallerPackageName(c.packageName)
return installer == "com.android.vending"
}private fun apasangDariPlayStore(c: Context): Boolean {
val installer = c.packageManager.getInstallerPackageName(c.packageName)
return installer == "com.android.vending"
}
- Jika install dari Play Store β skip GitHub update check, return null - Jika install dari GitHub (sideload) β tetap pakai GitHub auto-update ### Play Store in-app update ready - `AppUpdateManagerFactory.create(context)` + `AppUpdateType.FLEXIBLE` - Dialog "Update tersedia" dari Play Store - Update via Play Store, signature verified Google ## Status roadmap - **Fase 1 (sekarang):** GitHub auto-update aktif untuk sideload install β - **Fase 5 (Play Store live):** installer guard + in-app update aktif untuk Play Store install β ready - **Permission:** `REQUEST_INSTALL_PACKAGES` tetap untuk pre-Play Store, hapus setelah live
AppUpdateManagerFactory.create(context) + AppUpdateType.FLEXIBLEREQUEST_INSTALL_PACKAGES tetap untuk pre-Play Store, hapus setelah liveGoogle Play reviewer harus bisa test app tanpa device ortu untuk pairing.
LayarMasuk.kt (Compose) β tombol "Mode Demo" di login screenSimpanan.setDemo(ctx) β flag demo mode di SharedPreferencesMainActivity.onCreate β cek Simpanan.isDemo(ctx) β skip AgentService, tampilkan UI dengan sample dataDataDemo.kt β sample data: kuis, tugas, challenge, katalog hadiah, quest, ledger, pinjam1. Install APK β buka app
2. LayarMasuk muncul β klik "Mode Demo"
3. Full UI tampil dengan sample data (5 tab: Beranda, Kuis, Tugas, Hadiah, Saya)
4. Tidak perlu pairing device ortu, tidak perlu login anak
5. Tombol "Hubungkan ke Ortu" di Saya β kembali ke pairing flow
App butuh pairing device ortu untuk full functionality. Demo mode tersedia: buka app β tombol "Mode Demo" di layar login. Demo menampilkan sample kuis, tugas, hadiah, dan UI tanpa device ortu.
App butuh pairing device ortu untuk full functionality.
Demo mode tersedia: buka app β tombol "Mode Demo" di layar login.
Demo menampilkan sample kuis, tugas, hadiah, dan UI tanpa device ortu.
### String resources - EN: `demo_mode` = "Demo Mode", `demo_mode_desc` = "Browse the app with sample data..." - ID: `demo_mode` = "Mode Demo", `demo_mode_desc` = "Jelajahi app dengan data contoh..." ### Alternatif (tidak dipakai) Test account di backend β lebih realistic tapi butuh data dummy di DB + cleanup. Demo mode cukup untuk reviewer.
demo_mode = "Demo Mode", demo_mode_desc = "Browse the app with sample data..."demo_mode = "Mode Demo", demo_mode_desc = "Jelajahi app dengan data contoh..."Test account di backend β lebih realistic tapi butuh data dummy di DB + cleanup. Demo mode cukup untuk reviewer.
AksesService (accessibility service untuk blokir per-app) bisa:
AksesService.instance jadi nullBlokir.daftarApp() tetap di SharedPreferences (data bertahan)START_STICKY = Android restart eventually, tapi tidak immediatelyappBlokir (daftar app diblokir ortu) tidak enforced. Anak bisa buka app yang seharangnya diblokir. Ortu tidak tahu enforcement sedang tidak aktif.
Solusi: deteksi + alert + log, bukan prevent (Android tidak allow prevent).
1. AgentService ping loop β cek AksesService.instance == null tiap ping
2. Notifikasi HIGH priority β channel alert (IMPORTANCE_HIGH), "ZX Parenting perlu perhatian"
3. Log ke backend β Api.kirimAktivitas("accessibility_off") β ortu lihat di laporan
4. LayarSaya status card β tampilkan status accessibility (Aktif/Nonaktif), tombol "Buka Settings" langsung ke ACTION_ACCESSIBILITY_SETTINGS
URL: https://zxparenting.zomet.my.id/design/beranda-mockup.html
Repo: zx_parenting/public/design/beranda-mockup.html
{id, judul, topik, tokenReward, selesai}. Progress bar. Reward +5- Aksesibilitas: Aktif/Nonaktif + tombol Settings
- Device Admin: Aktif (anti-uninstall)
- Laporan Pemakaian: Aktif
- GPS Lokasi: Aktif
- Kunci Layar (lockNow): Siap
1. Beranda (house-door)
2. Kuis AI (stars)
3. Tugas (list-task)
4. Hadiah (card-giftcard) β BARU
5. Saya (person)
Default bahasa Inggris. Sementara hanya opsi Indonesia. Arsitektur harus mudah tambah bahasa + ganti tema.
| Aspek | Status |
|-------|--------|
| strings.xml EN (values/) | β 92+ strings |
| strings.xml ID (values-in/) | β 92+ strings |
| ManajerBahasa.kt | β AppCompatDelegate.setApplicationLocales, persist SharedPreferences |
| Language picker (LayarSaya) | β EN/ID/system |
| Tambah bahasa | values-xx/strings.xml + 1 baris di picker |
| File | Status | Desc |
|------|--------|------|
| tema/Palet.kt | β
| data class 20 warna, dariJson() parser, DEFAULT hardcoded |
| tema/WarnaAktif.kt | β
| singleton mutableStateOf, gantiByKey(), inisialisasi(), TemaZX wrapper |
| assets/tema/default.json | β
| tema Default (biru) |
| assets/tema/gelap.json | β
| tema Malam (dark) |
| assets/tema/laut.json | β
| tema Laut (cyan) |
| Theme picker (LayarSaya) | β | 3 pilihan, instant recompose |
| File | Status |
|------|--------|
| PasangActivity.kt | β WarnaAktif.palet.*.toArgb() |
| StatusActivity.kt | β WarnaAktif.palet.*.toArgb() |
| BlokirActivity.kt | β WarnaAktif.palet.*.toArgb() |
Color(0x...) di 8 file ComposeBg, Biru, Teks, dst computed property)WarnaAktif.palet.*.toArgb())Color(0x...) di Palet.kt (DEFAULT definition β correct, must be literal) + WarnaAktif.kt (dark/light scheme constants)Drop file JSON di assets/tema/nama.json β otomatis muncul di picker. No recompile.
Buat values-xx/strings.xml + 1 baris di language picker. Perlu recompile (APK split).