Build an Anti-Cyberbullying Keyboard

Build a custom Android keyboard that detects bullying words in real-time.

Introduction

30 Second Summary

Cyberbullying often starts with a single message typed in the heat of the moment. What if the keyboard itself could pause that moment and ask the sender to reconsider?

In this project, you will build SafeType, a custom Android keyboard that detects potentially hurtful words in Bahasa Indonesia as they are typed. When a flagged word appears, a warning banner gives the user a chance to change their message before it gets sent.

What You'll Build

You'll demo SafeType live on your physical Android device: type a harsh word in WhatsApp or any messaging app, watch the warning banner appear instantly above the keyboard, tap your choice, then open the companion dashboard to show detection statistics as a proof-of-concept for school counselors.

By the end of this project, you'll have:

  • A functional QWERTY keyboard that works as a system-wide input method in WhatsApp, Instagram, or any Android app on your device.
  • A real-time warning banner that appears when you type words from an Indonesian cyberbullying dictionary, offering "Kirim Aja" (send anyway) or "Ubah Pesan" (edit message) choices.
  • A privacy-preserving BK dashboard that displays aggregate detection statistics (date and trigger count only) without storing any message content.
  • Secret Mission: add a Cool Down Mode that disables the send button for 5 seconds with a visible countdown, forcing a pause before hurtful messages can go through.

Are there any prerequisites?

You should have basic experience building Android apps with Kotlin and Android Studio already installed on your computer. You also need a physical Android device for testing, because system keyboards cannot be fully tested on an emulator.

Before We Start

Before diving into code, let's lock in what you're about to build and why it matters. This step is your chance to articulate the goal in your own words before any hands-on work begins.

Set Up the Project and IME Configuration

Keyboard custom Android berbeda total dari app biasa. Tanpa konfigurasi InputMethodService yang benar di manifest, Android tidak akan mengenali app kamu sebagai keyboard yang bisa dipilih user.

Di step ini, kamu akan membuat project Android dari nol, menyiapkan semua dependencies yang dibutuhkan, lalu mendaftarkan SafeType sebagai keyboard system-level. Di akhir step ini, SafeType sudah muncul di daftar keyboard perangkatmu.

In this step, get ready to:
  • Membuat project Android Studio baru dan menambahkan semua dependencies (Compose, Room, KSP).
  • Membuat class SafeTypeIME dengan lifecycle bridging untuk Jetpack Compose.
  • Mendaftarkan keyboard di AndroidManifest dan memverifikasinya muncul di Settings device.
Buat project baru dan konfigurasi dependencies

Kamu butuh project Android baru dengan template Empty Activity sebagai titik awal. Template ini sudah menyertakan setup Jetpack Compose dasar yang akan kamu modifikasi.

  • Di Android Studio, klik New Project pada Welcome screen.
  • Pilih Empty Activity, lalu klik Next.
  • Isi field Name dengan SafeType.
  • Isi field Package name dengan com.safetype.app.
  • Set Minimum SDK ke API 24 (Android 7.0).
  • Pastikan Build configuration language adalah Kotlin DSL, lalu klik Finish.

Kenapa Empty Activity?

Template Empty Activity di Android Studio sudah menyertakan Compose dependencies dan Material Design setup. Kamu akan mengganti isi build.gradle.kts dengan konfigurasi yang sesuai kebutuhan SafeType.

Tunggu Gradle sync selesai. Setelah project terbuka, buka file build.gradle.kts (Module: app) dan ganti seluruh isinya.

  • Ganti isi build.gradle.kts (Module: app) dengan plugins dan android block berikut:
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
    id("com.google.devtools.ksp")
}

android {
    namespace = "com.safetype.app"
    compileSdk = 35

    defaultConfig {
        applicationId = "com.safetype.app"
        minSdk = 24
        targetSdk = 35
        versionCode = 1
        versionName = "1.0"
    }

    buildFeatures {
        compose = true
    }

    kotlinOptions {
        jvmTarget = "17"
    }
}

Apa yang ada di block ini?

  • Plugin com.google.devtools.ksp dibutuhkan oleh Room untuk code generation pada compile time.
  • Plugin org.jetbrains.kotlin.plugin.compose mengaktifkan Compose compiler untuk Kotlin 2.0+.
  • compileSdk = 35 dan targetSdk = 35 memastikan kompatibilitas dengan Compose BOM terbaru.
  • Langsung di bawah closing bracket android {}, tambahkan block dependencies ini:
dependencies {
    val composeBom = platform("androidx.compose:compose-bom:2026.06.00")
    implementation(composeBom)

    implementation("androidx.compose.material3:material3")
    implementation("androidx.compose.foundation:foundation")
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.animation:animation")

    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.9.1")
    implementation("androidx.activity:activity-compose:1.13.0")

    val roomVersion = "2.8.4"
    implementation("androidx.room:room-runtime:$roomVersion")
    implementation("androidx.room:room-ktx:$roomVersion")
    ksp("androidx.room:room-compiler:$roomVersion")

    implementation("androidx.core:core-ktx:1.16.0")
}

Apa yang ada di dependencies ini?

  • Compose BOM 2026.06.00 mengelola versi semua library Compose agar saling kompatibel.
  • Room 2.8.4 menyediakan local database untuk menyimpan statistik deteksi.
  • ksp("androidx.room:room-compiler") menjalankan annotation processing Room saat build.
  • Klik Sync Now di banner kuning yang muncul di atas editor.

Kamu akan melihat pesan BUILD SUCCESSFUL di tab Build setelah Gradle sync selesai.

Gradle sync gagal?

  • Pastikan kamu sudah menambahkan plugin com.google.devtools.ksp juga di file build.gradle.kts level project (biasanya sudah ada jika Android Studio versi terbaru).
  • Periksa koneksi internet. Gradle perlu mendownload dependencies dari Maven repositories.
  • Pastikan jvmTarget diset ke "17" dan JDK 17 terinstall di Android Studio.

Gradle sync gagal dan error saya tidak ada di atas.

Buat class SafeTypeIME dengan lifecycle bridging

InputMethodService adalah base class Android untuk semua custom keyboard. Tidak seperti Activity, service ini tidak punya lifecycle Compose bawaan, jadi kamu perlu meng-bridge lifecycle secara manual.

  • Buat package baru: klik kanan folder com.safetype.app di panel Project, pilih New lalu Package.
  • Beri nama ime sehingga path lengkapnya menjadi com.safetype.app.ime.
  • Di dalam package ime, buat file Kotlin baru bernama SafeTypeIME.kt dengan klik kanan pada package, pilih New lalu Kotlin Class/File.
  • Paste kode berikut sebagai isi SafeTypeIME.kt (bagian class declaration dan properties):
package com.safetype.app.ime

import android.inputmethodservice.InputMethodService
import android.view.View
import androidx.compose.ui.platform.AbstractComposeView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.savedstate.SavedStateRegistry
import androidx.savedstate.SavedStateRegistryController
import androidx.savedstate.SavedStateRegistryOwner

class SafeTypeIME : InputMethodService(), LifecycleOwner, SavedStateRegistryOwner {

    private val lifecycleRegistry = LifecycleRegistry(this)
    override val lifecycle: Lifecycle get() = lifecycleRegistry

    private val savedStateRegistryController = SavedStateRegistryController.create(this)
    override val savedStateRegistry: SavedStateRegistry
        get() = savedStateRegistryController.savedStateRegistry

Kenapa perlu LifecycleOwner dan SavedStateRegistryOwner?

Jetpack Compose membutuhkan LifecycleOwner untuk mengamati state changes dan SavedStateRegistryOwner untuk menyimpan state UI. InputMethodService tidak menyediakan keduanya secara default, jadi kamu harus mengimplementasikannya sendiri.

  • Langsung di bawah property savedStateRegistry, tambahkan lifecycle methods berikut (masih di dalam class yang sama):
    override fun onCreate() {
        super.onCreate()
        savedStateRegistryController.performRestore(null)
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
    }

    override fun onCreateInputView(): View {
        window?.window?.decorView?.let { decorView ->
            decorView.setViewTreeLifecycleOwner(this)
            decorView.setViewTreeSavedStateRegistryOwner(this)
        }
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)
        return SafeTypeKeyboardView(this)
    }

    override fun onDestroy() {
        super.onDestroy()
        lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    }
}

Apa yang dilakukan methods ini?

  • onCreate() melakukan restore saved state dan memberitahu lifecycle bahwa service sudah dibuat.
  • onCreateInputView() dipanggil Android saat keyboard perlu ditampilkan. Di sini kamu meng-attach lifecycle ke window dan return Compose-based view.
  • setViewTreeLifecycleOwner(this) menghubungkan Compose UI tree ke lifecycle service kamu.

SafeTypeIME me-return SafeTypeKeyboardView(this) yang belum ada. Kamu perlu membuat file placeholder ini agar project bisa compile.

  • Di package com.safetype.app.ime, buat file baru SafeTypeKeyboardView.kt dengan kode berikut:
package com.safetype.app.ime

import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.AbstractComposeView

// Placeholder - akan dibangun lengkap di Step 2
class SafeTypeKeyboardView(
    private val service: SafeTypeIME
) : AbstractComposeView(service) {
    @Composable
    override fun Content() {
        MaterialTheme {
            Text("SafeType Keyboard")
        }
    }
}

Kenapa ada placeholder?

File ini hanya supaya project compile. Di Step 2, kamu akan mengganti isinya dengan keyboard QWERTY yang fungsional.

Manifest juga akan mereferensikan SafeTypeApplication dan DashboardActivity yang dibangun di step berikutnya. Buat placeholder untuk keduanya sekarang.

  • Di package com.safetype.app (root package), buat file SafeTypeApplication.kt:
package com.safetype.app

import android.app.Application

// Placeholder - Room database ditambahkan di Step 5
class SafeTypeApplication : Application()
  • Buat package ui di dalam com.safetype.app, lalu buat file DashboardActivity.kt:
package com.safetype.app.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text

// Placeholder - dashboard lengkap dibangun di Step 5
class DashboardActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MaterialTheme {
                Text("SafeType Dashboard")
            }
        }
    }
}
  • Klik Build di menu bar, lalu pilih Make Project (atau tekan Cmd+F9 di macOS / Ctrl+F9 di Windows).

Kamu akan melihat BUILD SUCCESSFUL di panel Build output. Ini memastikan semua class dan dependencies terhubung dengan benar.

Build gagal dengan Unresolved reference?

  • Pastikan nama package di setiap file sesuai dengan lokasi foldernya. SafeTypeIME.kt harus ada di folder ime, bukan di root.
  • Periksa bahwa SafeTypeKeyboardView.kt berada di package yang sama (com.safetype.app.ime) dengan SafeTypeIME.kt.
  • Jika error pada setViewTreeLifecycleOwner, pastikan dependency lifecycle-runtime-ktx ada di build.gradle.kts.

Build saya gagal dan saya tidak bisa menemukan solusinya.

Konfigurasi manifest dan daftarkan keyboard

Android hanya mengenali app sebagai keyboard jika manifest mendeklarasikan service dengan permission BIND_INPUT_METHOD dan intent-filter yang tepat.

  • Buka AndroidManifest.xml (di folder app/src/main) dan ganti seluruh isinya dengan application tag berikut:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:name=".SafeTypeApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/Theme.SafeType">

        <activity
            android:name=".ui.DashboardActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

Apa isi manifest ini sejauh ini?

  • android:name=".SafeTypeApplication" menunjuk ke class Application custom yang mengelola database.
  • DashboardActivity didaftarkan sebagai LAUNCHER activity, jadi app muncul di home screen.
  • Sekarang tambahkan deklarasi service IME. Paste kode berikut di dalam tag <application>, tepat di bawah closing tag </activity>:
        <service
            android:name=".ime.SafeTypeIME"
            android:exported="true"
            android:label="@string/app_name"
            android:permission="android.permission.BIND_INPUT_METHOD">
            <intent-filter>
                <action android:name="android.view.InputMethod" />
            </intent-filter>
            <meta-data
                android:name="android.view.im"
                android:resource="@xml/input_method" />
        </service>

Kenapa perlu BIND_INPUT_METHOD?

Permission android.permission.BIND_INPUT_METHOD memastikan hanya Android system yang bisa bind ke service keyboard kamu. Tanpa permission ini, app lain bisa mengakses input method secara tidak sah.

Meta-data android.view.im mengarah ke file XML yang mendefinisikan subtype keyboard (bahasa dan mode input).

  • Buat folder xml di dalam res: klik kanan folder res, pilih New lalu Android Resource Directory, set Resource type ke xml, klik OK.
  • Di dalam folder res/xml, buat file baru bernama input_method.xml dengan kode berikut:
<?xml version="1.0" encoding="utf-8"?>
<input-method xmlns:android="http://schemas.android.com/apk/res/android">
    <subtype
        android:languageTag="id"
        android:imeSubtypeMode="keyboard"
        android:label="@string/subtype_label" />
</input-method>

Apa itu subtype?

Subtype mendefinisikan bahasa dan mode keyboard. languageTag="id" menandakan ini keyboard Bahasa Indonesia. Android menggunakan info ini untuk menampilkan label yang tepat di Settings.

  • Buka file res/values/strings.xml dan ganti isinya dengan:
<resources>
    <string name="app_name">SafeType</string>
    <string name="subtype_label">Bahasa Indonesia</string>
</resources>

Sekarang saatnya memverifikasi bahwa Android mengenali SafeType sebagai keyboard.

  • Hubungkan device fisik Android kamu via USB (pastikan USB Debugging aktif).
  • Klik tombol Run (ikon play hijau) atau tekan Shift+F10.
  • Tunggu app terinstall di device.

Sebelum membuka Settings, menurutmu apakah SafeType sudah muncul di daftar keyboard? Atau ada yang kurang?

  • Di device, buka Settings.
  • Pilih System.
  • Pilih Languages & input.
  • Pilih On-screen keyboard.
  • Tap Manage keyboards.

Kamu akan melihat SafeType dalam daftar keyboard yang tersedia. Ini membuktikan manifest dan konfigurasi IME kamu sudah benar.

SafeType tidak muncul di daftar keyboard?

  • Pastikan app berhasil terinstall tanpa error. Cek panel Run di Android Studio untuk error messages.
  • Periksa bahwa <service> di manifest punya permission android.permission.BIND_INPUT_METHOD dan intent-filter android.view.InputMethod.
  • Pastikan file res/xml/input_method.xml ada dan direferensikan dengan benar di meta-data.
  • Coba restart device lalu cek ulang Settings.

SafeType tetap tidak muncul di Settings setelah saya coba semua di atas.

✔️ Awesome, I've got everything!

Bagus. Pastikan kamu sudah save semua file sebelum lanjut ke step berikutnya.

ⓧ I'd like to double check the full code

Berikut file-file yang sudah kamu buat di step ini. Scroll ke atas dan pastikan setiap chunk sudah ada di project kamu, dalam urutan yang benar:

  • build.gradle.kts (Module: app): plugins + android block, lalu dependencies block.
  • SafeTypeIME.kt di package com.safetype.app.ime: class declaration + properties, lalu lifecycle methods.
  • SafeTypeKeyboardView.kt di package com.safetype.app.ime: placeholder Compose view.
  • SafeTypeApplication.kt di package com.safetype.app: placeholder Application class.
  • DashboardActivity.kt di package com.safetype.app.ui: placeholder launcher activity.
  • AndroidManifest.xml: application tag dengan activity + service declaration.
  • res/xml/input_method.xml: subtype Bahasa Indonesia.
  • res/values/strings.xml: app_name dan subtype_label.

SafeType sudah terdaftar sebagai keyboard di perangkatmu. Di step berikutnya, kamu akan mengubah placeholder keyboard menjadi layout QWERTY fungsional yang bisa mengetik di semua app.

Build a Functional QWERTY Keyboard

Android sekarang mengenali SafeType sebagai keyboard, tapi keyboard-nya belum bisa mengetik apapun. Kamu butuh keyboard yang betulan berfungsi sebelum bisa menambahkan fitur deteksi.

Di step ini, kamu akan membangun layout QWERTY penuh menggunakan Jetpack Compose dan menghubungkannya ke InputConnection agar setiap tap key menghasilkan huruf di app target.

In this step, get ready to:
  • Bangun layout keyboard QWERTY 3 baris plus baris aksi menggunakan Compose.
  • Implementasi fungsi input handling yang mengirim karakter ke app target.
  • Test keyboard di device fisik untuk memastikan huruf muncul saat diketik.
Create the QWERTY keyboard layout

Keyboard custom Android butuh sebuah View yang ditampilkan di bagian bawah layar. Kamu akan membuat file baru yang berisi Composable untuk merender tombol-tombol QWERTY.

  • Buat file baru SafeTypeKeyboardView.kt di folder src/main/java/com/safetype/app/ime/.
  • Klik kanan pada package ime di panel Project, pilih New lalu Kotlin Class/File.
  • Pilih File dan beri nama SafeTypeKeyboardView.
  • Tambahkan kode berikut sebagai class wrapper dan composable utama:
package com.safetype.app.ime

import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.AbstractComposeView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

// View wrapper yang menghubungkan Compose ke InputMethodService
class SafeTypeKeyboardView(private val service: SafeTypeIME) : AbstractComposeView(service) {
    @Composable
    override fun Content() {
        MaterialTheme {
            SafeTypeKeyboard(service = service)
        }
    }
}

// Composable utama keyboard
@Composable
fun SafeTypeKeyboard(service: SafeTypeIME) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color(0xFF1B1B1F))
    ) {
        KeyboardLayout(service = service)
    }
}

What does this code do?

SafeTypeKeyboardView extends AbstractComposeView sehingga kamu bisa menggunakan Jetpack Compose di dalam InputMethodService. Tanpa wrapper ini, IME hanya bisa menggunakan XML layout tradisional.

SafeTypeKeyboard adalah composable utama yang menjadi container berwarna gelap untuk semua tombol keyboard.

Kode ini belum bisa di-build karena KeyboardLayout belum ada. Tambahkan composable berikut tepat di bawah fungsi SafeTypeKeyboard di file yang sama:

// Layout QWERTY 3 baris + baris aksi
@Composable
fun KeyboardLayout(service: SafeTypeIME) {
    val rows = listOf(
        listOf("q", "w", "e", "r", "t", "y", "u", "i", "o", "p"),
        listOf("a", "s", "d", "f", "g", "h", "j", "k", "l"),
        listOf("z", "x", "c", "v", "b", "n", "m")
    )

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .padding(4.dp)
    ) {
        rows.forEach { row ->
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.Center
            ) {
                row.forEach { key ->
                    KeyButton(label = key) { service.onKeyPress(key) }
                }
            }
        }

        // Baris bawah: hapus, spasi, enter
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            ActionKeyButton(label = "DEL") { service.onBackspace() }
            ActionKeyButton(label = "SPASI", modifier = Modifier.weight(1f)) { service.onSpace() }
            ActionKeyButton(label = "ENTER") { service.onEnter() }
        }
    }
}

What does this code do?

  • rows menyimpan layout QWERTY sebagai list of list. Baris pertama 10 huruf, kedua 9, ketiga 7.
  • Setiap huruf dirender sebagai KeyButton yang memanggil service.onKeyPress(key) saat di-tap.
  • Baris bawah berisi tombol DEL, SPASI (flexible width), dan ENTER.

Sekarang kamu butuh composable untuk individual key. Tambahkan kedua fungsi berikut di bawah KeyboardLayout dalam file yang sama:

// Tombol huruf individual
@Composable
fun KeyButton(label: String, onClick: () -> Unit) {
    Box(
        modifier = Modifier
            .size(width = 32.dp, height = 42.dp)
            .padding(2.dp)
            .clip(RoundedCornerShape(4.dp))
            .background(Color(0xFF2C2C34))
            .clickable { onClick() },
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = label,
            color = Color.White,
            fontSize = 16.sp
        )
    }
}

// Tombol aksi (DEL, SPASI, ENTER) dengan style berbeda
@Composable
fun ActionKeyButton(
    label: String,
    modifier: Modifier = Modifier,
    onClick: () -> Unit
) {
    Box(
        modifier = modifier
            .height(42.dp)
            .padding(2.dp)
            .clip(RoundedCornerShape(4.dp))
            .background(Color(0xFF3D3D47))
            .clickable { onClick() },
        contentAlignment = Alignment.Center
    ) {
        Text(
            text = label,
            color = Color.White,
            fontSize = 12.sp,
            fontWeight = FontWeight.Bold
        )
    }
}

What does this code do?

  • KeyButton merender satu tombol huruf berukuran 32x42dp dengan background abu gelap.
  • ActionKeyButton mirip tapi menerima custom Modifier sehingga tombol SPASI bisa fleksibel lebarnya dengan weight(1f).
  • Warna background sedikit berbeda (0xFF3D3D47 vs 0xFF2C2C34) untuk membedakan tombol aksi dari tombol huruf.
  • Simpan file SafeTypeKeyboardView.kt.

Melihat error merah di editor?

Ini normal. Composable KeyboardLayout memanggil service.onKeyPress(), service.onBackspace(), dan fungsi lain yang belum ada di SafeTypeIME. Kita akan menambahkannya di substep berikutnya.

Jika kamu melihat error pada import, tekan Opt+Enter (macOS) atau Alt+Enter (Windows) untuk auto-import.

Implement input handling di SafeTypeIME

Keyboard sudah punya UI, tapi tap pada tombol belum menghasilkan apapun. Kamu perlu menambahkan fungsi-fungsi handler yang mengirim karakter ke app target melalui InputConnection.

  • Buka file SafeTypeIME.kt yang sudah ada di folder ime.
  • Tambahkan import baru dan state fields berikut. Letakkan state fields tepat di bawah deklarasi savedStateRegistry:
import android.view.inputmethod.EditorInfo
import androidx.compose.runtime.mutableStateOf

// ... (di dalam class SafeTypeIME, setelah savedStateRegistry)

// State untuk tracking teks yang diketik
var textBuffer = mutableStateOf("")
    private set
var isWarningActive = mutableStateOf(false)
    private set
var detectedWord = mutableStateOf("")
    private set

Kenapa ada isWarningActive dan detectedWord?

textBuffer menyimpan semua karakter yang diketik user di sesi ini. State isWarningActive dan detectedWord disiapkan untuk fitur WordFilter di step selanjutnya. Menambahkannya sekarang berarti kamu tidak perlu mengubah signature fungsi nanti.

  • Masih di file yang sama, tambahkan lifecycle methods baru. Letakkan tepat di bawah fungsi onCreateInputView():
// Dipanggil setiap kali keyboard muncul di layar
override fun onStartInputView(editorInfo: EditorInfo?, restarting: Boolean) {
    super.onStartInputView(editorInfo, restarting)
    lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
    // Reset buffer setiap mulai input baru
    textBuffer.value = ""
    isWarningActive.value = false
    detectedWord.value = ""
}

// Dipanggil saat keyboard disembunyikan
override fun onFinishInputView(finishingInput: Boolean) {
    super.onFinishInputView(finishingInput)
    lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_PAUSE)
}

What does this code do?

  • onStartInputView() dipanggil setiap kali keyboard muncul di app baru atau field baru. Buffer direset agar tidak ada sisa teks dari sesi sebelumnya.
  • onFinishInputView() dipanggil saat keyboard tersembunyi. Lifecycle event ON_PAUSE memastikan Compose tahu untuk pause recomposition.

Sekarang tambahkan 4 fungsi handler yang menjadi jembatan antara UI keyboard dan app target. Letakkan di bawah onDestroy():

// Kirim satu karakter ke app target
fun onKeyPress(key: String) {
    val ic = currentInputConnection ?: return
    ic.commitText(key, 1)
    textBuffer.value += key
}

// Hapus satu karakter ke kiri
fun onBackspace() {
    val ic = currentInputConnection ?: return
    ic.deleteSurroundingText(1, 0)
    if (textBuffer.value.isNotEmpty()) {
        textBuffer.value = textBuffer.value.dropLast(1)
    }
}

// Kirim newline (enter)
fun onEnter() {
    val ic = currentInputConnection ?: return
    ic.commitText("\n", 1)
    textBuffer.value = ""
}

// Kirim spasi
fun onSpace() {
    val ic = currentInputConnection ?: return
    ic.commitText(" ", 1)
    textBuffer.value += " "
}

What does this code do?

  • currentInputConnection adalah koneksi ke text field app yang sedang aktif (WhatsApp, Notes, dll).
  • commitText(key, 1) mengirim karakter ke posisi cursor saat ini.
  • deleteSurroundingText(1, 0) menghapus 1 karakter di sebelah kiri cursor.
  • Setiap operasi juga update textBuffer agar keyboard selalu tahu teks lengkap yang sudah diketik.
  • Simpan SafeTypeIME.kt.
  • Klik Build > Rebuild Project untuk memastikan project compile tanpa error.
  • Kamu harus melihat BUILD SUCCESSFUL di panel Build output.

Build gagal dengan Unresolved reference?

  • Pastikan import android.view.inputmethod.EditorInfo dan androidx.compose.runtime.mutableStateOf ada di bagian atas SafeTypeIME.kt.
  • Jika ada error merah di SafeTypeKeyboardView.kt, tekan Opt+Enter (macOS) atau Alt+Enter (Windows) pada setiap reference merah untuk auto-import.
  • Periksa bahwa semua fungsi (onKeyPress, onBackspace, onSpace, onEnter) berada di DALAM class SafeTypeIME, bukan di luar.

help me fix build errors in my SafeType keyboard project

Aktifkan SafeType dan test di device fisik

Project sudah compile. Sekarang waktunya melihat keyboard beraksi di device fisik.

  • Hubungkan device Android fisik ke komputer via USB.
  • Klik tombol Run (segitiga hijau) di Android Studio untuk install app ke device.
  • Di device, buka Settings > System > Languages & input > On-screen keyboard.
  • Aktifkan SafeType jika belum aktif.
  • Tap Manage on-screen keyboards dan pastikan toggle SafeType menyala.
  • Buka app Notes atau WhatsApp di device.
  • Tap pada text field untuk memunculkan keyboard.
  • Jika keyboard yang muncul bukan SafeType, tap ikon keyboard di navigation bar (biasanya pojok kanan bawah) dan pilih SafeType.

Sebelum kamu mulai mengetik, prediksi dulu: apakah huruf akan muncul di text field saat kamu tap tombol di SafeType?

  • Ketik beberapa huruf menggunakan SafeType keyboard.

Huruf yang kamu tap muncul di text field app target. Coba juga tombol SPASI, DEL (hapus karakter), dan ENTER (baris baru).

Huruf tidak muncul saat mengetik?

  • Pastikan kamu sudah switch ke SafeType keyboard (bukan Gboard atau keyboard lain).
  • Rebuild dan run ulang app dari Android Studio. Perubahan pada InputMethodService kadang butuh re-install untuk efektif.
  • Cek Logcat di Android Studio. Filter dengan tag SafeType untuk melihat apakah ada crash atau exception.

help me debug why my SafeType keyboard is not producing characters when I tap keys

✔️ Awesome, I've got everything!

Bagus. Pastikan kamu sudah save kedua file: SafeTypeKeyboardView.kt dan SafeTypeIME.kt.

ⓧ I'd like to double check the full code

Scroll ke atas dan pastikan kamu punya semua bagian ini di SafeTypeKeyboardView.kt, dalam urutan yang benar:

  • Package declaration dan imports.
  • Class SafeTypeKeyboardView extending AbstractComposeView.
  • Fungsi SafeTypeKeyboard composable.
  • Fungsi KeyboardLayout dengan 3 baris QWERTY + baris aksi.
  • Fungsi KeyButton dan ActionKeyButton.

Dan di SafeTypeIME.kt, pastikan kamu punya:

  • Tiga state fields: textBuffer, isWarningActive, detectedWord.
  • Override onStartInputView() yang reset buffer.
  • Override onFinishInputView() yang handle ON_PAUSE.
  • Empat handler functions: onKeyPress(), onBackspace(), onEnter(), onSpace().

Keyboard kamu sekarang bisa mengetik di semua app. Tapi belum ada yang membedakannya dari Gboard biasa. Di step berikutnya, kamu akan menambahkan WordFilter yang mendeteksi kata kasar secara real-time saat user mengetik.

Implement the WordFilter Engine

Keyboard SafeType sudah bisa dipakai mengetik di semua app. Tapi secara fungsional, belum ada bedanya dengan Gboard atau keyboard bawaan Android lainnya.

Sekarang kamu akan menambahkan WordFilter: mesin deteksi real-time yang memindai teks terhadap dictionary kata intimidasi Bahasa Indonesia. Setiap karakter yang diketik akan dicek, dan hasilnya akan mengubah state isWarningActive yang nanti akan memicu warning banner di step berikutnya.

In this step, get ready to:
  • Membuat WordFilterEngine dengan dictionary 25 kata kasar/intimidasi Bahasa Indonesia.
  • Mengintegrasikan filter ke SafeTypeIME agar setiap keystroke dicek secara real-time.
  • Memverifikasi deteksi melalui Logcat output.
Create the WordFilterEngine class

WordFilterEngine adalah otak deteksi SafeType. Engine ini berisi dictionary kata-kata intimidasi dan fungsi yang memindai teks input untuk menemukan kecocokan.

  • Buat package baru: klik kanan pada folder com.safetype.app di panel **Project**, pilih **New**, lalu **Package**.
  • Beri nama package engine.
  • Klik kanan pada package engine yang baru dibuat, pilih **New**, lalu **Kotlin Class/File**.
  • Pilih **File** dan beri nama WordFilterEngine.
  • Ganti seluruh isi file dengan kode berikut:
package com.safetype.app.engine

// Data class untuk hasil pengecekan filter
data class FilterResult(
    val isDetected: Boolean,
    val detectedWord: String = ""
)

object WordFilterEngine {
    // Dictionary kata-kata kasar/intimidasi Bahasa Indonesia
    private val dictionary = setOf(
        "bodoh", "goblok", "tolol", "bangsat", "anjing", "bego",
        "idiot", "kampret", "bajingan", "brengsek", "sialan",
        "gila", "setan", "iblis", "laknat", "keparat",
        "monyet", "babi", "tai", "kontol", "memek",
        "jelek", "mati", "bunuh", "sampah", "busuk"
    )

    // Scan teks terhadap dictionary
    fun checkText(text: String): FilterResult {
        val words = text.lowercase().split(Regex("\\s+"))
        for (word in words) {
            val cleaned = word.replace(Regex("[^a-z]"), "")
            if (cleaned in dictionary) {
                return FilterResult(isDetected = true, detectedWord = cleaned)
            }
        }
        return FilterResult(isDetected = false)
    }
}

What does this code do?

FilterResult adalah data class yang menyimpan hasil pemeriksaan: apakah kata terdeteksi dan kata apa yang terdeteksi.

WordFilterEngine adalah singleton (object) yang berisi dictionary berupa Set<String> dari 25 kata. Fungsi checkText() memecah teks berdasarkan spasi, membersihkan karakter non-huruf, lalu mencocokkan setiap kata dengan dictionary.

  • Simpan file WordFilterEngine.kt.
  • Periksa panel **Project** di Android Studio. Kamu seharusnya melihat file baru di path com.safetype.app.engine > WordFilterEngine.

File tidak muncul di Project panel?

Pastikan kamu membuat package engine di dalam com.safetype.app, bukan di level root. Cek juga bahwa baris pertama file adalah package com.safetype.app.engine.

Help me fix my Kotlin package structure in Android Studio

✔️ Awesome, I've got everything!

Bagus. Pastikan file sudah tersimpan dan lanjut ke langkah berikutnya.

ⓧ I'd like to double check the full code

package com.safetype.app.engine

data class FilterResult(
    val isDetected: Boolean,
    val detectedWord: String = ""
)

object WordFilterEngine {
    private val dictionary = setOf(
        "bodoh", "goblok", "tolol", "bangsat", "anjing", "bego",
        "idiot", "kampret", "bajingan", "brengsek", "sialan",
        "gila", "setan", "iblis", "laknat", "keparat",
        "monyet", "babi", "tai", "kontol", "memek",
        "jelek", "mati", "bunuh", "sampah", "busuk"
    )

    fun checkText(text: String): FilterResult {
        val words = text.lowercase().split(Regex("\\s+"))
        for (word in words) {
            val cleaned = word.replace(Regex("[^a-z]"), "")
            if (cleaned in dictionary) {
                return FilterResult(isDetected = true, detectedWord = cleaned)
            }
        }
        return FilterResult(isDetected = false)
    }
}
Wire the filter into SafeTypeIME

Engine sudah siap. Sekarang kamu perlu menghubungkannya ke SafeTypeIME supaya setiap keystroke memicu pengecekan.

  • Buka file SafeTypeIME.kt.
  • Tambahkan dua import baru di bagian atas file, di bawah import yang sudah ada:
import android.util.Log
import com.safetype.app.engine.WordFilterEngine
  • Tambahkan fungsi runWordFilter() berikut di bagian bawah class SafeTypeIME, setelah fungsi onSpace():
private fun runWordFilter() {
    val result = WordFilterEngine.checkText(textBuffer.value)
    isWarningActive.value = result.isDetected
    detectedWord.value = result.detectedWord
    if (result.isDetected) {
        Log.d("WordFilter", "detected ${result.detectedWord} in buffer")
    }
}

What does this code do?

runWordFilter() memanggil WordFilterEngine.checkText() dengan isi text buffer saat ini. Hasilnya langsung mengupdate state isWarningActive dan detectedWord.

Log.d() menulis pesan ke Logcat dengan tag WordFilter setiap kali kata terdeteksi. Ini memungkinkan kamu memverifikasi deteksi berjalan tanpa perlu UI banner (yang dibuat di step berikutnya).

Selanjutnya, kamu perlu memanggil runWordFilter() dari setiap fungsi yang mengubah text buffer.

  • Di dalam SafeTypeIME.kt, cari fungsi onKeyPress(). Temukan baris terakhir yang saat ini berbunyi:
textBuffer.value += key
  • Tambahkan runWordFilter() di baris baru tepat setelahnya, sehingga fungsi terlihat seperti ini:
fun onKeyPress(key: String) {
    val ic = currentInputConnection ?: return
    ic.commitText(key, 1)
    textBuffer.value += key
    runWordFilter()
}
  • Cari fungsi onBackspace(). Tambahkan runWordFilter() di baris terakhir sebelum closing brace, sehingga menjadi:
fun onBackspace() {
    val ic = currentInputConnection ?: return
    ic.deleteSurroundingText(1, 0)
    if (textBuffer.value.isNotEmpty()) {
        textBuffer.value = textBuffer.value.dropLast(1)
    }
    runWordFilter()
}
  • Cari fungsi onSpace(). Tambahkan runWordFilter() di baris terakhir sebelum closing brace, sehingga menjadi:
fun onSpace() {
    val ic = currentInputConnection ?: return
    ic.commitText(" ", 1)
    textBuffer.value += " "
    runWordFilter()
}
  • Simpan file SafeTypeIME.kt.

Why not onEnter()?

Fungsi onEnter() me-reset buffer ke string kosong dan langsung set isWarningActive ke false. Tidak perlu menjalankan filter karena buffer sudah dikosongkan.

Seeing an unresolved reference error?

Pastikan kedua import baru (android.util.Log dan com.safetype.app.engine.WordFilterEngine) sudah ditambahkan di bagian atas file. Jika Android Studio menampilkan garis merah pada WordFilterEngine, periksa bahwa package declaration di WordFilterEngine.kt adalah com.safetype.app.engine.

Help me fix unresolved reference errors in my SafeTypeIME.kt file

Test detection with Logcat

Saatnya membuktikan bahwa filter bekerja. Kamu akan menjalankan app, mengetik kata dari dictionary, dan melihat hasilnya di Logcat.

  • Klik tombol **Run** (ikon play hijau) di toolbar Android Studio untuk deploy app ke device fisik.
  • Buka panel **Logcat** di Android Studio: klik **View** di menu bar, lalu **Tool Windows**, lalu **Logcat**.
  • Di query field Logcat, ketik tag:WordFilter untuk memfilter hanya log dari tag kamu.

Sebelum kamu mengetik kata kasar di device, pikirkan dulu: menurutmu apa yang akan muncul di Logcat saat kamu mengetik kata "bodoh" menggunakan SafeType?

  • Di device fisik, buka app apapun yang memiliki text field (Notes, WhatsApp, atau browser).
  • Pastikan SafeType sudah aktif sebagai keyboard (switch keyboard jika perlu).
  • Ketik kata bodoh menggunakan keyboard SafeType.

Di Logcat Android Studio, kamu akan melihat output seperti ini:

D/WordFilter: detected bodoh in buffer
  • Coba juga ketik goblok atau tolol untuk mengkonfirmasi deteksi kata lain.
  • Ketik kata normal seperti halo atau selamat dan konfirmasi bahwa TIDAK ada log yang muncul.

Why Logcat but no UI change?

State isWarningActive sudah berubah menjadi true di belakang layar. Tapi kamu belum membuat Composable yang membaca state ini dan menampilkan banner. Logcat membuktikan engine berjalan tanpa bergantung pada UI yang belum ada.

Log tidak muncul di Logcat?

Periksa bahwa filter di Logcat query field adalah tag:WordFilter (case-sensitive). Pastikan juga device yang dipilih di dropdown Logcat sama dengan device yang kamu gunakan untuk mengetik.

Jika app crash saat mengetik, periksa bahwa import com.safetype.app.engine.WordFilterEngine ada di SafeTypeIME.kt dan tidak ada typo di nama package.

Help me debug why my WordFilter Logcat output isn't showing

✔️ Awesome, I've got everything!

Bagus. Pastikan SafeTypeIME.kt sudah tersimpan dan lanjut ke step berikutnya.

ⓧ I'd like to double check the full code

File SafeTypeIME.kt sekarang cukup panjang. Scroll ke atas dan konfirmasi kamu memiliki bagian-bagian berikut secara berurutan:

  • Import block termasuk android.util.Log dan com.safetype.app.engine.WordFilterEngine.
  • Class declaration SafeTypeIME dengan lifecycle fields dan state fields (textBuffer, isWarningActive, detectedWord).
  • Lifecycle overrides: onCreate, onCreateInputView, onStartInputView, onFinishInputView, onDestroy.
  • Input handlers: onKeyPress(), onBackspace(), onEnter(), onSpace() masing-masing diakhiri runWordFilter() (kecuali onEnter).
  • Private function runWordFilter() di bagian bawah class.

WordFilter engine sudah aktif dan membuktikan deteksi bekerja via Logcat. Step berikutnya, kamu akan membuat warning banner visual yang muncul langsung di atas keyboard saat kata terdeteksi.

Display the Warning Banner

WordFilter sudah berjalan di balik layar dan mendeteksi kata kasar yang diketik user. Tapi user sama sekali tidak tahu bahwa deteksi ini terjadi.

Tanpa feedback visual, fitur deteksi tidak berguna. Di step ini kamu akan membangun warning banner berwarna oranye yang muncul di atas keyboard saat kata kasar terdeteksi. Banner ini memberi dua pilihan: kirim pesan apa adanya, atau ubah pesan.

In this step, get ready to:
  • Create a WarningBanner composable with warning text and action buttons.
  • Add dismissWarning() and editMessage() handlers to SafeTypeIME.
  • Wire the banner into SafeTypeKeyboard with fade-in/fade-out animation.
Add action handlers to SafeTypeIME

Before building the banner UI, you need the methods that the banner buttons will call. One method dismisses the warning and lets the text through. The other selects all text in the input field so the user can retype their message.

  • In SafeTypeIME.kt, find the onSpace() function.
  • Add these two new functions directly below onSpace() by copying the following code:
fun dismissWarning() {
    // Clear warning state so banner disappears
    isWarningActive.value = false
    detectedWord.value = ""
}

fun editMessage() {
    // Select all text in the target app's input field
    val ic = currentInputConnection ?: return
    ic.performContextMenuAction(android.R.id.selectAll)
    isWarningActive.value = false
    detectedWord.value = ""
}

What do these methods do?

  • dismissWarning() resets both isWarningActive and detectedWord to empty state. This hides the banner and lets the message go through unchanged.
  • editMessage() uses performContextMenuAction(android.R.id.selectAll) to select all text in the target app's input field. This makes it easy for the user to delete or retype their message.
  • Save SafeTypeIME.kt.
  • Press Ctrl+F9 (Windows) or Cmd+F9 (macOS) to build. Confirm you see BUILD SUCCESSFUL in the Build output panel.

Seeing an unresolved reference error?

  • Make sure dismissWarning() and editMessage() are inside the SafeTypeIME class body, not outside it.
  • Check that editMessage() references currentInputConnection (inherited from InputMethodService).
  • Help me fix my SafeTypeIME build error.
Create the WarningBanner composable

Now build the visual component that the user actually sees. The banner uses an orange background to grab attention, displays which word was detected, and offers two action buttons.

  • In SafeTypeKeyboardView.kt, add these imports at the top of the file below the existing imports:
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.runtime.getValue
import androidx.compose.ui.text.style.TextAlign
  • Below the SafeTypeKeyboard function, add the WarningBanner composable. Start with the container and warning text:
@Composable
fun WarningBanner(
    detectedWord: String,
    onDismiss: () -> Unit,
    onEdit: () -> Unit
) {
    // Orange banner container that appears above the keyboard
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color(0xFFFFA726))
            .padding(12.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        // Warning message showing which word was detected
        Text(
            text = "Pesanmu mengandung kata \"$detectedWord\" yang bisa menyakiti.",
            color = Color.Black,
            fontSize = 13.sp,
            fontWeight = FontWeight.Medium,
            textAlign = TextAlign.Center
        )

        // Action buttons will go here
    }
}
  • Replace the // Action buttons will go here comment with the action buttons Row:
        // Action buttons row
        Row(
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = 8.dp),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            // "Send anyway" - dismisses the warning
            Button(
                onClick = onDismiss,
                colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF424242))
            ) {
                Text("Kirim Aja", fontSize = 12.sp)
            }

            // "Edit message" - selects all text for rewriting
            Button(
                onClick = onEdit,
                colors = ButtonDefaults.buttonColors(containerColor = Color(0xFF2E7D32))
            ) {
                Text("Ubah Pesan", fontSize = 12.sp)
            }
        }

What does WarningBanner do?

  • The Column uses Color(0xFFFFA726) as background. This is a Material Design orange that signals caution without being as aggressive as red.
  • The Text interpolates detectedWord into the warning message so the user knows exactly which word triggered the alert.
  • Two Button composables give the user agency. Dark grey for "Kirim Aja" (send anyway) and green for "Ubah Pesan" (edit message) create clear visual hierarchy.
  • Build the project with Ctrl+F9 (Windows) or Cmd+F9 (macOS) to confirm WarningBanner compiles without errors.

Build failing on WarningBanner?

  • Check that all 7 new imports are present at the top of SafeTypeKeyboardView.kt. Missing ButtonDefaults or TextAlign causes unresolved reference errors.
  • Confirm the closing braces match: WarningBanner has exactly one Column with one Text and one Row inside it.
  • Help me fix my WarningBanner compile error.
Wire the banner into SafeTypeKeyboard with animation

The banner composable exists, but nothing renders it yet. You need to update SafeTypeKeyboard to read the warning state from the service and conditionally display the banner with a smooth fade animation.

  • In SafeTypeKeyboardView.kt, find the existing SafeTypeKeyboard function. It currently looks like this:
@Composable
fun SafeTypeKeyboard(service: SafeTypeIME) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color(0xFF1B1B1F))
    ) {
        KeyboardLayout(service = service)
    }
}
  • Replace the entire SafeTypeKeyboard function with this updated version that reads state and shows the banner:
@Composable
fun SafeTypeKeyboard(service: SafeTypeIME) {
    // Read warning state from the IME service
    val isWarningActive by service.isWarningActive
    val detectedWord by service.detectedWord

    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(Color(0xFF1B1B1F))
    ) {
        // Show banner with fade animation when a word is detected
        AnimatedVisibility(
            visible = isWarningActive,
            enter = fadeIn(),
            exit = fadeOut()
        ) {
            WarningBanner(
                detectedWord = detectedWord,
                onDismiss = { service.dismissWarning() },
                onEdit = { service.editMessage() }
            )
        }

        KeyboardLayout(service = service)
    }
}

What changed in SafeTypeKeyboard?

  • val isWarningActive by service.isWarningActive uses Compose's by delegate to observe the mutable state. When the value changes, Compose automatically recomposes.
  • AnimatedVisibility wraps the banner so it fades in smoothly when a word is detected, and fades out when the warning clears. This feels polished rather than jarring.
  • The onDismiss lambda calls service.dismissWarning() and onEdit calls service.editMessage(). These connect the banner buttons to the handlers you wrote earlier.
  • Save SafeTypeKeyboardView.kt.
  • Run the app on your physical device by pressing Shift+F10 (Windows) or Ctrl+R (macOS).
  • Open any app with a text field (WhatsApp, Notes, or a browser search bar).
  • Make sure SafeType is your active keyboard.

Before you type, predict: what do you think will happen when you type "dasar bodoh" using the SafeType keyboard?

  • Type dasar bodoh using the SafeType keyboard.

An orange banner appears above the keyboard with the message: "Pesanmu mengandung kata "bodoh" yang bisa menyakiti." along with two buttons.

  • Tap Ubah Pesan. The text in the input field gets selected, ready for you to retype.
  • Delete the selected text and type something friendly instead. The banner fades away as soon as the offensive word is no longer in the buffer.
  • Type dasar bodoh again, then tap Kirim Aja. The banner dismisses and the text stays as-is.

Why does the banner disappear when you delete the word?

Every time you press backspace, onBackspace() calls runWordFilter(). When the offensive word is no longer in the text buffer, WordFilterEngine.checkText() returns isDetected = false. This sets isWarningActive to false, triggering AnimatedVisibility to fade the banner out automatically.

Banner not appearing when you type an offensive word?

  • Confirm SafeType is the active keyboard. If you switched apps, Android may have reverted to your default keyboard.
  • Check that the word you typed matches one in the dictionary exactly. The filter checks whole words separated by spaces, so "bodo" (incomplete) will not trigger, but "bodoh" (complete word followed by a space) will.
  • Make sure WarningBanner is placed above KeyboardLayout(service = service) inside the Column, not below it. The banner must render above the keys.
  • Help me debug why my warning banner isn't showing.

✔️ Awesome, I've got everything!

Great. Double check you have saved both SafeTypeIME.kt and SafeTypeKeyboardView.kt.

ⓧ I'd like to double check the full code

Your SafeTypeKeyboardView.kt is too long to show in one block. Scroll through your file and confirm these sections exist in order:

  • All imports at the top (including AnimatedVisibility, fadeIn, fadeOut, Button, ButtonDefaults, getValue, TextAlign).
  • SafeTypeKeyboardView class with Content() calling SafeTypeKeyboard(service = service).
  • SafeTypeKeyboard function reading isWarningActive and detectedWord via by delegate, with AnimatedVisibility wrapping WarningBanner above KeyboardLayout.
  • WarningBanner function with orange Column, Text showing the warning message, and Row containing "Kirim Aja" and "Ubah Pesan" buttons.
  • KeyboardLayout, KeyButton, and ActionKeyButton functions unchanged from the previous step.

For SafeTypeIME.kt, confirm that dismissWarning() and editMessage() exist between onSpace() and runWordFilter().

Warning banner berfungsi. Deteksi yang tadinya tersembunyi sekarang berubah menjadi intervensi visual yang nyata. Di step berikutnya, kamu akan menyimpan statistik deteksi ke database lokal dan menampilkannya sebagai BK-Notify dashboard.

Build the BK-Notify Dashboard with Room

WordFilter dan Sentimend warning sudah berjalan. Tapi saat ini, tidak ada cara bagi guru BK untuk melihat tren cyberbullying di kalangan siswa.

Dalam step ini, kamu membangun dashboard lokal yang menyimpan HANYA statistik agregat (tanggal + jumlah trigger) tanpa isi pesan. Ini membuktikan konsep privacy-preserving reporting untuk fitur BK-Notify.

In this step, get ready to:
  • Setup Room database dengan Entity, DAO, dan Database class.
  • Connect WordFilter ke database agar setiap deteksi tercatat otomatis.
  • Bangun companion dashboard activity yang menampilkan statistik deteksi.
Create the Room database layer

Room menyimpan data terstruktur di SQLite secara lokal. Kamu butuh tiga komponen: Entity (tabel), DAO (query), dan Database class (penghubung).

  • Di panel Project Android Studio, klik kanan pada package com.safetype.app.
  • Pilih New lalu Package.
  • Beri nama data lalu tekan Enter.

Sekarang buat file Entity yang merepresentasikan tabel penyimpanan deteksi.

  • Klik kanan pada package data, pilih New > Kotlin Class/File, dan beri nama DetectionLog.
  • Ganti seluruh isi file dengan kode berikut:
package com.safetype.app.data

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "detection_log")
data class DetectionLog(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val date: String,
    val triggerCount: Int
)

What does this code do?

DetectionLog adalah tabel Room yang menyimpan satu row per hari.

  • `@Entity(tableName = "detection_log")` memberitahu Room untuk membuat tabel bernama detection_log.
  • `date` menyimpan tanggal (format ISO string). triggerCount menyimpan berapa kali warning tampil hari itu.
  • Perhatikan: tidak ada kolom untuk isi pesan. Privacy-first design.
  • Buat file Kotlin baru di package data dengan nama DetectionLogDao.
  • Ganti seluruh isi file dengan kode berikut:
package com.safetype.app.data

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Dao
interface DetectionLogDao {
    @Query("SELECT * FROM detection_log WHERE date = :date LIMIT 1")
    suspend fun getLogByDate(date: String): DetectionLog?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertLog(log: DetectionLog)

    @Query("SELECT * FROM detection_log ORDER BY date DESC LIMIT 7")
    fun getLastSevenDays(): Flow<List<DetectionLog>>

    @Query("SELECT COALESCE(SUM(triggerCount), 0) FROM detection_log WHERE date = :date")
    fun getTodayCount(date: String): Flow<Int>
}

What does this code do?

DAO (Data Access Object) mendefinisikan semua query yang bisa dilakukan terhadap tabel.

  • `getLogByDate` mengambil satu row berdasarkan tanggal. suspend artinya harus dipanggil dari coroutine.
  • `insertLog` dengan OnConflictStrategy.REPLACE akan menimpa row jika ID sudah ada.
  • `getLastSevenDays` dan getTodayCount mengembalikan Flow agar UI otomatis update saat data berubah.
  • Pastikan kedua file DetectionLog.kt dan DetectionLogDao.kt sudah tersimpan (Cmd+S atau Ctrl+S).
  • Cek bahwa kedua file muncul di panel Project di bawah com.safetype.app.data.

Sekarang buat Database class dan Application singleton yang menginstansiasi database.

  • Buat file Kotlin baru di package data dengan nama SafeTypeDatabase.
  • Ganti seluruh isi file dengan kode berikut:
package com.safetype.app.data

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [DetectionLog::class], version = 1, exportSchema = false)
abstract class SafeTypeDatabase : RoomDatabase() {
    abstract fun detectionLogDao(): DetectionLogDao
}

What does this code do?

`SafeTypeDatabase` menghubungkan Entity dan DAO. Annotation @Database memberi tahu Room entities mana yang termasuk dalam database ini. Room menggunakan KSP untuk auto-generate implementasi DAO saat compile.

  • Sekarang buat file SafeTypeApplication.kt di package com.safetype.app (root package, bukan di data). Klik kanan pada com.safetype.app, pilih New > Kotlin Class/File.
  • Beri nama SafeTypeApplication dan ganti isi file dengan:
package com.safetype.app

import android.app.Application
import androidx.room.Room
import com.safetype.app.data.SafeTypeDatabase

class SafeTypeApplication : Application() {
    lateinit var database: SafeTypeDatabase
        private set

    override fun onCreate() {
        super.onCreate()
        database = Room.databaseBuilder(
            applicationContext,
            SafeTypeDatabase::class.java,
            "safetype_db"
        ).build()
    }
}

What does this code do?

`SafeTypeApplication` extends Application agar database di-instantiate sekali saat app pertama kali launch. Semua komponen (IME service, Activity) bisa mengakses database via application as SafeTypeApplication.

Database diberi nama safetype_db dan tersimpan di internal storage device. Hanya app ini yang bisa mengaksesnya.

  • Simpan semua file baru.
  • Klik Build > Make Project (atau tekan Cmd+F9 / Ctrl+F9) untuk memastikan Room KSP berhasil generate kode.

Build harus succeed tanpa error. Ini mengkonfirmasi Room KSP berhasil membaca Entity, DAO, dan Database class kamu.

Build error di Room?

  • Pastikan nama tabel di @Query DAO (detection_log) sama persis dengan tableName di @Entity.
  • Periksa bahwa SafeTypeDatabase mereferensikan DetectionLog::class di array entities.
  • Cek bahwa plugin com.google.devtools.ksp sudah ada di build.gradle.kts.

Masih error? Bantu saya debug Room database setup yang gagal compile.

Log detections from the keyboard service

Room sudah siap menerima data. Sekarang kamu perlu menghubungkan WordFilter ke database agar setiap deteksi kata kasar tercatat secara otomatis.

  • Buka file SafeTypeIME.kt di package com.safetype.app.ime.
  • Di bagian import, tambahkan import baru berikut di bawah import yang sudah ada:
import com.safetype.app.SafeTypeApplication
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import java.time.LocalDate
  • Di dalam class SafeTypeIME, cari baris ini:
    private val savedStateRegistryController = SavedStateRegistryController.create(this)
    override val savedStateRegistry: SavedStateRegistry
        get() = savedStateRegistryController.savedStateRegistry
  • Tambahkan baris berikut tepat di bawahnya (sebelum deklarasi textBuffer):
    private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)

Why a dedicated CoroutineScope?

Database operations harus berjalan di background thread. SupervisorJob() memastikan jika satu coroutine gagal, yang lain tetap jalan. Dispatchers.IO mengarahkan eksekusi ke thread pool yang dioptimasi untuk I/O.

Sekarang tambahkan fungsi yang mencatat deteksi ke database.

  • Scroll ke bagian bawah class SafeTypeIME, tepat di bawah fungsi runWordFilter(). Tambahkan fungsi baru:
    private fun logDetection() {
        val app = application as SafeTypeApplication
        val dao = app.database.detectionLogDao()
        val today = LocalDate.now().toString()

        serviceScope.launch {
            val existing = dao.getLogByDate(today)
            if (existing != null) {
                dao.insertLog(existing.copy(triggerCount = existing.triggerCount + 1))
            } else {
                dao.insertLog(
                    com.safetype.app.data.DetectionLog(date = today, triggerCount = 1)
                )
            }
        }
    }

What does this code do?

  • `application as SafeTypeApplication` mengakses database singleton yang kamu buat tadi.
  • `LocalDate.now().toString()` menghasilkan tanggal hari ini dalam format ISO (contoh: 2026-07-24).
  • Jika sudah ada entry untuk hari ini, increment triggerCount. Jika belum, buat entry baru dengan count 1.
  • Semua ini berjalan di serviceScope agar main thread tidak blocked.

Terakhir, hubungkan logDetection() ke WordFilter yang sudah ada.

  • Di dalam fungsi runWordFilter(), cari blok if (result.isDetected):
        if (result.isDetected) {
            Log.d("WordFilter", "detected ${result.detectedWord} in buffer")
        }
  • Tambahkan logDetection() di baris baru setelah Log.d, sehingga blok menjadi:
        if (result.isDetected) {
            Log.d("WordFilter", "detected ${result.detectedWord} in buffer")
            logDetection()
        }
  • Simpan SafeTypeIME.kt.
  • Klik Build > Make Project untuk memastikan tidak ada error.

Build berhasil. Setiap kali keyboard mendeteksi kata kasar, counter hari ini akan increment di Room database secara otomatis.

Unresolved reference: SafeTypeApplication?

  • Pastikan import com.safetype.app.SafeTypeApplication ada di bagian atas file.
  • Periksa bahwa SafeTypeApplication.kt berada di package com.safetype.app (root), bukan di com.safetype.app.data.

Masih stuck? Bantu saya fix unresolved reference di SafeTypeIME.

Build the companion dashboard activity

Data sudah mengalir ke Room. Sekarang kamu butuh UI untuk menampilkannya. Dashboard ini yang akan di-demo ke juri sebagai proof-of-concept BK-Notify.

  • Di panel Project, klik kanan pada package com.safetype.app. Pilih New > Package dan beri nama ui.
  • Buat file Kotlin baru di package ui dengan nama DashboardActivity.
  • Ganti seluruh isi file. Pertama, tambahkan package dan imports:
package com.safetype.app.ui

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.safetype.app.SafeTypeApplication
import com.safetype.app.data.DetectionLog
import java.time.LocalDate

Why so many imports?

Jetpack Compose menggunakan modul kecil per-komponen. Setiap Modifier, layout (Column, Row, Box), dan material component (Card, Surface) perlu import masing-masing. Android Studio bisa auto-import, tapi menulisnya manual memastikan tidak ada yang terlewat.

  • Di bawah imports, tambahkan class DashboardActivity:
class DashboardActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        val app = application as SafeTypeApplication
        val dao = app.database.detectionLogDao()
        val today = LocalDate.now().toString()

        setContent {
            MaterialTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    val todayCount by dao.getTodayCount(today).collectAsState(initial = 0)
                    val weekLogs by dao.getLastSevenDays().collectAsState(initial = emptyList())

                    DashboardScreen(
                        todayCount = todayCount,
                        weekLogs = weekLogs
                    )
                }
            }
        }
    }
}

What does this code do?

  • Activity mengakses database via SafeTypeApplication singleton (pattern yang sama dengan IME service).
  • `getTodayCount` dan getLastSevenDays mengembalikan Flow. Dengan collectAsState, Compose otomatis re-render saat data berubah.
  • UI akan update secara real-time saat kamu mengetik kata kasar di keyboard dan membuka dashboard kembali.

Sekarang tambahkan composable DashboardScreen yang merender tampilan utama dashboard.

  • Di bawah class DashboardActivity, tambahkan bagian pertama DashboardScreen yang berisi judul dan card "Deteksi Hari Ini":
@Composable
fun DashboardScreen(todayCount: Int, weekLogs: List<DetectionLog>) {
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp)
    ) {
        Text(
            text = "SafeType - BK Dashboard",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold,
            modifier = Modifier.padding(bottom = 16.dp)
        )

        Card(
            modifier = Modifier.fillMaxWidth(),
            colors = CardDefaults.cardColors(containerColor = Color(0xFFFFF3E0))
        ) {
            Column(modifier = Modifier.padding(16.dp)) {
                Text(
                    text = "Deteksi Hari Ini",
                    fontSize = 14.sp,
                    color = Color.Gray
                )
                Text(
                    text = "$todayCount",
                    fontSize = 48.sp,
                    fontWeight = FontWeight.Bold,
                    color = Color(0xFFE65100)
                )
            }
        }

What does this code do?

Bagian ini menampilkan heading "SafeType - BK Dashboard" dan card besar berwarna orange muda yang menunjukkan berapa kali word filter aktif hari ini. Angka ditampilkan besar (48sp) agar mudah dibaca saat demo.

  • Lanjutkan di dalam blok Column yang sama. Tambahkan bagian chart dan privacy badge berikut tepat setelah Card yang baru ditutup:
        Spacer(modifier = Modifier.height(24.dp))

        Text(
            text = "7 Hari Terakhir",
            fontSize = 16.sp,
            fontWeight = FontWeight.SemiBold,
            modifier = Modifier.padding(bottom = 8.dp)
        )

        WeekChart(logs = weekLogs)

        Spacer(modifier = Modifier.height(24.dp))

        Card(
            modifier = Modifier.fillMaxWidth(),
            colors = CardDefaults.cardColors(containerColor = Color(0xFFE8F5E9))
        ) {
            Text(
                text = "Privasi Terjaga: 0 pesan tersimpan",
                modifier = Modifier.padding(16.dp),
                fontSize = 14.sp,
                fontWeight = FontWeight.Medium,
                color = Color(0xFF2E7D32)
            )
        }
    }
}

What does this code do?

  • `WeekChart` (yang akan kamu buat berikutnya) menampilkan bar chart 7 hari terakhir.
  • Card hijau di bawah adalah privacy badge. Text "Privasi Terjaga: 0 pesan tersimpan" menegaskan ke juri bahwa tidak ada pesan yang disimpan.
  • Ini closing statement yang kuat untuk demo lomba.
  • Terakhir, tambahkan composable WeekChart di bawah fungsi DashboardScreen:
@Composable
fun WeekChart(logs: List<DetectionLog>) {
    val maxCount = logs.maxOfOrNull { it.triggerCount } ?: 1

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .height(120.dp),
        horizontalArrangement = Arrangement.SpaceEvenly,
        verticalAlignment = Alignment.Bottom
    ) {
        logs.forEach { log ->
            val heightFraction = if (maxCount > 0) log.triggerCount.toFloat() / maxCount else 0f

            Column(horizontalAlignment = Alignment.CenterHorizontally) {
                Box(
                    modifier = Modifier
                        .width(28.dp)
                        .height((100 * heightFraction).dp.coerceAtLeast(4.dp))
                        .clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
                        .background(Color(0xFFFFA726))
                )
                Text(
                    text = log.date.takeLast(2),
                    fontSize = 10.sp,
                    textAlign = TextAlign.Center
                )
            }
        }
    }
}

What does this code do?

WeekChart menampilkan bar chart sederhana tanpa library eksternal.

  • `maxCount` mencari hari dengan trigger tertinggi. Setiap bar dihitung proporsional terhadap max ini.
  • Setiap Box berwarna orange menjadi satu bar. Height-nya dinamis berdasarkan heightFraction.
  • Label di bawah bar menampilkan 2 digit terakhir tanggal (contoh: "24" untuk tanggal 24).
  • Simpan DashboardActivity.kt.
  • Klik Build > Make Project untuk memastikan semua composable compile tanpa error.

Build berhasil. Dashboard activity siap dijalankan.

Unresolved reference di DashboardActivity?

  • Periksa bahwa semua import di bagian atas file sudah lengkap (terutama import com.safetype.app.data.DetectionLog dan import com.safetype.app.SafeTypeApplication).
  • Pastikan file berada di package com.safetype.app.ui sesuai package declaration di baris pertama.
  • Jika DashboardScreen atau WeekChart unresolved, pastikan kedua fungsi ada di file yang sama di bawah class DashboardActivity.

Masih error? Bantu saya fix compilation error di DashboardActivity.

✔️ Awesome, I've got everything!

Pastikan kamu sudah save semua file. Selanjutnya kamu akan run app dan verifikasi dashboard berjalan di device fisik.

ⓧ I'd like to double check the full code

File DashboardActivity.kt terdiri dari 3 bagian yang kamu tulis di atas, dalam urutan ini:

  • Package declaration + all imports.
  • Class DashboardActivity (onCreate with setContent).
  • Composable DashboardScreen (title + today card + chart section + privacy badge).
  • Composable WeekChart (bar chart with proportional heights).

Scroll ke atas dan pastikan setiap bagian ada dalam urutan yang benar. Untuk file Room yang lebih pendek, berikut referensinya:

package com.safetype.app.data

import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity(tableName = "detection_log")
data class DetectionLog(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val date: String,
    val triggerCount: Int
)
package com.safetype.app.data

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow

@Dao
interface DetectionLogDao {
    @Query("SELECT * FROM detection_log WHERE date = :date LIMIT 1")
    suspend fun getLogByDate(date: String): DetectionLog?

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertLog(log: DetectionLog)

    @Query("SELECT * FROM detection_log ORDER BY date DESC LIMIT 7")
    fun getLastSevenDays(): Flow<List<DetectionLog>>

    @Query("SELECT COALESCE(SUM(triggerCount), 0) FROM detection_log WHERE date = :date")
    fun getTodayCount(date: String): Flow<Int>
}
package com.safetype.app.data

import androidx.room.Database
import androidx.room.RoomDatabase

@Database(entities = [DetectionLog::class], version = 1, exportSchema = false)
abstract class SafeTypeDatabase : RoomDatabase() {
    abstract fun detectionLogDao(): DetectionLogDao
}
package com.safetype.app

import android.app.Application
import androidx.room.Room
import com.safetype.app.data.SafeTypeDatabase

class SafeTypeApplication : Application() {
    lateinit var database: SafeTypeDatabase
        private set

    override fun onCreate() {
        super.onCreate()
        database = Room.databaseBuilder(
            applicationContext,
            SafeTypeDatabase::class.java,
            "safetype_db"
        ).build()
    }
}

Saatnya verifikasi end-to-end. Sebelum run, menurutmu apa yang akan muncul di dashboard jika kamu belum mengetik kata kasar apapun?

  • Klik Run (ikon play hijau) untuk install app ke device fisik.
  • App akan langsung membuka DashboardActivity karena ini adalah launcher activity. Kamu akan melihat: judul "SafeType - BK Dashboard", card orange dengan angka "0" (belum ada deteksi), area chart kosong, dan badge hijau "Privasi Terjaga: 0 pesan tersimpan".
  • Sekarang buka app lain (WhatsApp, Notes, atau browser) dan ketik "dasar bodoh" menggunakan keyboard SafeType.
  • Warning banner akan muncul. Tap Kirim Aja atau Ubah Pesan.
  • Kembali ke app SafeType dari launcher. Angka di card "Deteksi Hari Ini" sekarang menunjukkan jumlah deteksi yang kamu trigger.

Dashboard menampilkan statistik real-time tanpa menyimpan satu pun isi pesanmu. Privacy-preserving BK-Notify sudah berjalan.

Dashboard masih menunjukkan 0?

  • Pastikan kamu menggunakan keyboard SafeType (bukan Gboard) saat mengetik. Cek di notification bar bahwa SafeType aktif.
  • Pastikan android:name=".SafeTypeApplication" ada di tag <application> di AndroidManifest.xml. Tanpa ini, database singleton tidak ter-instantiate.
  • Cek Logcat dengan filter "WordFilter". Jika log muncul tapi dashboard tetap 0, kemungkinan logDetection() tidak terpanggil. Pastikan baris logDetection() ada di dalam blok if (result.isDetected).

Masih bermasalah? Bantu saya debug kenapa dashboard SafeType tidak menampilkan data deteksi.

Keyboard SafeType sekarang punya sistem reporting lengkap. Deteksi berjalan real-time, data tersimpan lokal tanpa isi pesan, dan dashboard siap di-demo. Selanjutnya, Secret Mission menambahkan Cool Down Mode yang memaksa jeda sebelum pesan kasar bisa dikirim.

Secret mission

Cool Down Mode

Saat ini, warning banner bisa di-dismiss dengan satu tap "Kirim Aja". Terlalu mudah. Bisakah kamu menambahkan friction berupa countdown 5 detik yang memaksa user berpikir ulang sebelum mengirim pesan kasar?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely on your Android device, so there are no ongoing cloud costs.

Resources you used:

  • Room database safetype_db stored in the app's internal storage on your Android device.
  • SafeType keyboard registration in your device's system Settings.

Keep everything running

No action needed. Choose this if you want to keep SafeType installed on your device for a demo or competition presentation.

  • SafeType stays registered as a keyboard in your system Settings.
  • The Room database continues storing detection statistics locally.
  • No data leaves your device. There is nothing running in the background that costs money or consumes significant resources.

Pause - I'll come back to this later

There are no running processes or cloud services to pause. SafeType only activates when you select it as your keyboard.

  • If you want to stop using SafeType temporarily, switch back to your default keyboard (like Gboard) in Settings > System > Languages & input > On-screen keyboard.
  • The app and its data remain on your device, ready for when you come back.

Delete - I don't want to use this again

Remove all project resources from your device and start fresh.

  • On your Android device, open Settings > Apps.
  • Find SafeType in the app list and tap it.
  • Tap Uninstall and confirm.
  • This removes the app, the safetype_db Room database, and automatically deregisters the keyboard from your system Settings.
  • To remove the project source code from your computer, delete the project folder from Android Studio or run this in your terminal:
rm -rf ~/Desktop/SafeType

On Windows, use:

Remove-Item -Recurse -Force ~\Desktop\SafeType

Nice Work!

Nice Work!

You just built SafeType, a fully functional custom Android keyboard that detects cyberbullying in real-time and intervenes before harmful messages get sent.

You've learned how to:

  • Build a custom Android keyboard from scratch using InputMethodService and Jetpack Compose, complete with lifecycle bridging and input connection handling.
  • Implement a real-time WordFilter that scans typed text against a local dictionary of harmful words without ever storing the actual message content.
  • Store anonymous aggregate statistics in a Room database and display them in a privacy-preserving BK-Notify dashboard with reactive Flow-based UI updates.
  • Secret Mission: You added a Cool Down Mode with a 5-second countdown timer that forces a pause before harmful messages can be sent.

Ready to quiz yourself?