Revert using WebView for debug log screen.

This commit is contained in:
lisa-signal
2025-07-24 12:27:01 -04:00
committed by GitHub
parent a249fbb38f
commit 3cbb2ffd3f
4 changed files with 247 additions and 144 deletions

View File

@@ -11,8 +11,6 @@ import android.text.util.Linkify;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.TextView;
import android.widget.Toast;
@@ -21,13 +19,13 @@ import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.widget.SearchView;
import androidx.core.app.ShareCompat;
import androidx.core.content.ContextCompat;
import androidx.core.text.util.LinkifyCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.json.JSONObject;
import org.thoughtcrime.securesms.BaseActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ProgressCard;
@@ -40,13 +38,13 @@ import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
import java.util.List;
public class SubmitDebugLogActivity extends BaseActivity {
public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugLogAdapter.Listener {
private static final int CODE_SAVE = 24601;
private WebView logWebView;
private RecyclerView lineList;
private SubmitDebugLogAdapter adapter;
private SubmitDebugLogViewModel viewModel;
private boolean isPageLoaded;
private View warningBanner;
private View editBanner;
@@ -95,12 +93,13 @@ public class SubmitDebugLogActivity extends BaseActivity {
SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
viewModel.onQueryUpdated(query);
return true;
}
@Override
public boolean onQueryTextChange(String query) {
onQueryChanged(query);
viewModel.onQueryUpdated(query);
return true;
}
};
@@ -115,7 +114,7 @@ public class SubmitDebugLogActivity extends BaseActivity {
@Override
public boolean onMenuItemActionCollapse(MenuItem item) {
searchView.setOnQueryTextListener(null);
onQueryChanged("");
viewModel.onSearchClosed();
return true;
}
});
@@ -166,32 +165,13 @@ public class SubmitDebugLogActivity extends BaseActivity {
}
}
// TODO [lisa][debug-log-delete]
// public void onLogDeleted(@NonNull LogLine logLine) {
// viewModel.onLogDeleted(logLine);
// }
private void initWebView() {
logWebView.getSettings().setBuiltInZoomControls(true);
logWebView.getSettings().setDisplayZoomControls(false);
logWebView.getSettings().setUseWideViewPort(true);
logWebView.getSettings().setJavaScriptEnabled(true);
logWebView.setHorizontalScrollBarEnabled(true);
logWebView.setWebViewClient(new WebViewClient() {
@Override
public void onPageFinished(WebView view, String url) {
isPageLoaded = true;
}
});
}
private String intToCssHex(int color) {
return String.format("#%06X", 0xFFFFFF & color);
@Override
public void onLogDeleted(@NonNull LogLine logLine) {
viewModel.onLogDeleted(logLine);
}
private void initView() {
this.logWebView = findViewById(R.id.debug_log_lines);
this.lineList = 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);
@@ -199,27 +179,35 @@ public class SubmitDebugLogActivity extends BaseActivity {
this.scrollToTopButton = findViewById(R.id.debug_log_scroll_to_top);
this.progressCard = findViewById(R.id.debug_log_progress_card);
initWebView();
this.adapter = new SubmitDebugLogAdapter(this, viewModel.getPagingController());
this.lineList.setLayoutManager(new LinearLayoutManager(this));
this.lineList.setAdapter(adapter);
this.lineList.setItemAnimator(null);
submitButton.setOnClickListener(v -> onSubmitClicked());
scrollToBottomButton.setOnClickListener(v -> logWebView.pageDown(true));
scrollToTopButton.setOnClickListener(v -> logWebView.pageUp(true));
scrollToBottomButton.setOnClickListener(v -> lineList.scrollToPosition(adapter.getItemCount() - 1));
scrollToTopButton.setOnClickListener(v -> lineList.scrollToPosition(0));
logWebView.getViewTreeObserver().addOnScrollChangedListener(() -> {
if (logWebView.getScrollY() + logWebView.getHeight() < logWebView.getContentHeight() * logWebView.getScale() - 10) {
scrollToBottomButton.setVisibility(View.VISIBLE);
} else {
scrollToBottomButton.setVisibility(View.GONE);
}
lineList.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition() < adapter.getItemCount() - 10) {
scrollToBottomButton.setVisibility(View.VISIBLE);
} else {
scrollToBottomButton.setVisibility(View.GONE);
}
if (logWebView.getScrollY() > 10) {
scrollToTopButton.setVisibility(View.VISIBLE);
} else {
scrollToTopButton.setVisibility(View.GONE);
if (((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition() > 10) {
scrollToTopButton.setVisibility(View.VISIBLE);
} else {
scrollToTopButton.setVisibility(View.GONE);
}
}
});
this.progressCard.setVisibility(View.VISIBLE);
}
private void initViewModel() {
@@ -229,10 +217,6 @@ public class SubmitDebugLogActivity extends BaseActivity {
}
private void presentLines(@NonNull List<LogLine> lines) {
if (!isPageLoaded) {
initWebView();
}
if (progressCard != null && lines.size() > 0) {
progressCard.setVisibility(View.GONE);
@@ -240,108 +224,23 @@ public class SubmitDebugLogActivity extends BaseActivity {
submitButton.setVisibility(View.VISIBLE);
}
StringBuilder body = new StringBuilder();
int backgroundColor = ContextCompat.getColor(this, R.color.signal_colorBackground);
int noneColor = ContextCompat.getColor(this, R.color.debuglog_color_none);
int verboseColor = ContextCompat.getColor(this, R.color.debuglog_color_verbose);
int debugColor = ContextCompat.getColor(this, R.color.debuglog_color_debug);
int infoColor = ContextCompat.getColor(this, R.color.debuglog_color_info);
int warningColor = ContextCompat.getColor(this, R.color.debuglog_color_warn);
int errorColor = ContextCompat.getColor(this, R.color.debuglog_color_error);
String css = String.format("""
<style>
body {background-color: %s;}
div {white-space: pre; margin-top: 8; margin-bottom: 8; height: 10px;}
.none {color: %s;}
.verbose {color: %s;}
.debug {color: %s;}
.info {color: %s;}
.warning {color: %s;}
.error {color: %s;}
.hidden {display: none;}
</style>
""",
intToCssHex(backgroundColor),
intToCssHex(noneColor),
intToCssHex(verboseColor),
intToCssHex(debugColor),
intToCssHex(infoColor),
intToCssHex(warningColor),
intToCssHex(errorColor)
);
String js = """
<script type='text/javascript'>
let debounceTimer = null;
function filterLogLines(query) {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(function() {
const container = document.getElementById('container');
if (!container) return;
const lower = query.toLowerCase();
const lines = container.getElementsByTagName('div');
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
const text = line.textContent.toLowerCase();
if (text.includes(lower)) {
line.classList.remove('hidden');
} else {
line.classList.add('hidden');
}
}
}, 100);
}
</script>
""";
body.append(String.format("<html><head>%s%s</head><body style=\"font-family: monospace; font-size: 12px; overflow-y: scroll;\"><div id=\"container\">", css, js));
for (LogLine line : lines) {
if (line == null) continue;
String newLine = line.getText();
String lineClass = switch (line.getStyle()) {
case VERBOSE -> "verbose";
case DEBUG -> "debug";
case INFO -> "info";
case WARNING -> "warning";
case ERROR -> "error";
default -> "none";
};
body.append(String.format("<div class=%s>%s</div>", lineClass, newLine));
}
body.append("</div></body></html>");
String htmlContent = body.toString();
logWebView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null);
}
private void onQueryChanged(String query) {
String script = String.format("filterLogLines(%s);\n", JSONObject.quote(query));
logWebView.evaluateJavascript(script, null);
adapter.submitList(lines);
}
private void presentMode(@NonNull SubmitDebugLogViewModel.Mode mode) {
switch (mode) {
case NORMAL:
editBanner.setVisibility(View.GONE);
// TODO [lisa][debug-log-editing]
// setEditing(false);
adapter.setEditing(false);
saveMenuItem.setVisible(true);
// TODO [greyson][log] Not yet implemented
// editMenuItem.setVisible(true);
// doneMenuItem.setVisible(false);
searchMenuItem.setVisible(true);
// searchMenuItem.setVisible(true);
break;
case SUBMITTING:
editBanner.setVisibility(View.GONE);
// setEditing(false);
adapter.setEditing(false);
editMenuItem.setVisible(false);
doneMenuItem.setVisible(false);
searchMenuItem.setVisible(false);
@@ -349,7 +248,7 @@ public class SubmitDebugLogActivity extends BaseActivity {
break;
case EDIT:
editBanner.setVisibility(View.VISIBLE);
// setEditing(true);
adapter.setEditing(true);
editMenuItem.setVisible(false);
doneMenuItem.setVisible(true);
searchMenuItem.setVisible(true);

View File

@@ -0,0 +1,188 @@
package org.thoughtcrime.securesms.logsubmit;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ListenableHorizontalScrollView;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
public class SubmitDebugLogAdapter extends RecyclerView.Adapter<SubmitDebugLogAdapter.LineViewHolder> {
private static final int LINE_LENGTH = 500;
private static final int TYPE_LOG = 1;
private static final int TYPE_PLACEHOLDER = 2;
private final ScrollManager scrollManager;
private final Listener listener;
private final PagingController pagingController;
private final List<LogLine> lines;
private boolean editing;
public SubmitDebugLogAdapter(@NonNull Listener listener, @NonNull PagingController pagingController) {
this.listener = listener;
this.pagingController = pagingController;
this.scrollManager = new ScrollManager();
this.lines = new ArrayList<>();
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
LogLine item = getItem(position);
return item != null ? getItem(position).getId() : -1;
}
@Override
public int getItemViewType(int position) {
return getItem(position) == null ? TYPE_PLACEHOLDER : TYPE_LOG;
}
protected LogLine getItem(int position) {
pagingController.onDataNeededAroundIndex(position);
return lines.get(position);
}
public void submitList(@NonNull List<LogLine> list) {
this.lines.clear();
this.lines.addAll(list);
notifyDataSetChanged();
}
@Override
public int getItemCount() {
return lines.size();
}
@Override
public @NonNull LineViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
return new LineViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.submit_debug_log_line_item, parent, false));
}
@Override
public void onBindViewHolder(@NonNull LineViewHolder holder, int position) {
LogLine item = getItem(position);
if (item == null) {
item = SimpleLogLine.EMPTY;
}
holder.bind(item, LINE_LENGTH, editing, scrollManager, listener);
}
@Override
public void onViewRecycled(@NonNull LineViewHolder holder) {
holder.unbind(scrollManager);
}
public void setEditing(boolean editing) {
this.editing = editing;
notifyDataSetChanged();
}
private static class ScrollManager {
private final List<ScrollObserver> listeners = new CopyOnWriteArrayList<>();
private int currentPosition;
void subscribe(@NonNull ScrollObserver observer) {
listeners.add(observer);
observer.onScrollChanged(currentPosition);
}
void unsubscribe(@NonNull ScrollObserver observer) {
listeners.remove(observer);
}
void notify(int position) {
currentPosition = position;
for (ScrollObserver listener : listeners) {
listener.onScrollChanged(position);
}
}
}
private interface ScrollObserver {
void onScrollChanged(int position);
}
interface Listener {
void onLogDeleted(@NonNull LogLine logLine);
}
static class LineViewHolder extends RecyclerView.ViewHolder implements ScrollObserver {
private final TextView text;
private final ListenableHorizontalScrollView scrollView;
LineViewHolder(@NonNull View itemView) {
super(itemView);
this.text = itemView.findViewById(R.id.log_item_text);
this.scrollView = itemView.findViewById(R.id.log_item_scroll);
}
void bind(@NonNull LogLine line, int longestLine, boolean editing, @NonNull ScrollManager scrollManager, @NonNull Listener listener) {
Context context = itemView.getContext();
if (line.getText().length() > longestLine) {
text.setText(line.getText().substring(0, longestLine));
} else if (line.getText().length() < longestLine) {
text.setText(padRight(line.getText(), longestLine));
} else {
text.setText(line.getText());
}
switch (line.getStyle()) {
case NONE: text.setTextColor(ContextCompat.getColor(context, R.color.debuglog_color_none)); break;
case VERBOSE: text.setTextColor(ContextCompat.getColor(context, R.color.debuglog_color_verbose)); break;
case DEBUG: text.setTextColor(ContextCompat.getColor(context, R.color.debuglog_color_debug)); break;
case INFO: text.setTextColor(ContextCompat.getColor(context, R.color.debuglog_color_info)); break;
case WARNING: text.setTextColor(ContextCompat.getColor(context, R.color.debuglog_color_warn)); break;
case ERROR: text.setTextColor(ContextCompat.getColor(context, R.color.debuglog_color_error)); break;
}
scrollView.setOnScrollListener((newLeft, oldLeft) -> {
if (oldLeft - newLeft != 0) {
scrollManager.notify(newLeft);
}
});
scrollManager.subscribe(this);
if (editing) {
text.setOnClickListener(v -> listener.onLogDeleted(line));
} else {
text.setOnClickListener(null);
}
}
void unbind(@NonNull ScrollManager scrollManager) {
text.setOnClickListener(null);
scrollManager.unsubscribe(this);
}
@Override
public void onScrollChanged(int position) {
scrollView.scrollTo(position, 0);
}
private static String padRight(String s, int n) {
return String.format("%-" + n + "s", s);
}
}
}

View File

@@ -13,6 +13,11 @@ import androidx.lifecycle.ViewModelProvider;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.signal.paging.LivePagedData;
import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.signal.paging.ProxyPagingController;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
@@ -27,6 +32,7 @@ public class SubmitDebugLogViewModel extends ViewModel {
private final SubmitDebugLogRepository repo;
private final MutableLiveData<Mode> mode;
private final ProxyPagingController<Long> pagingController;
private final List<LogLine> staticLines;
private final MediatorLiveData<List<LogLine>> lines;
private final SingleLiveEvent<Event> event;
@@ -38,6 +44,7 @@ public class SubmitDebugLogViewModel extends ViewModel {
this.repo = new SubmitDebugLogRepository();
this.mode = new MutableLiveData<>();
this.trace = Tracer.getInstance().serialize();
this.pagingController = new ProxyPagingController<>();
this.firstViewTime = System.currentTimeMillis();
this.staticLines = new ArrayList<>();
this.lines = new MediatorLiveData<>();
@@ -49,12 +56,17 @@ public class SubmitDebugLogViewModel extends ViewModel {
Log.blockUntilAllWritesFinished();
LogDatabase.getInstance(AppDependencies.getApplication()).logs().trimToSize();
LogDataSource dataSource = new LogDataSource(AppDependencies.getApplication(), staticLines, firstViewTime);
int size = dataSource.size();
List<LogLine> allLogLines = new ArrayList<>(dataSource.load(0, size, size, () -> false));
LogDataSource dataSource = new LogDataSource(AppDependencies.getApplication(), staticLines, firstViewTime);
PagingConfig config = new PagingConfig.Builder().setPageSize(100)
.setBufferPages(3)
.setStartIndex(0)
.build();
LivePagedData<Long, LogLine> pagedData = PagedData.createForLiveData(dataSource, config);
ThreadUtil.runOnMain(() -> {
lines.setValue(allLogLines);
pagingController.set(pagedData.getController());
lines.addSource(pagedData.getData(), lines::setValue);
mode.setValue(Mode.NORMAL);
});
});
@@ -64,6 +76,10 @@ public class SubmitDebugLogViewModel extends ViewModel {
return lines;
}
@NonNull PagingController getPagingController() {
return pagingController;
}
@NonNull LiveData<Mode> getMode() {
return mode;
}

View File

@@ -44,7 +44,7 @@
app:barrierDirection="bottom"
app:constraint_referenced_ids="debug_log_warning_banner,debug_log_edit_banner" />
<WebView
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/debug_log_lines"
android:layout_width="match_parent"
android:layout_height="0dp"