mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 13:08:46 +00:00
Add an internal sqlite playground.
This commit is contained in:
@@ -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."),
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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 -->
|
||||
|
||||
Reference in New Issue
Block a user