Add an internal sqlite playground.

This commit is contained in:
Greyson Parrelli
2024-11-25 12:22:52 -05:00
parent 263ea37a9e
commit 1f91ed4274
4 changed files with 309 additions and 13 deletions

View File

@@ -149,24 +149,15 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
onUnregisterClicked()
}
)
dividerPref()
sectionHeaderPref(DSLSettingsText.from("Miscellaneous"))
sectionHeaderPref(DSLSettingsText.from("Playgrounds"))
clickPref(
title = DSLSettingsText.from("Search for a recipient"),
summary = DSLSettingsText.from("Search by ID, ACI, or PNI."),
title = DSLSettingsText.from("SQLite Playground"),
summary = DSLSettingsText.from("Run raw SQLite queries."),
onClick = {
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToInternalSearchFragment())
}
)
clickPref(
title = DSLSettingsText.from("SVR Playground"),
summary = DSLSettingsText.from("Quickly test various SVR options and error conditions."),
onClick = {
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToInternalSvrPlaygroundFragment())
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToInternalSqlitePlaygroundFragment())
}
)
@@ -186,6 +177,26 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
clickPref(
title = DSLSettingsText.from("SVR Playground"),
summary = DSLSettingsText.from("Quickly test various SVR options and error conditions."),
onClick = {
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToInternalSvrPlaygroundFragment())
}
)
dividerPref()
sectionHeaderPref(DSLSettingsText.from("Miscellaneous"))
clickPref(
title = DSLSettingsText.from("Search for a recipient"),
summary = DSLSettingsText.from("Search by ID, ACI, or PNI."),
onClick = {
findNavController().safeNavigate(InternalSettingsFragmentDirections.actionInternalSettingsFragmentToInternalSearchFragment())
}
)
switchPref(
title = DSLSettingsText.from("'Internal Details' button"),
summary = DSLSettingsText.from("Show a button in conversation settings that lets you see more information about a user."),

View File

@@ -0,0 +1,209 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.internal.sqlite
import androidx.compose.foundation.ScrollState
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.ui.Buttons
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.signal.libsignal.protocol.util.Hex
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.settings.app.internal.sqlite.InternalSqlitePlaygroundViewModel.QueryResult
import org.thoughtcrime.securesms.compose.ComposeFragment
class InternalSqlitePlaygroundFragment : ComposeFragment() {
val viewModel by viewModels<InternalSqlitePlaygroundViewModel>()
@Composable
override fun FragmentContent() {
val queryResults by viewModel.queryResults
Screen(
onBackPressed = { findNavController().popBackStack() },
queryResults = queryResults,
onQuerySubmitted = { viewModel.onQuerySubmitted(it) }
)
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun Screen(
onBackPressed: () -> Unit = {},
queryResults: QueryResult?,
onQuerySubmitted: (String) -> Unit = {}
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text("SQLite Playground") },
navigationIcon = {
IconButton(onClick = onBackPressed) {
Icon(
painter = painterResource(R.drawable.symbol_arrow_left_24),
tint = MaterialTheme.colorScheme.onSurface,
contentDescription = null
)
}
}
)
}
) { contentPadding ->
Surface(modifier = Modifier.padding(contentPadding)) {
Column(modifier = Modifier.padding(8.dp)) {
Text("Warning! This allows you to run arbitrary queries. Only use this if you know what you're doing!", color = Color.Red)
Spacer(Modifier.height(8.dp))
QueryBox(onQuerySubmitted)
Spacer(Modifier.height(8.dp))
QueryResults(queryResults)
}
}
}
}
@Composable
private fun QueryBox(onQuerySubmitted: (String) -> Unit = {}) {
var queryText: String by remember { mutableStateOf("") }
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.End
) {
OutlinedTextField(
value = queryText,
onValueChange = { queryText = it },
modifier = Modifier.fillMaxWidth(),
minLines = 3,
textStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace)
)
Spacer(Modifier.height(2.dp))
Buttons.LargePrimary(onClick = { onQuerySubmitted(queryText) }) {
Text("Execute")
}
}
}
@Composable
private fun QueryResults(results: QueryResult?) {
val columnWidth = LocalConfiguration.current.screenWidthDp.dp / 2
val horizontalScrollState = rememberScrollState()
if (results == null) {
Text("Waiting on query results.")
return
}
Text("${results.rows.size} rows in ${results.totalTimeString} ms", modifier = Modifier.padding(4.dp))
QueryRow(data = results.columns, columnWidth = columnWidth, scrollState = horizontalScrollState, fontWeight = FontWeight.Bold)
LazyColumn {
items(results.rows) { row ->
QueryRow(data = row, columnWidth = columnWidth, scrollState = horizontalScrollState)
}
}
}
@Composable
private fun QueryRow(data: List<Any?>, columnWidth: Dp, scrollState: ScrollState, fontWeight: FontWeight = FontWeight.Normal) {
val context = LocalContext.current
Row(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(scrollState)
) {
data.forEach {
Text(
text = it.toDisplayString(),
fontWeight = fontWeight,
overflow = TextOverflow.Ellipsis,
maxLines = 1,
modifier = Modifier.width(columnWidth)
.padding(4.dp)
.clickable {
MaterialAlertDialogBuilder(context)
.setMessage(it.toDisplayString())
.setPositiveButton("Ok", null)
.show()
}
)
}
}
}
private fun Any?.toDisplayString(): String {
return when (this) {
is ByteArray -> "Blob { ${Hex.toStringCondensed(this)} }"
else -> this.toString()
}
}
@SignalPreview
@Composable
private fun ScreenPreview() {
Previews.Preview {
Screen(
queryResults = QueryResult(
columns = listOf("column1", "column2", "column3"),
rows = listOf(
listOf("a", 1, ByteArray(16) { 0 }),
listOf("b", 2, ByteArray(16) { 1 }),
listOf("c", 3, ByteArray(16) { 2 })
),
totalTimeString = "3.42"
)
)
}
}
@SignalPreview
@Composable
private fun ScreenPreviewNoResults() {
Previews.Preview {
Screen(
queryResults = null
)
}
}

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.settings.app.internal.sqlite
import android.database.Cursor
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.signal.core.util.ExceptionUtil
import org.signal.core.util.readToList
import org.signal.core.util.roundedString
import org.thoughtcrime.securesms.database.SignalDatabase
import kotlin.time.Duration.Companion.nanoseconds
import kotlin.time.DurationUnit
class InternalSqlitePlaygroundViewModel : ViewModel() {
private val _queryResults: MutableState<QueryResult?> = mutableStateOf(null)
val queryResults: State<QueryResult?>
get() = _queryResults
fun onQuerySubmitted(query: String) {
viewModelScope.launch(Dispatchers.IO) {
_queryResults.value = null
val startTime = System.nanoTime()
try {
SignalDatabase.rawDatabase.rawQuery(query).use { cursor ->
val columnNames = cursor.columnNames.toList()
val rows: List<List<Any?>> = cursor.readToList { row ->
val out: MutableList<Any?> = ArrayList(row.columnCount)
for (i in 0 until row.columnCount) {
if (row.getType(i) == Cursor.FIELD_TYPE_BLOB) {
out.add(row.getBlob(i))
} else {
out.add(row.getString(i))
}
}
out
}
val endTime = System.nanoTime()
val timeMs: String = (endTime - startTime).nanoseconds.toDouble(DurationUnit.MILLISECONDS).roundedString(2)
_queryResults.value = QueryResult(columns = columnNames, rows = rows, totalTimeString = timeMs)
}
} catch (e: Exception) {
_queryResults.value = QueryResult(
columns = listOf("Query failed!"),
rows = listOf(listOf(ExceptionUtil.convertThrowableToString(e))),
totalTimeString = ""
)
}
}
}
data class QueryResult(
val columns: List<String>,
val rows: List<List<Any?>>,
val totalTimeString: String
)
}

View File

@@ -751,6 +751,9 @@
<action
android:id="@+id/action_internalSettingsFragment_to_internalStorageServicePlaygroundFragment"
app:destination="@id/internalStorageServicePlaygroundFragment" />
<action
android:id="@+id/action_internalSettingsFragment_to_internalSqlitePlaygroundFragment"
app:destination="@id/internalSqlitePlaygroundFragment" />
</fragment>
<fragment
@@ -808,6 +811,11 @@
android:name="org.thoughtcrime.securesms.components.settings.app.internal.storage.InternalStorageServicePlaygroundFragment"
android:label="internal_storage_service_playground_fragment" />
<fragment
android:id="@+id/internalSqlitePlaygroundFragment"
android:name="org.thoughtcrime.securesms.components.settings.app.internal.sqlite.InternalSqlitePlaygroundFragment"
android:label="internal_sqlite_playground_fragment" />
<!-- endregion -->
<!-- App updates -->