Add filter by log level feature in debug log screen.

This commit is contained in:
lisa-signal
2025-08-08 13:04:13 -04:00
committed by Greyson Parrelli
parent e93f889115
commit c0f826808b
8 changed files with 284 additions and 114 deletions

View File

@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.logsubmit;
import android.app.Activity;
import android.content.Intent;
import android.content.res.ColorStateList;
import android.net.Uri;
import android.os.Bundle;
import android.text.SpannableString;
@@ -20,6 +21,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.core.app.ShareCompat;
import androidx.core.content.ContextCompat;
import androidx.core.text.util.LinkifyCompat;
import androidx.lifecycle.ViewModelProvider;
@@ -49,15 +51,12 @@ public class SubmitDebugLogActivity extends BaseActivity {
private SubmitDebugLogViewModel viewModel;
private View warningBanner;
private View editBanner;
private CircularProgressMaterialButton submitButton;
private ConversationSearchBottomBar searchNav;
private View scrollToBottomButton;
private View scrollToTopButton;
private ProgressCard progressCard;
private MenuItem editMenuItem;
private MenuItem doneMenuItem;
private MenuItem searchMenuItem;
private MenuItem saveMenuItem;
@@ -66,9 +65,22 @@ public class SubmitDebugLogActivity extends BaseActivity {
private TextView searchPosition;
private ImageButton searchUpButton;
private ImageButton searchDownButton;
private TextView uncaughtButton;
private TextView verboseButton;
private TextView debugButton;
private TextView infoButton;
private TextView warningButton;
private TextView errorButton;
private boolean isCaseSensitive;
private boolean isFiltered;
private boolean isUncaught;
private boolean isVerbose;
private boolean isDebug;
private boolean isInfo;
private boolean isWarning;
private boolean isError;
private boolean isWebViewLoaded;
private boolean hasPresentedLines;
@@ -98,17 +110,22 @@ public class SubmitDebugLogActivity extends BaseActivity {
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.submit_debug_log_normal, menu);
this.editMenuItem = menu.findItem(R.id.menu_edit_log);
this.doneMenuItem = menu.findItem(R.id.menu_done_editing_log);
this.searchMenuItem = menu.findItem(R.id.menu_search);
this.saveMenuItem = menu.findItem(R.id.menu_save);
this.searchNav = findViewById(R.id.debug_log_search_nav);
this.filterButton = findViewById(R.id.debug_log_filter);
this.caseSensitiveButton = findViewById(R.id.case_sensitive_button);
this.searchPosition = findViewById(R.id.debug_log_search_position);
this.searchUpButton = findViewById(R.id.debug_log_search_up);
this.searchDownButton = findViewById(R.id.debug_log_search_down);
this.searchNav = findViewById(R.id.debug_log_search_nav);
this.filterButton = findViewById(R.id.debug_log_filter);
this.caseSensitiveButton = findViewById(R.id.case_sensitive_button);
this.searchPosition = findViewById(R.id.debug_log_search_position);
this.searchUpButton = findViewById(R.id.debug_log_search_up);
this.searchDownButton = findViewById(R.id.debug_log_search_down);
this.uncaughtButton = findViewById(R.id.debug_log_signalUncaughtException);
this.verboseButton = findViewById(R.id.debug_log_verbose);
this.debugButton = findViewById(R.id.debug_log_debug);
this.infoButton = findViewById(R.id.debug_log_info);
this.warningButton = findViewById(R.id.debug_log_warning);
this.errorButton = findViewById(R.id.debug_log_error);
searchUpButton.setOnClickListener(v -> {
DebugLogsViewer.onSearchUp(logWebView);
@@ -179,6 +196,7 @@ public class SubmitDebugLogActivity extends BaseActivity {
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
onFilterLevelClose();
searchNav.setVisibility(View.GONE);
submitButton.setVisibility(View.VISIBLE);
DebugLogsViewer.onSearchClose(logWebView);
@@ -189,6 +207,37 @@ public class SubmitDebugLogActivity extends BaseActivity {
}
});
verboseButton.setOnClickListener(v -> {
isVerbose = !isVerbose;
onFilterLevel(v, isVerbose);
});
debugButton.setOnClickListener(v -> {
isDebug = !isDebug;
onFilterLevel(v, isDebug);
});
infoButton.setOnClickListener(v -> {
isInfo = !isInfo;
onFilterLevel(v, isInfo);
});
warningButton.setOnClickListener(v -> {
isWarning = !isWarning;
onFilterLevel(v, isWarning);
});
errorButton.setOnClickListener(v -> {
isError = !isError;
onFilterLevel(v, isError);
});
uncaughtButton.setOnClickListener(v -> {
isUncaught = !isUncaught;
onFilterLevel(v, isUncaught);
});
if (viewModel.getMode().getValue() != null) {
presentMode(viewModel.getMode().getValue());
}
@@ -196,6 +245,38 @@ public class SubmitDebugLogActivity extends BaseActivity {
return true;
}
private void onFilterLevel(View view, boolean isChecked) {
view.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(this, (isChecked) ? R.color.transparent_black_25 : R.color.signal_background_secondary)));
List<String> selectedLevels = new ArrayList<>();
if (isVerbose) selectedLevels.add("\" V \"");
if (isDebug) selectedLevels.add("\" D \"");
if (isInfo) selectedLevels.add("\" I \"");
if (isWarning) selectedLevels.add("\" W \"");
if (isError) selectedLevels.add("\" E \"");
if (isUncaught) selectedLevels.add("\" SignalUncaughtException:\"");
DebugLogsViewer.onFilterLevel(logWebView, "[" + String.join(",", selectedLevels) + "]");
DebugLogsViewer.getSearchPosition(logWebView, position -> searchPosition.setText(position));
}
private void onFilterLevelClose() {
isVerbose = false;
isDebug = false;
isInfo = false;
isWarning = false;
isError = false;
isUncaught = false;
verboseButton.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.signal_background_secondary)));
debugButton.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.signal_background_secondary)));
infoButton.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.signal_background_secondary)));
warningButton.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.signal_background_secondary)));
errorButton.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.signal_background_secondary)));
uncaughtButton.setBackgroundTintList(ColorStateList.valueOf(ContextCompat.getColor(this, R.color.signal_background_secondary)));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
@@ -203,10 +284,6 @@ public class SubmitDebugLogActivity extends BaseActivity {
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.menu_edit_log) {
viewModel.onEditButtonPressed();
} else if (item.getItemId() == R.id.menu_done_editing_log) {
viewModel.onDoneEditingButtonPressed();
} else if (item.getItemId() == R.id.menu_save) {
Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
@@ -242,7 +319,6 @@ public class SubmitDebugLogActivity extends BaseActivity {
private void initView() {
this.logWebView = findViewById(R.id.debug_log_lines);
this.warningBanner = findViewById(R.id.debug_log_warning_banner);
this.editBanner = findViewById(R.id.debug_log_edit_banner);
this.submitButton = findViewById(R.id.debug_log_submit_button);
this.scrollToBottomButton = findViewById(R.id.debug_log_scroll_to_bottom);
this.scrollToTopButton = findViewById(R.id.debug_log_scroll_to_top);
@@ -279,50 +355,44 @@ public class SubmitDebugLogActivity extends BaseActivity {
hasPresentedLines = true;
}
int chunkSize = 1000;
int count = 0;
StringBuilder lineBuilder = new StringBuilder();
for (LogLine line : lines) {
if (line == null) continue;
lineBuilder.append(String.format("%s\n", line.getText()));
count++;
if (count >= chunkSize) {
DebugLogsViewer.presentLines(logWebView, lineBuilder.toString());
lineBuilder.setLength(0);
count = 0;
}
}
DebugLogsViewer.presentLines(logWebView, lineBuilder.toString());
if (lineBuilder.length() > 0) {
DebugLogsViewer.presentLines(logWebView, lineBuilder.toString());
}
}
private void presentMode(@NonNull SubmitDebugLogViewModel.Mode mode) {
if (editMenuItem == null || doneMenuItem == null || searchMenuItem == null || saveMenuItem == null) {
if (searchMenuItem == null || saveMenuItem == null) {
return;
}
switch (mode) {
case NORMAL:
editBanner.setVisibility(View.GONE);
searchNav.setVisibility(View.GONE);
// TODO [lisa][debug-log-editing]
// setEditing(false);
saveMenuItem.setVisible(true);
// TODO [greyson][log] Not yet implemented
// editMenuItem.setVisible(true);
// doneMenuItem.setVisible(false);
searchMenuItem.setVisible(true);
break;
case SUBMITTING:
editBanner.setVisibility(View.GONE);
// setEditing(false);
editMenuItem.setVisible(false);
doneMenuItem.setVisible(false);
searchMenuItem.setVisible(false);
saveMenuItem.setVisible(false);
break;
case EDIT:
editBanner.setVisibility(View.VISIBLE);
// setEditing(true);
editMenuItem.setVisible(false);
doneMenuItem.setVisible(true);
searchMenuItem.setVisible(true);
saveMenuItem.setVisible(false);
break;
}
}

View File

@@ -110,37 +110,12 @@ public class SubmitDebugLogViewModel extends ViewModel {
});
}
void onQueryUpdated(@NonNull String query) {
throw new UnsupportedOperationException("Not yet implemented.");
}
void onSearchClosed() {
throw new UnsupportedOperationException("Not yet implemented.");
}
void onEditButtonPressed() {
throw new UnsupportedOperationException("Not yet implemented.");
}
void onDoneEditingButtonPressed() {
throw new UnsupportedOperationException("Not yet implemented.");
}
void onLogDeleted(@NonNull LogLine line) {
throw new UnsupportedOperationException("Not yet implemented.");
}
boolean onBackPressed() {
if (mode.getValue() == Mode.EDIT) {
mode.setValue(Mode.NORMAL);
return true;
} else {
return false;
}
return false;
}
enum Mode {
NORMAL, EDIT, SUBMITTING
NORMAL, SUBMITTING
}
enum Event {

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
<solid android:color="@color/transparent_black_40"/>
<solid android:color="@color/transparent_black_25"/>
</shape>

View File

@@ -20,28 +20,12 @@
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible"/>
<TextView
android:id="@+id/debug_log_edit_banner"
android:layout_width="0dp"
android:layout_height="0dp"
android:padding="8dp"
android:gravity="center"
android:text="@string/SubmitDebugLogActivity_tap_a_line_to_delete_it"
android:textColor="@color/core_white"
android:fontFamily="sans-serif-medium"
android:background="@color/core_ultramarine"
android:visibility="gone"
app:layout_constraintTop_toTopOf="@id/debug_log_warning_banner"
app:layout_constraintBottom_toBottomOf="@id/debug_log_warning_banner"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/debug_log_header_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="debug_log_warning_banner,debug_log_edit_banner" />
app:constraint_referenced_ids="debug_log_warning_banner" />
<WebView
android:id="@+id/debug_log_lines"
@@ -98,34 +82,130 @@
app:layout_constraintBottom_toBottomOf="parent"
tools:visibility="visible">
<LinearLayout
android:id="@+id/debug_log_levels"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toTopOf="@id/debug_log_filter"
android:layout_marginVertical="5dp"
android:paddingVertical="4dp"
android:orientation="horizontal"
android:gravity="center"
>
<TextView
android:id="@+id/debug_log_verbose"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Signal.Text.Caption"
android:textAllCaps="true"
android:padding="8dp"
android:background="@drawable/tintable_pill_bg"
android:backgroundTint="@color/signal_background_secondary"
android:text="@string/SubmitDebugLogActivity_verbose"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/debug_log_debug"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Signal.Text.Caption"
android:textAllCaps="true"
android:padding="8dp"
android:background="@drawable/tintable_pill_bg"
android:backgroundTint="@color/signal_background_secondary"
android:text="@string/SubmitDebugLogActivity_debug"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/debug_log_verbose"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/debug_log_info"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/SubmitDebugLogActivity_info"
style="@style/Signal.Text.Caption"
android:textAllCaps="true"
android:padding="8dp"
android:background="@drawable/tintable_pill_bg"
android:backgroundTint="@color/signal_background_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/debug_log_debug"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/debug_log_warning"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/SubmitDebugLogActivity_warning"
style="@style/Signal.Text.Caption"
android:textAllCaps="true"
android:padding="8dp"
android:background="@drawable/tintable_pill_bg"
android:backgroundTint="@color/signal_background_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/info"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/debug_log_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/SubmitDebugLogActivity_error"
style="@style/Signal.Text.Caption"
android:textAllCaps="true"
android:padding="8dp"
android:background="@drawable/tintable_pill_bg"
android:backgroundTint="@color/signal_background_secondary"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/debug_log_warning"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/debug_log_signalUncaughtException"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/SubmitDebugLogActivity_signal_uncaught_exception"
style="@style/Signal.Text.Caption"
android:textAllCaps="true"
android:padding="8dp"
android:background="@drawable/tintable_pill_bg"
android:backgroundTint="@color/signal_background_secondary"
android:textAlignment="center"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/debug_log_error"
app:layout_constraintTop_toTopOf="parent" />
</LinearLayout>
<ImageButton
android:id="@+id/debug_log_filter"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/circle_touch_highlight_background"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:padding="8dp"
android:src="@drawable/symbol_filter_24"
app:tint="@color/signal_colorOnSurface"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
app:layout_constraintTop_toBottomOf="@id/debug_log_levels" />
<ImageButton
android:id="@+id/case_sensitive_button"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="@drawable/circle_touch_highlight_background"
android:layout_marginTop="8dp"
android:layout_marginStart="8dp"
android:layout_marginBottom="8dp"
android:padding="8dp"
android:src="@drawable/symbol_match_case_24"
app:tint="@color/signal_colorOnSurface"
app:layout_constraintStart_toEndOf="@+id/debug_log_filter"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toEndOf="@id/debug_log_filter"
app:layout_constraintTop_toBottomOf="@id/debug_log_levels"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
@@ -133,19 +213,17 @@
style="@style/Signal.Text.Body"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/debug_log_search_up"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintTop_toBottomOf="@id/debug_log_levels"
tools:text="37 of 73" />
<ImageButton
android:id="@+id/debug_log_search_up"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:background="?selectableItemBackgroundBorderless"
@@ -154,13 +232,12 @@
app:tint="@color/core_ultramarine"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/debug_log_search_down"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/debug_log_levels" />
<ImageButton
android:id="@+id/debug_log_search_down"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:background="?selectableItemBackgroundBorderless"
@@ -169,8 +246,7 @@
app:tint="@color/core_ultramarine"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
app:layout_constraintTop_toBottomOf="@id/debug_log_levels" />
</org.thoughtcrime.securesms.components.ConversationSearchBottomBar>
<org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton
@@ -179,6 +255,7 @@
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_margin="8dp"
android:paddingTop="8dp"
android:visibility="gone"
app:circularProgressMaterialButton__label="@string/SubmitDebugLogActivity_submit"
app:layout_constraintStart_toStartOf="parent"

View File

@@ -12,18 +12,6 @@
app:actionViewClass="org.thoughtcrime.securesms.components.SearchView"
app:showAsAction="collapseActionView|always" />
<item
android:id="@+id/menu_edit_log"
android:title="@string/SubmitDebugLogActivity_edit"
android:visible="false"
app:showAsAction="always" />
<item
android:id="@+id/menu_done_editing_log"
android:title="@string/SubmitDebugLogActivity_done"
android:visible="false"
app:showAsAction="always" />
<item
android:id="@+id/menu_save"
android:icon="@drawable/symbol_save_android_24"

View File

@@ -2928,6 +2928,13 @@
<string name="SubmitDebugLogActivity_this_log_will_be_posted_publicly_online_for_contributors">This log will be posted publicly online for contributors to view. You may examine it before uploading.</string>
<!-- Banner message shown while submitting debug log -->
<string name="SubmitDebugLogActivity_your_log_will_be_posted_online">When you click Submit, your log will be posted online for 30 days at a unique, unpublished URL. You may Save it locally first.</string>
<!-- Debug log level names to filter by levels. -->
<string name="SubmitDebugLogActivity_signal_uncaught_exception" translatable="false">Uncaught</string>
<string name="SubmitDebugLogActivity_verbose" translatable="false">Verbose</string>
<string name="SubmitDebugLogActivity_debug" translatable="false">Debug</string>
<string name="SubmitDebugLogActivity_info" translatable="false">Info</string>
<string name="SubmitDebugLogActivity_warning" translatable="false">Warn</string>
<string name="SubmitDebugLogActivity_error" translatable="false">Error</string>
<!-- SupportEmailUtil -->
<string name="SupportEmailUtil_support_email" translatable="false">support@signal.org</string>

View File

@@ -54,6 +54,7 @@ const session = editor.getSession();
let logLines = ""; // Original logLines
let input = ""; // Search query input
let selectedLevels = []; // Log levels that are selected in checkboxes
let markers = []; // IDs of highlighted search markers
let matchRanges = []; // Ranges of all search matches
let matchCount = 0; // Total number of matches
@@ -134,6 +135,7 @@ function onSearchDown() {
}
function onSearchClose() {
editor.setValue(logLines, -1);
editor.getSelection().clearSelection();
input = "";
clearMarkers();
@@ -156,19 +158,64 @@ function onFilter() {
isFiltered = true;
editor.getSelection().clearSelection();
clearMarkers();
applyFilter();
}
function onFilterClose() {
isFiltered = false;
clearMarkers();
editor.getSelection().clearSelection();
if (selectedLevels.length === 0) {
editor.setValue(logLines, -1);
} else {
const filtered = logLines
.split("\n")
.filter((line) => {
return selectedLevels.some((level) => line.includes(level));
})
.join("\n");
editor.setValue(filtered, -1);
}
highlightAllMatches(input);
}
function onFilterLevel(sLevels) {
selectedLevels = sLevels;
if (isFiltered) {
applyFilter();
} else {
if (selectedLevels.length === 0) {
editor.setValue(logLines, -1);
editor.scrollToRow(0);
} else {
const filtered = logLines
.split("\n")
.filter((line) => {
return selectedLevels.some((level) => line.includes(level));
})
.join("\n");
editor.setValue(filtered, -1);
}
onSearch();
}
}
function applyFilter() {
const filtered = logLines
.split("\n")
.filter((line) => {
const newLine = isCaseSensitive ? line : line.toLowerCase();
return newLine.includes(isCaseSensitive ? input : input.toLowerCase());
const lineMatch = newLine.includes(isCaseSensitive ? input : input.toLowerCase());
const levelMatch = selectedLevels.length === 0 || selectedLevels.some((level) => line.includes(level));
return lineMatch && levelMatch;
})
.join("\n");
editor.setValue(filtered, -1);
}
function onFilterClose() {
isFiltered = false;
editor.setValue(logLines, -1);
highlightAllMatches(input);
}

View File

@@ -47,7 +47,7 @@ object DebugLogsViewer {
fun presentLines(webview: WebView, lines: String) {
// Set the debug log lines
val escaped = JSONObject.quote(lines)
webview.evaluateJavascript("editor.insert($escaped); logLines=$escaped;", null)
webview.evaluateJavascript("editor.insert($escaped); logLines+=$escaped;", null)
}
@JvmStatic
@@ -80,6 +80,12 @@ object DebugLogsViewer {
webview.evaluateJavascript("onFilterClose()", null)
}
@JvmStatic
fun onFilterLevel(webview: WebView, selectedLevels: String) {
webview.evaluateJavascript("if (isFiltered) { onFilter(); }", null)
webview.evaluateJavascript("onFilterLevel($selectedLevels)", null)
}
@JvmStatic
fun onSearchUp(webview: WebView) {
webview.evaluateJavascript("onSearchUp();", null)