Add a ColumnTransformer system to Spinner.
This commit is contained in:
parent
935dd7de45
commit
f4002850bb
9 changed files with 215 additions and 163 deletions
|
@ -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),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -47,7 +47,7 @@
|
|||
{{#each queryResult.rows}}
|
||||
<tr>
|
||||
{{#each this}}
|
||||
<td>{{this}}</td>
|
||||
<td>{{{this}}}</td>
|
||||
{{/each}}
|
||||
</tr>
|
||||
{{/each}}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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>")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue