mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-24 21:15:48 +00:00
Revert using WebView for debug log screen.
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user