Add a ColumnTransformer system to Spinner.

This commit is contained in:
Greyson Parrelli 2022-02-23 18:22:58 -05:00 committed by Alex Hart
parent 935dd7de45
commit f4002850bb
9 changed files with 215 additions and 163 deletions

View file

@ -4,12 +4,14 @@ import android.content.ContentValues
import android.os.Build
import leakcanary.LeakCanary
import org.signal.spinner.Spinner
import org.signal.spinner.Spinner.DatabaseConfig
import org.thoughtcrime.securesms.database.DatabaseMonitor
import org.thoughtcrime.securesms.database.JobDatabase
import org.thoughtcrime.securesms.database.KeyValueDatabase
import org.thoughtcrime.securesms.database.LocalMetricsDatabase
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.database.MegaphoneDatabase
import org.thoughtcrime.securesms.database.MessageBitmaskColumnTransformer
import org.thoughtcrime.securesms.database.QueryMonitor
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.util.AppSignatureUtil
@ -27,12 +29,15 @@ class SpinnerApplicationContext : ApplicationContext() {
appVersion = "${BuildConfig.VERSION_NAME} (${BuildConfig.CANONICAL_VERSION_CODE}, ${BuildConfig.GIT_HASH})"
),
linkedMapOf(
"signal" to SignalDatabase.rawDatabase,
"jobmanager" to JobDatabase.getInstance(this).sqlCipherDatabase,
"keyvalue" to KeyValueDatabase.getInstance(this).sqlCipherDatabase,
"megaphones" to MegaphoneDatabase.getInstance(this).sqlCipherDatabase,
"localmetrics" to LocalMetricsDatabase.getInstance(this).sqlCipherDatabase,
"logs" to LogDatabase.getInstance(this).sqlCipherDatabase,
"signal" to DatabaseConfig(
db = SignalDatabase.rawDatabase,
columnTransformers = listOf(MessageBitmaskColumnTransformer)
),
"jobmanager" to DatabaseConfig(db = JobDatabase.getInstance(this).sqlCipherDatabase),
"keyvalue" to DatabaseConfig(db = KeyValueDatabase.getInstance(this).sqlCipherDatabase),
"megaphones" to DatabaseConfig(db = MegaphoneDatabase.getInstance(this).sqlCipherDatabase),
"localmetrics" to DatabaseConfig(db = LocalMetricsDatabase.getInstance(this).sqlCipherDatabase),
"logs" to DatabaseConfig(db = LogDatabase.getInstance(this).sqlCipherDatabase),
)
)

View file

@ -0,0 +1,122 @@
package org.thoughtcrime.securesms.database
import android.database.Cursor
import org.signal.spinner.ColumnTransformer
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BAD_DECRYPT_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_DRAFT_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_INBOX_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_OUTBOX_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_PENDING_INSECURE_SMS_FALLBACK
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_PENDING_SECURE_SMS_FALLBACK
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_SENDING_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_SENT_FAILED_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_SENT_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BASE_TYPE_MASK
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.BOOST_REQUEST_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.CHANGE_NUMBER_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_DUPLICATE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_FAILED_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_LEGACY_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.ENCRYPTION_REMOTE_NO_SESSION_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.END_SESSION_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.EXPIRATION_TIMER_UPDATE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_CALL_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_LEAVE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_UPDATE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_V2_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.GV1_MIGRATION_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.INCOMING_AUDIO_CALL_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.INCOMING_VIDEO_CALL_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.INVALID_MESSAGE_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.JOINED_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_BUNDLE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_CONTENT_FORMAT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_CORRUPTED_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_IDENTITY_DEFAULT_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_IDENTITY_UPDATE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_IDENTITY_VERIFIED_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.KEY_EXCHANGE_INVALID_VERSION_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.MESSAGE_FORCE_SMS_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.MESSAGE_RATE_LIMITED_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.MISSED_AUDIO_CALL_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.MISSED_VIDEO_CALL_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.OUTGOING_AUDIO_CALL_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.OUTGOING_MESSAGE_TYPES
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.OUTGOING_VIDEO_CALL_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PROFILE_CHANGE_TYPE
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PUSH_MESSAGE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SECURE_MESSAGE_BIT
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.UNSUPPORTED_MESSAGE_TYPE
object MessageBitmaskColumnTransformer : ColumnTransformer {
override fun matches(tableName: String?, columnName: String): Boolean {
return columnName == "type" || columnName == "msg_box"
}
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
val type = cursor.requireLong(columnName)
val describe = """
isOutgoingMessageType:${isOutgoingMessageType(type)}
isForcedSms:${type and MESSAGE_FORCE_SMS_BIT != 0L}
isDraftMessageType:${type and BASE_TYPE_MASK == BASE_DRAFT_TYPE}
isFailedMessageType:${type and BASE_TYPE_MASK == BASE_SENT_FAILED_TYPE}
isPendingMessageType:${type and BASE_TYPE_MASK == BASE_OUTBOX_TYPE || type and BASE_TYPE_MASK == BASE_SENDING_TYPE }
isSentType:${type and BASE_TYPE_MASK == BASE_SENT_TYPE}
isPendingSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_INSECURE_SMS_FALLBACK || type and BASE_TYPE_MASK == BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingSecureSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingInsecureSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_INSECURE_SMS_FALLBACK}
isInboxType:${type and BASE_TYPE_MASK == BASE_INBOX_TYPE}
isJoinedType:${type and BASE_TYPE_MASK == JOINED_TYPE}
isUnsupportedMessageType:${type and BASE_TYPE_MASK == UNSUPPORTED_MESSAGE_TYPE}
isInvalidMessageType:${type and BASE_TYPE_MASK == INVALID_MESSAGE_TYPE}
isBadDecryptType:${type and BASE_TYPE_MASK == BAD_DECRYPT_TYPE}
isSecureType:${type and SECURE_MESSAGE_BIT != 0L}
isPushType:${type and PUSH_MESSAGE_BIT != 0L}
isEndSessionType:${type and END_SESSION_BIT != 0L}
isKeyExchangeType:${type and KEY_EXCHANGE_BIT != 0L}
isIdentityVerified:${type and KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
isIdentityDefault:${type and KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
isCorruptedKeyExchange:${type and KEY_EXCHANGE_CORRUPTED_BIT != 0L}
isInvalidVersionKeyExchange:${type and KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
isBundleKeyExchange:${type and KEY_EXCHANGE_BUNDLE_BIT != 0L}
isContentBundleKeyExchange:${type and KEY_EXCHANGE_CONTENT_FORMAT != 0L}
isIdentityUpdate:${type and KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
isRateLimited:${type and MESSAGE_RATE_LIMITED_BIT != 0L}
isExpirationTimerUpdate:${type and EXPIRATION_TIMER_UPDATE_BIT != 0L}
isIncomingAudioCall:${type == INCOMING_AUDIO_CALL_TYPE}
isIncomingVideoCall:${type == INCOMING_VIDEO_CALL_TYPE}
isOutgoingAudioCall:${type == OUTGOING_AUDIO_CALL_TYPE}
isOutgoingVideoCall:${type == OUTGOING_VIDEO_CALL_TYPE}
isMissedAudioCall:${type == MISSED_AUDIO_CALL_TYPE}
isMissedVideoCall:${type == MISSED_VIDEO_CALL_TYPE}
isGroupCall:${type == GROUP_CALL_TYPE}
isGroupUpdate:${type and GROUP_UPDATE_BIT != 0L}
isGroupV2:${type and GROUP_V2_BIT != 0L}
isGroupQuit:${type and GROUP_LEAVE_BIT != 0L && type and GROUP_V2_BIT == 0L}
isChatSessionRefresh:${type and ENCRYPTION_REMOTE_FAILED_BIT != 0L}
isDuplicateMessageType:${type and ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L}
isDecryptInProgressType:${type and 0x40000000 != 0L}
isNoRemoteSessionType:${type and ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L}
isLegacyType:${type and ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and ENCRYPTION_REMOTE_BIT != 0L}
isProfileChange:${type == PROFILE_CHANGE_TYPE}
isGroupV1MigrationEvent:${type == GV1_MIGRATION_TYPE}
isChangeNumber:${type == CHANGE_NUMBER_TYPE}
isBoostRequest:${type == BOOST_REQUEST_TYPE}
isGroupV2LeaveOnly:${type and GROUP_V2_LEAVE_BITS == GROUP_V2_LEAVE_BITS}
""".trimIndent()
return "$type<br><br>" + describe.replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "<br>")
}
private fun isOutgoingMessageType(type: Long): Boolean {
for (outgoingType in OUTGOING_MESSAGE_TYPES) {
if (type and BASE_TYPE_MASK == outgoingType) return true
}
return false
}
}

View file

@ -18,13 +18,13 @@ class MainActivity : AppCompatActivity() {
// insertMockData(db.writableDatabase)
Spinner.init(
this,
application,
Spinner.DeviceInfo(
name = "${Build.MODEL} (API ${Build.VERSION.SDK_INT})",
packageName = packageName,
appVersion = "0.1"
),
mapOf("main" to db)
mapOf("main" to Spinner.DatabaseConfig(db = db))
)
}

View file

@ -47,7 +47,7 @@
{{#each queryResult.rows}}
<tr>
{{#each this}}
<td>{{this}}</td>
<td>{{{this}}}</td>
{{/each}}
</tr>
{{/each}}

View file

@ -0,0 +1,18 @@
package org.signal.spinner
import android.database.Cursor
/**
* An interface to transform on column value into another. Useful for making certain data fields (like bitmasks) more readable.
*/
interface ColumnTransformer {
/**
* In certain circumstances (like some queries), the table name may not be guaranteed.
*/
fun matches(tableName: String?, columnName: String): Boolean
/**
* In certain circumstances (like some queries), the table name may not be guaranteed.
*/
fun transform(tableName: String?, columnName: String, cursor: Cursor): String
}

View file

@ -0,0 +1,20 @@
package org.signal.spinner
import android.database.Cursor
import android.util.Base64
internal object DefaultColumnTransformer : ColumnTransformer {
override fun matches(tableName: String?, columnName: String): Boolean {
return true
}
override fun transform(tableName: String?, columnName: String, cursor: Cursor): String {
val index = cursor.getColumnIndex(columnName)
val data: String? = when (cursor.getType(index)) {
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(cursor.getBlob(index), 0)
else -> cursor.getString(index)
}
return data ?: "null"
}
}

View file

@ -1,117 +0,0 @@
package org.signal.spinner
object MessageUtil {
private const val BASE_TYPE_MASK: Long = 0x1F
private const val INCOMING_AUDIO_CALL_TYPE: Long = 1
private const val OUTGOING_AUDIO_CALL_TYPE: Long = 2
private const val MISSED_AUDIO_CALL_TYPE: Long = 3
private const val JOINED_TYPE: Long = 4
private const val UNSUPPORTED_MESSAGE_TYPE: Long = 5
private const val INVALID_MESSAGE_TYPE: Long = 6
private const val PROFILE_CHANGE_TYPE: Long = 7
private const val MISSED_VIDEO_CALL_TYPE: Long = 8
private const val GV1_MIGRATION_TYPE: Long = 9
private const val INCOMING_VIDEO_CALL_TYPE: Long = 10
private const val OUTGOING_VIDEO_CALL_TYPE: Long = 11
private const val GROUP_CALL_TYPE: Long = 12
private const val BAD_DECRYPT_TYPE: Long = 13
private const val CHANGE_NUMBER_TYPE: Long = 14
private const val BOOST_REQUEST_TYPE: Long = 15
private const val BASE_INBOX_TYPE: Long = 20
private const val BASE_OUTBOX_TYPE: Long = 21
private const val outgoingSmsMessageType: Long = 22
private const val BASE_SENT_TYPE: Long = 23
private const val BASE_SENT_FAILED_TYPE: Long = 24
private const val BASE_PENDING_SECURE_SMS_FALLBACK: Long = 25
private const val BASE_PENDING_INSECURE_SMS_FALLBACK: Long = 26
private const val BASE_DRAFT_TYPE: Long = 27
private val OUTGOING_MESSAGE_TYPES = longArrayOf(BASE_OUTBOX_TYPE, BASE_SENT_TYPE, outgoingSmsMessageType, BASE_SENT_FAILED_TYPE, BASE_PENDING_SECURE_SMS_FALLBACK, BASE_PENDING_INSECURE_SMS_FALLBACK, OUTGOING_AUDIO_CALL_TYPE, OUTGOING_VIDEO_CALL_TYPE)
private const val MESSAGE_RATE_LIMITED_BIT: Long = 0x80
private const val MESSAGE_FORCE_SMS_BIT: Long = 0x40
private const val KEY_EXCHANGE_BIT: Long = 0x8000
private const val KEY_EXCHANGE_IDENTITY_VERIFIED_BIT: Long = 0x4000
private const val KEY_EXCHANGE_IDENTITY_DEFAULT_BIT: Long = 0x2000
private const val KEY_EXCHANGE_CORRUPTED_BIT: Long = 0x1000
private const val KEY_EXCHANGE_INVALID_VERSION_BIT: Long = 0x800
private const val KEY_EXCHANGE_BUNDLE_BIT: Long = 0x400
private const val KEY_EXCHANGE_IDENTITY_UPDATE_BIT: Long = 0x200
private const val KEY_EXCHANGE_CONTENT_FORMAT: Long = 0x100
private const val SECURE_MESSAGE_BIT: Long = 0x800000
private const val END_SESSION_BIT: Long = 0x400000
private const val PUSH_MESSAGE_BIT: Long = 0x200000
private const val GROUP_UPDATE_BIT: Long = 0x10000
private const val GROUP_LEAVE_BIT: Long = 0x20000
private const val EXPIRATION_TIMER_UPDATE_BIT: Long = 0x40000
private const val GROUP_V2_BIT: Long = 0x80000
private const val GROUP_V2_LEAVE_BITS = GROUP_V2_BIT or GROUP_LEAVE_BIT or GROUP_UPDATE_BIT
private const val ENCRYPTION_REMOTE_BIT: Long = 0x20000000
private const val ENCRYPTION_REMOTE_FAILED_BIT: Long = 0x10000000
private const val ENCRYPTION_REMOTE_NO_SESSION_BIT: Long = 0x08000000
private const val ENCRYPTION_REMOTE_DUPLICATE_BIT: Long = 0x04000000
private const val ENCRYPTION_REMOTE_LEGACY_BIT: Long = 0x02000000
fun String.isMessageType(): Boolean {
return this == "type" || this == "msg_box"
}
private fun isOutgoingMessageType(type: Long): Boolean {
for (outgoingType in OUTGOING_MESSAGE_TYPES) {
if (type and BASE_TYPE_MASK == outgoingType) return true
}
return false
}
fun describeMessageType(type: Long): String {
val describe = """
isOutgoingMessageType:${isOutgoingMessageType(type)}
isForcedSms:${type and MESSAGE_FORCE_SMS_BIT != 0L}
isDraftMessageType:${type and BASE_TYPE_MASK == BASE_DRAFT_TYPE}
isFailedMessageType:${type and BASE_TYPE_MASK == BASE_SENT_FAILED_TYPE}
isPendingMessageType:${type and BASE_TYPE_MASK == BASE_OUTBOX_TYPE || type and BASE_TYPE_MASK == outgoingSmsMessageType}
isSentType:${type and BASE_TYPE_MASK == BASE_SENT_TYPE}
isPendingSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_INSECURE_SMS_FALLBACK || type and BASE_TYPE_MASK == BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingSecureSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_SECURE_SMS_FALLBACK}
isPendingInsecureSmsFallbackType:${type and BASE_TYPE_MASK == BASE_PENDING_INSECURE_SMS_FALLBACK}
isInboxType:${type and BASE_TYPE_MASK == BASE_INBOX_TYPE}
isJoinedType:${type and BASE_TYPE_MASK == JOINED_TYPE}
isUnsupportedMessageType:${type and BASE_TYPE_MASK == UNSUPPORTED_MESSAGE_TYPE}
isInvalidMessageType:${type and BASE_TYPE_MASK == INVALID_MESSAGE_TYPE}
isBadDecryptType:${type and BASE_TYPE_MASK == BAD_DECRYPT_TYPE}
isSecureType:${type and SECURE_MESSAGE_BIT != 0L}
isPushType:${type and PUSH_MESSAGE_BIT != 0L}
isEndSessionType:${type and END_SESSION_BIT != 0L}
isKeyExchangeType:${type and KEY_EXCHANGE_BIT != 0L}
isIdentityVerified:${type and KEY_EXCHANGE_IDENTITY_VERIFIED_BIT != 0L}
isIdentityDefault:${type and KEY_EXCHANGE_IDENTITY_DEFAULT_BIT != 0L}
isCorruptedKeyExchange:${type and KEY_EXCHANGE_CORRUPTED_BIT != 0L}
isInvalidVersionKeyExchange:${type and KEY_EXCHANGE_INVALID_VERSION_BIT != 0L}
isBundleKeyExchange:${type and KEY_EXCHANGE_BUNDLE_BIT != 0L}
isContentBundleKeyExchange:${type and KEY_EXCHANGE_CONTENT_FORMAT != 0L}
isIdentityUpdate:${type and KEY_EXCHANGE_IDENTITY_UPDATE_BIT != 0L}
isRateLimited:${type and MESSAGE_RATE_LIMITED_BIT != 0L}
isExpirationTimerUpdate:${type and EXPIRATION_TIMER_UPDATE_BIT != 0L}
isIncomingAudioCall:${type == INCOMING_AUDIO_CALL_TYPE}
isIncomingVideoCall:${type == INCOMING_VIDEO_CALL_TYPE}
isOutgoingAudioCall:${type == OUTGOING_AUDIO_CALL_TYPE}
isOutgoingVideoCall:${type == OUTGOING_VIDEO_CALL_TYPE}
isMissedAudioCall:${type == MISSED_AUDIO_CALL_TYPE}
isMissedVideoCall:${type == MISSED_VIDEO_CALL_TYPE}
isGroupCall:${type == GROUP_CALL_TYPE}
isGroupUpdate:${type and GROUP_UPDATE_BIT != 0L}
isGroupV2:${type and GROUP_V2_BIT != 0L}
isGroupQuit:${type and GROUP_LEAVE_BIT != 0L && type and GROUP_V2_BIT == 0L}
isChatSessionRefresh:${type and ENCRYPTION_REMOTE_FAILED_BIT != 0L}
isDuplicateMessageType:${type and ENCRYPTION_REMOTE_DUPLICATE_BIT != 0L}
isDecryptInProgressType:${type and 0x40000000 != 0L}
isNoRemoteSessionType:${type and ENCRYPTION_REMOTE_NO_SESSION_BIT != 0L}
isLegacyType:${type and ENCRYPTION_REMOTE_LEGACY_BIT != 0L || type and ENCRYPTION_REMOTE_BIT != 0L}
isProfileChange:${type == PROFILE_CHANGE_TYPE}
isGroupV1MigrationEvent:${type == GV1_MIGRATION_TYPE}
isChangeNumber:${type == CHANGE_NUMBER_TYPE}
isBoostRequest:${type == BOOST_REQUEST_TYPE}
isGroupV2LeaveOnly:${type and GROUP_V2_LEAVE_BITS == GROUP_V2_LEAVE_BITS}
""".trimIndent()
return describe.replace(Regex("is[A-Z][A-Za-z0-9]*:false\n?"), "").replace("\n", "<br>")
}
}

View file

@ -1,7 +1,7 @@
package org.signal.spinner
import android.app.Application
import android.content.ContentValues
import android.content.Context
import android.database.sqlite.SQLiteQueryBuilder
import androidx.sqlite.db.SupportSQLiteDatabase
import org.signal.core.util.logging.Log
@ -15,9 +15,9 @@ object Spinner {
private lateinit var server: SpinnerServer
fun init(context: Context, deviceInfo: DeviceInfo, databases: Map<String, SupportSQLiteDatabase>) {
fun init(application: Application, deviceInfo: DeviceInfo, databases: Map<String, DatabaseConfig>) {
try {
server = SpinnerServer(context, deviceInfo, databases)
server = SpinnerServer(application, deviceInfo, databases)
server.start()
} catch (e: IOException) {
Log.w(TAG, "Spinner server hit IO exception!", e)
@ -92,4 +92,9 @@ object Spinner {
val packageName: String,
val appVersion: String
)
data class DatabaseConfig(
val db: SupportSQLiteDatabase,
val columnTransformers: List<ColumnTransformer> = emptyList()
)
}

View file

@ -1,8 +1,7 @@
package org.signal.spinner
import android.content.Context
import android.app.Application
import android.database.Cursor
import android.util.Base64
import androidx.sqlite.db.SupportSQLiteDatabase
import com.github.jknack.handlebars.Handlebars
import com.github.jknack.handlebars.Template
@ -10,7 +9,7 @@ import com.github.jknack.handlebars.helper.ConditionalHelpers
import fi.iki.elonen.NanoHTTPD
import org.signal.core.util.ExceptionUtil
import org.signal.core.util.logging.Log
import org.signal.spinner.MessageUtil.isMessageType
import org.signal.spinner.Spinner.DatabaseConfig
import java.lang.IllegalArgumentException
import java.text.SimpleDateFormat
import java.util.Date
@ -26,16 +25,16 @@ import kotlin.math.min
* to [renderTemplate].
*/
internal class SpinnerServer(
private val context: Context,
private val application: Application,
private val deviceInfo: Spinner.DeviceInfo,
private val databases: Map<String, SupportSQLiteDatabase>
private val databases: Map<String, DatabaseConfig>
) : NanoHTTPD(5000) {
companion object {
private val TAG = Log.tag(SpinnerServer::class.java)
}
private val handlebars: Handlebars = Handlebars(AssetTemplateLoader(context)).apply {
private val handlebars: Handlebars = Handlebars(AssetTemplateLoader(application)).apply {
registerHelper("eq", ConditionalHelpers.eq)
registerHelper("neq", ConditionalHelpers.neq)
}
@ -50,18 +49,18 @@ internal class SpinnerServer(
}
val dbParam: String = session.queryParam("db") ?: session.parameters["db"]?.toString() ?: databases.keys.first()
val db: SupportSQLiteDatabase = databases[dbParam] ?: return internalError(IllegalArgumentException("Invalid db param!"))
val dbConfig: DatabaseConfig = databases[dbParam] ?: return internalError(IllegalArgumentException("Invalid db param!"))
try {
return when {
session.method == Method.GET && session.uri == "/css/main.css" -> newFileResponse("css/main.css", "text/css")
session.method == Method.GET && session.uri == "/js/main.js" -> newFileResponse("js/main.js", "text/javascript")
session.method == Method.GET && session.uri == "/" -> getIndex(dbParam, db)
session.method == Method.GET && session.uri == "/browse" -> getBrowse(dbParam, db)
session.method == Method.POST && session.uri == "/browse" -> postBrowse(dbParam, db, session)
session.method == Method.GET && session.uri == "/query" -> getQuery(dbParam, db)
session.method == Method.POST && session.uri == "/query" -> postQuery(dbParam, db, session)
session.method == Method.GET && session.uri == "/recent" -> getRecent(dbParam, db)
session.method == Method.GET && session.uri == "/" -> getIndex(dbParam, dbConfig.db)
session.method == Method.GET && session.uri == "/browse" -> getBrowse(dbParam, dbConfig.db)
session.method == Method.POST && session.uri == "/browse" -> postBrowse(dbParam, dbConfig, session)
session.method == Method.GET && session.uri == "/query" -> getQuery(dbParam, dbConfig.db)
session.method == Method.POST && session.uri == "/query" -> postQuery(dbParam, dbConfig, session)
session.method == Method.GET && session.uri == "/recent" -> getRecent(dbParam, dbConfig.db)
else -> newFixedLengthResponse(Response.Status.NOT_FOUND, MIME_HTML, "Not found")
}
} catch (t: Throwable) {
@ -108,13 +107,13 @@ internal class SpinnerServer(
)
}
private fun postBrowse(dbName: String, db: SupportSQLiteDatabase, session: IHTTPSession): Response {
private fun postBrowse(dbName: String, dbConfig: DatabaseConfig, session: IHTTPSession): Response {
val table: String = session.parameters["table"]?.get(0).toString()
val pageSize: Int = session.parameters["pageSize"]?.get(0)?.toInt() ?: 1000
var pageIndex: Int = session.parameters["pageIndex"]?.get(0)?.toInt() ?: 0
val action: String? = session.parameters["action"]?.get(0)
val rowCount = db.getTableRowCount(table)
val rowCount = dbConfig.db.getTableRowCount(table)
val pageCount = ceil(rowCount.toFloat() / pageSize.toFloat()).toInt()
when (action) {
@ -125,7 +124,7 @@ internal class SpinnerServer(
}
val query = "select * from $table limit $pageSize offset ${pageSize * pageIndex}"
val queryResult = db.query(query).toQueryResult()
val queryResult = dbConfig.db.query(query).toQueryResult(columnTransformers = dbConfig.columnTransformers)
return renderTemplate(
"browse",
@ -133,7 +132,7 @@ internal class SpinnerServer(
deviceInfo = deviceInfo,
database = dbName,
databases = databases.keys.toList(),
tableNames = db.getTableNames(),
tableNames = dbConfig.db.getTableNames(),
table = table,
queryResult = queryResult,
pagingData = PagingData(
@ -180,7 +179,7 @@ internal class SpinnerServer(
)
}
private fun postQuery(dbName: String, db: SupportSQLiteDatabase, session: IHTTPSession): Response {
private fun postQuery(dbName: String, dbConfig: DatabaseConfig, session: IHTTPSession): Response {
val action: String = session.parameters["action"]?.get(0).toString()
val rawQuery: String = session.parameters["query"]?.get(0).toString()
val query = if (action == "analyze") "EXPLAIN QUERY PLAN $rawQuery" else rawQuery
@ -193,7 +192,7 @@ internal class SpinnerServer(
database = dbName,
databases = databases.keys.toList(),
query = rawQuery,
queryResult = db.query(query).toQueryResult(startTime)
queryResult = dbConfig.db.query(query).toQueryResult(queryStartTime = startTime, columnTransformers = dbConfig.columnTransformers)
)
)
}
@ -218,20 +217,26 @@ internal class SpinnerServer(
return newChunkedResponse(
Response.Status.OK,
mimeType,
context.assets.open(assetPath)
application.assets.open(assetPath)
)
}
private fun Cursor.toQueryResult(queryStartTime: Long = 0): QueryResult {
private fun Cursor.toQueryResult(queryStartTime: Long = 0, columnTransformers: List<ColumnTransformer> = emptyList()): QueryResult {
val numColumns = this.columnCount
val columns = mutableListOf<String>()
val transformers = mutableListOf<ColumnTransformer>()
for (i in 0 until numColumns) {
val columnName = getColumnName(i)
columns += columnName
if (columnName.isMessageType()) {
columns += "meta_type"
val customTransformer: ColumnTransformer? = columnTransformers.find { it.matches(null, columnName) }
columns += if (customTransformer != null) {
"$columnName *"
} else {
columnName
}
transformers += customTransformer ?: DefaultColumnTransformer
}
var timeOfFirstRow = 0L
@ -243,16 +248,10 @@ internal class SpinnerServer(
val row = mutableListOf<String>()
for (i in 0 until numColumns) {
val data: String? = when (getType(i)) {
Cursor.FIELD_TYPE_BLOB -> Base64.encodeToString(getBlob(i), 0)
else -> getString(i)
}
row += data ?: "null"
if (getColumnName(i).isMessageType()) {
row += MessageUtil.describeMessageType(getLong(i))
}
val columnName: String = getColumnName(i)
row += transformers[i].transform(null, columnName, this)
}
rows += row
}