package com.example.debughelper // use any package, for example your app's own import android.content.ContentProvider import android.content.ContentValues import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.net.Uri import android.os.Bundle import android.util.Base64 import org.json.JSONArray /** * DEBUG BUILDS ONLY. * * Lets SQLite Viewer (https://sqliteviewer.codemasterlk.com) edit this app's databases * while the app keeps running. The class can live in src/main, but it is only registered * in src/debug/AndroidManifest.xml, so release builds never expose it. */ class SqliteViewerProvider : ContentProvider() { companion object { /** Optional: called after every successful live edit, e.g. to refresh Room observers. */ @JvmStatic var afterWrite: (() -> Unit)? = null private val allowedStatement = Regex("""^(INSERT|UPDATE|DELETE|REPLACE|CREATE|DROP|ALTER)\b""", RegexOption.IGNORE_CASE) private val leadingNoise = Regex("""^(\s|--[^\n]*\n|/\*.*?\*/)*""", RegexOption.DOT_MATCHES_ALL) private val safeName = Regex("""^[A-Za-z0-9._-]+$""") } private val handles = HashMap() override fun onCreate(): Boolean = true override fun call(method: String, arg: String?, extras: Bundle?): Bundle { val answer = try { when (method) { "ping" -> "OK:1" "batch" -> "OK:" + runBatch(extras?.getString("db"), arg) else -> "ERR:unknown method $method" } } catch (e: Throwable) { "ERR:" + (e.message ?: e.javaClass.simpleName).replace('\n', ' ') } return Bundle().apply { putString("r", answer) } } @Synchronized private fun database(name: String): SQLiteDatabase { handles[name]?.let { if (it.isOpen) return it } val file = context!!.getDatabasePath(name) require(file.exists()) { "database not found: $name" } return SQLiteDatabase.openDatabase(file.path, null, SQLiteDatabase.OPEN_READWRITE) .also { handles[name] = it } } private fun runBatch(name: String?, base64: String?): Int { require(name != null && safeName.matches(name)) { "invalid database name" } require(!base64.isNullOrEmpty()) { "no statements" } val statements = JSONArray(String(Base64.decode(base64, Base64.DEFAULT), Charsets.UTF_8)) val db = database(name) db.beginTransaction() try { for (i in 0 until statements.length()) { val sql = statements.getString(i) require(allowedStatement.containsMatchIn(sql.replaceFirst(leadingNoise, ""))) { "statement type not allowed" } db.execSQL(sql) } db.setTransactionSuccessful() } finally { db.endTransaction() } afterWrite?.invoke() return statements.length() } // Not used: the viewer talks to this provider only through call(). override fun query(uri: Uri, projection: Array?, selection: String?, selectionArgs: Array?, sortOrder: String?): Cursor? = null override fun getType(uri: Uri): String? = null override fun insert(uri: Uri, values: ContentValues?): Uri? = null override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int = 0 override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0 }