mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-20 02:58:45 +00:00
View debug log screen through WebView through a module.
This commit is contained in:
@@ -501,6 +501,7 @@ dependencies {
|
|||||||
implementation(project(":device-transfer"))
|
implementation(project(":device-transfer"))
|
||||||
implementation(project(":image-editor"))
|
implementation(project(":image-editor"))
|
||||||
implementation(project(":donations"))
|
implementation(project(":donations"))
|
||||||
|
implementation(project(":debuglogs-viewer"))
|
||||||
implementation(project(":contacts"))
|
implementation(project(":contacts"))
|
||||||
implementation(project(":qr"))
|
implementation(project(":qr"))
|
||||||
implementation(project(":sticky-header-grid"))
|
implementation(project(":sticky-header-grid"))
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.logsubmit
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import org.signal.core.util.logging.Scrubber
|
|
||||||
import org.signal.paging.PagedDataSource
|
|
||||||
import org.thoughtcrime.securesms.database.LogDatabase
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves logs to show in the [SubmitDebugLogActivity].
|
|
||||||
*
|
|
||||||
* @param prefixLines A static list of lines to show before all of the lines retrieved from [LogDatabase]
|
|
||||||
* @param untilTime Only show logs before this time. This is our way of making sure the set of logs we show on this screen doesn't grow.
|
|
||||||
*/
|
|
||||||
class LogDataSource(
|
|
||||||
application: Application,
|
|
||||||
private val prefixLines: List<LogLine>,
|
|
||||||
private val untilTime: Long
|
|
||||||
) :
|
|
||||||
PagedDataSource<Long, LogLine> {
|
|
||||||
|
|
||||||
val logDatabase = LogDatabase.getInstance(application)
|
|
||||||
|
|
||||||
override fun size(): Int {
|
|
||||||
return prefixLines.size + logDatabase.logs.getLogCountBeforeTime(untilTime)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun load(start: Int, length: Int, totalSize: Int, cancellationSignal: PagedDataSource.CancellationSignal): List<LogLine> {
|
|
||||||
if (start + length < prefixLines.size) {
|
|
||||||
return prefixLines.subList(start, start + length)
|
|
||||||
} else if (start < prefixLines.size) {
|
|
||||||
return prefixLines.subList(start, prefixLines.size) +
|
|
||||||
logDatabase.logs.getRangeBeforeTime(0, length - (prefixLines.size - start), untilTime).map { convertToLogLine(it) }
|
|
||||||
} else {
|
|
||||||
return logDatabase.logs.getRangeBeforeTime(start - prefixLines.size, length, untilTime).map { convertToLogLine(it) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun load(key: Long?): LogLine? {
|
|
||||||
throw UnsupportedOperationException("Not implemented!")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getKey(data: LogLine): Long {
|
|
||||||
return data.id
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun convertToLogLine(raw: String): LogLine {
|
|
||||||
val scrubbed: String = Scrubber.scrub(raw).toString()
|
|
||||||
return SimpleLogLine(scrubbed, LogStyleParser.parseStyle(scrubbed), LogLine.Placeholder.NONE)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,21 +11,20 @@ import android.text.util.Linkify;
|
|||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.webkit.WebView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.widget.SearchView;
|
|
||||||
import androidx.core.app.ShareCompat;
|
import androidx.core.app.ShareCompat;
|
||||||
import androidx.core.text.util.LinkifyCompat;
|
import androidx.core.text.util.LinkifyCompat;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import org.signal.debuglogsviewer.DebugLogsViewer;
|
||||||
import org.thoughtcrime.securesms.BaseActivity;
|
import org.thoughtcrime.securesms.BaseActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.ProgressCard;
|
import org.thoughtcrime.securesms.components.ProgressCard;
|
||||||
@@ -36,14 +35,14 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
|||||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||||
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugLogAdapter.Listener {
|
public class SubmitDebugLogActivity extends BaseActivity {
|
||||||
|
|
||||||
private static final int CODE_SAVE = 24601;
|
private static final int CODE_SAVE = 24601;
|
||||||
|
|
||||||
private RecyclerView lineList;
|
private WebView logWebView;
|
||||||
private SubmitDebugLogAdapter adapter;
|
|
||||||
private SubmitDebugLogViewModel viewModel;
|
private SubmitDebugLogViewModel viewModel;
|
||||||
|
|
||||||
private View warningBanner;
|
private View warningBanner;
|
||||||
@@ -58,6 +57,9 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
|
|||||||
private MenuItem searchMenuItem;
|
private MenuItem searchMenuItem;
|
||||||
private MenuItem saveMenuItem;
|
private MenuItem saveMenuItem;
|
||||||
|
|
||||||
|
private boolean isWebViewLoaded;
|
||||||
|
private boolean hasPresentedLines;
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
private final DynamicTheme dynamicTheme = new DynamicTheme();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -89,35 +91,33 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
|
|||||||
this.searchMenuItem = menu.findItem(R.id.menu_search);
|
this.searchMenuItem = menu.findItem(R.id.menu_search);
|
||||||
this.saveMenuItem = menu.findItem(R.id.menu_save);
|
this.saveMenuItem = menu.findItem(R.id.menu_save);
|
||||||
|
|
||||||
SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
// TODO [lisa][debug-log-search]
|
||||||
SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() {
|
// SearchView searchView = (SearchView) searchMenuItem.getActionView();
|
||||||
@Override
|
// SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() {
|
||||||
public boolean onQueryTextSubmit(String query) {
|
// @Override
|
||||||
viewModel.onQueryUpdated(query);
|
// public boolean onQueryTextSubmit(String query) {
|
||||||
return true;
|
// return true;
|
||||||
}
|
// }
|
||||||
|
//
|
||||||
@Override
|
// @Override
|
||||||
public boolean onQueryTextChange(String query) {
|
// public boolean onQueryTextChange(String query) {
|
||||||
viewModel.onQueryUpdated(query);
|
// return true;
|
||||||
return true;
|
// }
|
||||||
}
|
// };
|
||||||
};
|
//
|
||||||
|
// searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
||||||
searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() {
|
// @Override
|
||||||
@Override
|
// public boolean onMenuItemActionExpand(MenuItem item) {
|
||||||
public boolean onMenuItemActionExpand(MenuItem item) {
|
// searchView.setOnQueryTextListener(queryListener);
|
||||||
searchView.setOnQueryTextListener(queryListener);
|
// return true;
|
||||||
return true;
|
// }
|
||||||
}
|
//
|
||||||
|
// @Override
|
||||||
@Override
|
// public boolean onMenuItemActionCollapse(MenuItem item) {
|
||||||
public boolean onMenuItemActionCollapse(MenuItem item) {
|
// searchView.setOnQueryTextListener(null);
|
||||||
searchView.setOnQueryTextListener(null);
|
// return true;
|
||||||
viewModel.onSearchClosed();
|
// }
|
||||||
return true;
|
// });
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -165,13 +165,8 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLogDeleted(@NonNull LogLine logLine) {
|
|
||||||
viewModel.onLogDeleted(logLine);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initView() {
|
private void initView() {
|
||||||
this.lineList = findViewById(R.id.debug_log_lines);
|
this.logWebView = findViewById(R.id.debug_log_lines);
|
||||||
this.warningBanner = findViewById(R.id.debug_log_warning_banner);
|
this.warningBanner = findViewById(R.id.debug_log_warning_banner);
|
||||||
this.editBanner = findViewById(R.id.debug_log_edit_banner);
|
this.editBanner = findViewById(R.id.debug_log_edit_banner);
|
||||||
this.submitButton = findViewById(R.id.debug_log_submit_button);
|
this.submitButton = findViewById(R.id.debug_log_submit_button);
|
||||||
@@ -179,35 +174,16 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
|
|||||||
this.scrollToTopButton = findViewById(R.id.debug_log_scroll_to_top);
|
this.scrollToTopButton = findViewById(R.id.debug_log_scroll_to_top);
|
||||||
this.progressCard = findViewById(R.id.debug_log_progress_card);
|
this.progressCard = findViewById(R.id.debug_log_progress_card);
|
||||||
|
|
||||||
this.adapter = new SubmitDebugLogAdapter(this, viewModel.getPagingController());
|
DebugLogsViewer.initWebView(logWebView, this, () -> {
|
||||||
|
isWebViewLoaded = true;
|
||||||
this.lineList.setLayoutManager(new LinearLayoutManager(this));
|
presentLines((viewModel.getLines().getValue() != null) ? viewModel.getLines().getValue() : new ArrayList<>());
|
||||||
this.lineList.setAdapter(adapter);
|
});
|
||||||
this.lineList.setItemAnimator(null);
|
|
||||||
|
|
||||||
submitButton.setOnClickListener(v -> onSubmitClicked());
|
submitButton.setOnClickListener(v -> onSubmitClicked());
|
||||||
|
scrollToTopButton.setOnClickListener(v -> DebugLogsViewer.scrollToTop(logWebView));
|
||||||
|
scrollToBottomButton.setOnClickListener(v -> DebugLogsViewer.scrollToBottom(logWebView));
|
||||||
|
|
||||||
scrollToBottomButton.setOnClickListener(v -> lineList.scrollToPosition(adapter.getItemCount() - 1));
|
|
||||||
scrollToTopButton.setOnClickListener(v -> lineList.scrollToPosition(0));
|
|
||||||
|
|
||||||
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 (((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition() > 10) {
|
|
||||||
scrollToTopButton.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
scrollToTopButton.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.progressCard.setVisibility(View.VISIBLE);
|
this.progressCard.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initViewModel() {
|
private void initViewModel() {
|
||||||
@@ -217,21 +193,35 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void presentLines(@NonNull List<LogLine> lines) {
|
private void presentLines(@NonNull List<LogLine> lines) {
|
||||||
if (progressCard != null && lines.size() > 0) {
|
if (!isWebViewLoaded || hasPresentedLines) {
|
||||||
progressCard.setVisibility(View.GONE);
|
return;
|
||||||
|
|
||||||
warningBanner.setVisibility(View.VISIBLE);
|
|
||||||
submitButton.setVisibility(View.VISIBLE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
adapter.submitList(lines);
|
if (progressCard != null && lines.size() > 0) {
|
||||||
|
progressCard.setVisibility(View.GONE);
|
||||||
|
warningBanner.setVisibility(View.VISIBLE);
|
||||||
|
submitButton.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
hasPresentedLines = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
StringBuilder lineBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
for (LogLine line : lines) {
|
||||||
|
if (line == null) continue;
|
||||||
|
|
||||||
|
lineBuilder.append(String.format("%s\n", line.getText()));
|
||||||
|
}
|
||||||
|
|
||||||
|
DebugLogsViewer.presentLines(logWebView, lineBuilder.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void presentMode(@NonNull SubmitDebugLogViewModel.Mode mode) {
|
private void presentMode(@NonNull SubmitDebugLogViewModel.Mode mode) {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case NORMAL:
|
case NORMAL:
|
||||||
editBanner.setVisibility(View.GONE);
|
editBanner.setVisibility(View.GONE);
|
||||||
adapter.setEditing(false);
|
// TODO [lisa][debug-log-editing]
|
||||||
|
// setEditing(false);
|
||||||
saveMenuItem.setVisible(true);
|
saveMenuItem.setVisible(true);
|
||||||
// TODO [greyson][log] Not yet implemented
|
// TODO [greyson][log] Not yet implemented
|
||||||
// editMenuItem.setVisible(true);
|
// editMenuItem.setVisible(true);
|
||||||
@@ -240,7 +230,7 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
|
|||||||
break;
|
break;
|
||||||
case SUBMITTING:
|
case SUBMITTING:
|
||||||
editBanner.setVisibility(View.GONE);
|
editBanner.setVisibility(View.GONE);
|
||||||
adapter.setEditing(false);
|
// setEditing(false);
|
||||||
editMenuItem.setVisible(false);
|
editMenuItem.setVisible(false);
|
||||||
doneMenuItem.setVisible(false);
|
doneMenuItem.setVisible(false);
|
||||||
searchMenuItem.setVisible(false);
|
searchMenuItem.setVisible(false);
|
||||||
@@ -248,7 +238,7 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
|
|||||||
break;
|
break;
|
||||||
case EDIT:
|
case EDIT:
|
||||||
editBanner.setVisibility(View.VISIBLE);
|
editBanner.setVisibility(View.VISIBLE);
|
||||||
adapter.setEditing(true);
|
// setEditing(true);
|
||||||
editMenuItem.setVisible(false);
|
editMenuItem.setVisible(false);
|
||||||
doneMenuItem.setVisible(true);
|
doneMenuItem.setVisible(true);
|
||||||
searchMenuItem.setVisible(true);
|
searchMenuItem.setVisible(true);
|
||||||
|
|||||||
@@ -1,188 +0,0 @@
|
|||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -11,13 +11,9 @@ import androidx.lifecycle.ViewModel;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
|
||||||
import org.signal.core.util.ThreadUtil;
|
import org.signal.core.util.ThreadUtil;
|
||||||
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
import org.signal.core.util.tracing.Tracer;
|
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.database.LogDatabase;
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
import org.thoughtcrime.securesms.dependencies.AppDependencies;
|
||||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||||
@@ -32,54 +28,51 @@ public class SubmitDebugLogViewModel extends ViewModel {
|
|||||||
|
|
||||||
private final SubmitDebugLogRepository repo;
|
private final SubmitDebugLogRepository repo;
|
||||||
private final MutableLiveData<Mode> mode;
|
private final MutableLiveData<Mode> mode;
|
||||||
private final ProxyPagingController<Long> pagingController;
|
|
||||||
private final List<LogLine> staticLines;
|
private final List<LogLine> staticLines;
|
||||||
private final MediatorLiveData<List<LogLine>> lines;
|
private final MediatorLiveData<List<LogLine>> lines;
|
||||||
private final SingleLiveEvent<Event> event;
|
private final SingleLiveEvent<Event> event;
|
||||||
private final long firstViewTime;
|
private final long firstViewTime;
|
||||||
private final byte[] trace;
|
private final byte[] trace;
|
||||||
|
private final List<LogLine> allLines;
|
||||||
|
|
||||||
private SubmitDebugLogViewModel() {
|
private SubmitDebugLogViewModel() {
|
||||||
this.repo = new SubmitDebugLogRepository();
|
this.repo = new SubmitDebugLogRepository();
|
||||||
this.mode = new MutableLiveData<>();
|
this.mode = new MutableLiveData<>();
|
||||||
this.trace = Tracer.getInstance().serialize();
|
this.trace = Tracer.getInstance().serialize();
|
||||||
this.pagingController = new ProxyPagingController<>();
|
|
||||||
this.firstViewTime = System.currentTimeMillis();
|
this.firstViewTime = System.currentTimeMillis();
|
||||||
this.staticLines = new ArrayList<>();
|
this.staticLines = new ArrayList<>();
|
||||||
this.lines = new MediatorLiveData<>();
|
this.lines = new MediatorLiveData<>();
|
||||||
this.event = new SingleLiveEvent<>();
|
this.event = new SingleLiveEvent<>();
|
||||||
|
this.allLines = new ArrayList<>();
|
||||||
|
|
||||||
repo.getPrefixLogLines(staticLines -> {
|
repo.getPrefixLogLines(staticLines -> {
|
||||||
this.staticLines.addAll(staticLines);
|
this.staticLines.addAll(staticLines);
|
||||||
|
|
||||||
Log.blockUntilAllWritesFinished();
|
Log.blockUntilAllWritesFinished();
|
||||||
LogDatabase.getInstance(AppDependencies.getApplication()).logs().trimToSize();
|
LogDatabase.getInstance(AppDependencies.getApplication()).logs().trimToSize();
|
||||||
|
SignalExecutors.UNBOUNDED.execute(() -> {
|
||||||
|
allLines.clear();
|
||||||
|
allLines.addAll(staticLines);
|
||||||
|
|
||||||
LogDataSource dataSource = new LogDataSource(AppDependencies.getApplication(), staticLines, firstViewTime);
|
try (LogDatabase.LogTable.CursorReader logReader = (LogDatabase.LogTable.CursorReader) LogDatabase.getInstance(AppDependencies.getApplication()).logs().getAllBeforeTime(firstViewTime)) {
|
||||||
PagingConfig config = new PagingConfig.Builder().setPageSize(100)
|
while (logReader.hasNext()) {
|
||||||
.setBufferPages(3)
|
String next = logReader.next();
|
||||||
.setStartIndex(0)
|
allLines.add(new SimpleLogLine(next, LogStyleParser.parseStyle(next), LogLine.Placeholder.NONE));
|
||||||
.build();
|
}
|
||||||
|
}
|
||||||
LivePagedData<Long, LogLine> pagedData = PagedData.createForLiveData(dataSource, config);
|
|
||||||
|
|
||||||
ThreadUtil.runOnMain(() -> {
|
ThreadUtil.runOnMain(() -> {
|
||||||
pagingController.set(pagedData.getController());
|
lines.setValue(allLines);
|
||||||
lines.addSource(pagedData.getData(), lines::setValue);
|
|
||||||
mode.setValue(Mode.NORMAL);
|
mode.setValue(Mode.NORMAL);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull LiveData<List<LogLine>> getLines() {
|
@NonNull LiveData<List<LogLine>> getLines() {
|
||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull PagingController getPagingController() {
|
|
||||||
return pagingController;
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull LiveData<Mode> getMode() {
|
@NonNull LiveData<Mode> getMode() {
|
||||||
return mode;
|
return mode;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
|
|||||||
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord.ActionId
|
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord.ActionId
|
||||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||||
import org.thoughtcrime.securesms.megaphone.RemoteMegaphoneRepository.Action
|
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.LocaleRemoteConfig
|
import org.thoughtcrime.securesms.util.LocaleRemoteConfig
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ public final class ContextUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implementation "borrowed" from com.airbnb.lottie.utils.Utils#getAnimationScale(android.content.Context)
|
* Implementation "borrowed" from com.airbnb.lottie.utils.DebugLogViewer#getAnimationScale(android.content.Context)
|
||||||
*/
|
*/
|
||||||
public static float getAnimationScale(Context context) {
|
public static float getAnimationScale(Context context) {
|
||||||
return Settings.Global.getFloat(context.getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f);
|
return Settings.Global.getFloat(context.getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f);
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
app:barrierDirection="bottom"
|
app:barrierDirection="bottom"
|
||||||
app:constraint_referenced_ids="debug_log_warning_banner,debug_log_edit_banner" />
|
app:constraint_referenced_ids="debug_log_warning_banner,debug_log_edit_banner" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<WebView
|
||||||
android:id="@+id/debug_log_lines"
|
android:id="@+id/debug_log_lines"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
|||||||
17
debuglogs-viewer/app/build.gradle.kts
Normal file
17
debuglogs-viewer/app/build.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
plugins {
|
||||||
|
id("signal-sample-app")
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "org.signal.debuglogsviewer.app"
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "org.signal.debuglogsviewer.app"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":debuglogs-viewer"))
|
||||||
|
implementation(project(":core-util"))
|
||||||
|
}
|
||||||
26
debuglogs-viewer/app/src/main/AndroidManifest.xml
Normal file
26
debuglogs-viewer/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:allowBackup="true"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:supportsRtl="true">
|
||||||
|
<activity android:name=".MainActivity"
|
||||||
|
android:theme="@style/Theme.DebugLogs"
|
||||||
|
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
|
||||||
|
android:exported="true">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.wallet.api.enabled"
|
||||||
|
android:value="true" />
|
||||||
|
</application>
|
||||||
|
|
||||||
|
</manifest>
|
||||||
59
debuglogs-viewer/app/src/main/assets/WebView.html
Normal file
59
debuglogs-viewer/app/src/main/assets/WebView.html
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright 2025 Signal Messenger, LLC
|
||||||
|
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
#container { position: absolute; top: 50px; bottom: 0; left: 0; right: 0; }
|
||||||
|
#searchBar { position: fixed; top: 0; left: 0; width: 350px; display: none; padding: 10px; gap: 5px; z-index: 1; }
|
||||||
|
#searchBar input { flex-grow: 1; padding: 5px ;}
|
||||||
|
.searchMatches { position: absolute; background-color: rgba(182, 190, 250, 0.2); }
|
||||||
|
#caseSensitiveButton.active { background-color: rgba(182, 190, 250, 0.2); }
|
||||||
|
#matchCount { font-size: 10px; }
|
||||||
|
#filterLevel { display: none; }
|
||||||
|
|
||||||
|
.ace_editor { background-color: #FBFCFF; }
|
||||||
|
.ace_none { color: #000000; }
|
||||||
|
.ace_verbose { color: #515151; }
|
||||||
|
.ace_debug { color: #089314; }
|
||||||
|
.ace_info { color: #0a7087; }
|
||||||
|
.ace_warning { color: #b58c12; }
|
||||||
|
.ace_error { color: #af0d0a; }
|
||||||
|
|
||||||
|
body.dark .ace_editor { background-color: #1B1C1F; }
|
||||||
|
body.dark .ace_none { color: #ffffff; }
|
||||||
|
body.dark .ace_verbose { color: #8a8a8a; }
|
||||||
|
body.dark .ace_debug { color: #5ca72b; }
|
||||||
|
body.dark .ace_info { color: #46bbb9; }
|
||||||
|
body.dark .ace_warning { color: #cdd637; }
|
||||||
|
body.dark .ace_error { color: #ff6b68; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="searchBar">
|
||||||
|
<button id="cancelButton"> x </button>
|
||||||
|
<input type="text" id="searchInput" placeholder="Search" />
|
||||||
|
<button id="prevButton"> < </button>
|
||||||
|
<button id="nextButton"> > </button>
|
||||||
|
<button id="caseSensitiveButton"> Cc </button>
|
||||||
|
<div id="matchCount"> No match </div>
|
||||||
|
</div>
|
||||||
|
<div id="filterLevel">
|
||||||
|
<div class="filterLevelMenu">
|
||||||
|
<label><input type="checkbox" value=" V ">Verbose</label>
|
||||||
|
<label><input type="checkbox" value=" D ">Debug</label>
|
||||||
|
<label><input type="checkbox" value=" I ">Info</label>
|
||||||
|
<label><input type="checkbox" value=" W ">Warning</label>
|
||||||
|
<label><input type="checkbox" value=" E ">Error</label>
|
||||||
|
<label><input type="checkbox" value="SignalUncaughtException">SignalUncaughtException</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="container">Loading...</div>
|
||||||
|
<script src="https://www.unpkg.com/ace-builds@latest/src-noconflict/ace.js"></script>
|
||||||
|
<script src="WebView.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
171
debuglogs-viewer/app/src/main/assets/WebView.js
Normal file
171
debuglogs-viewer/app/src/main/assets/WebView.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
// Create custom text mode to color different levels of debug log lines
|
||||||
|
const TextMode = ace.require("ace/mode/text").Mode;
|
||||||
|
const TextHighlightRules = ace.require("ace/mode/text_highlight_rules").TextHighlightRules;
|
||||||
|
|
||||||
|
function CustomHighlightRules() {
|
||||||
|
this.$rules = {
|
||||||
|
start: [
|
||||||
|
{ token: "verbose", regex: "^.*\\sV\\s.*$" },
|
||||||
|
{ token: "debug", regex: "^.*\\sD\\s.*$" },
|
||||||
|
{ token: "info", regex: "^.*\\sI\\s.*$" },
|
||||||
|
{ token: "warning", regex: "^.*\\sW\\s.*$" },
|
||||||
|
{ token: "error", regex: "^.*\\sE\\s.*$" },
|
||||||
|
{ token: "none", regex: ".*" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomHighlightRules.prototype = new TextHighlightRules();
|
||||||
|
|
||||||
|
function CustomMode() {
|
||||||
|
TextMode.call(this);
|
||||||
|
this.HighlightRules = CustomHighlightRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomMode.prototype = Object.create(TextMode.prototype);
|
||||||
|
CustomMode.prototype.constructor = CustomMode;
|
||||||
|
|
||||||
|
// Create Ace Editor using the custom mode
|
||||||
|
let editor = ace.edit("container", {
|
||||||
|
mode: new CustomMode(),
|
||||||
|
theme: "ace/theme/textmate",
|
||||||
|
wrap: false, // Allow for horizontal scrolling
|
||||||
|
readOnly: true,
|
||||||
|
showGutter: false,
|
||||||
|
highlightActiveLine: false,
|
||||||
|
highlightSelectedWord: false, // Prevent Ace Editor from automatically highlighting all instances of a selected word (really laggy!)
|
||||||
|
showPrintMargin: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get search bar functionalities
|
||||||
|
const input = document.getElementById("searchInput");
|
||||||
|
const prevButton = document.getElementById("prevButton");
|
||||||
|
const nextButton = document.getElementById("nextButton");
|
||||||
|
const cancelButton = document.getElementById("cancelButton");
|
||||||
|
const caseSensitiveButton = document.getElementById("caseSensitiveButton");
|
||||||
|
|
||||||
|
// Generate highlight markers for all search matches
|
||||||
|
const Range = ace.require("ace/range").Range;
|
||||||
|
const session = editor.getSession();
|
||||||
|
|
||||||
|
let markers = []; // IDs of highlighted search markers
|
||||||
|
let matchRanges = []; // Ranges of all search matches
|
||||||
|
let matchCount = 0; // Total number of matches
|
||||||
|
let caseSensitive = false;
|
||||||
|
|
||||||
|
// Clear all search markers and match info
|
||||||
|
function clearMarkers() {
|
||||||
|
markers.forEach((id) => session.removeMarker(id));
|
||||||
|
markers = [];
|
||||||
|
matchRanges = [];
|
||||||
|
matchCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight all instances of the search term
|
||||||
|
function highlightAllMatches(term) {
|
||||||
|
clearMarkers();
|
||||||
|
if (!term) {
|
||||||
|
updateMatchPosition();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchTerm = caseSensitive ? term : term.toLowerCase();
|
||||||
|
session
|
||||||
|
.getDocument()
|
||||||
|
.getAllLines()
|
||||||
|
.forEach((line, row) => {
|
||||||
|
let start = 0;
|
||||||
|
const caseLine = caseSensitive ? line : line.toLowerCase();
|
||||||
|
while (true) {
|
||||||
|
const index = caseLine.indexOf(searchTerm, start);
|
||||||
|
if (index === -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const range = new Range(row, index, row, index + term.length);
|
||||||
|
markers.push(session.addMarker(range, "searchMatches", "text", false));
|
||||||
|
matchRanges.push(range);
|
||||||
|
start = index + term.length;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
matchCount = markers.length;
|
||||||
|
updateMatchPosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
input.addEventListener("input", () => highlightAllMatches(input.value));
|
||||||
|
|
||||||
|
// Return index of current match
|
||||||
|
function getCurrentMatchIndex() {
|
||||||
|
const current = editor.getSelection().getRange();
|
||||||
|
return matchRanges.findIndex(
|
||||||
|
(r) =>
|
||||||
|
r.start.row === current.start.row &&
|
||||||
|
r.start.column === current.start.column &&
|
||||||
|
r.end.row === current.end.row &&
|
||||||
|
r.end.column === current.end.column,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the display for current match
|
||||||
|
function updateMatchPosition() {
|
||||||
|
document.getElementById("matchCount").textContent = matchCount == 0 ? "No match" : `${getCurrentMatchIndex() + 1} / ${matchCount}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
prevButton.onclick = () => {
|
||||||
|
editor.find(input.value, {
|
||||||
|
backwards: true,
|
||||||
|
wrap: true,
|
||||||
|
skipCurrent: true,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
});
|
||||||
|
updateMatchPosition();
|
||||||
|
};
|
||||||
|
|
||||||
|
nextButton.onclick = () => {
|
||||||
|
editor.find(input.value, {
|
||||||
|
backwards: false,
|
||||||
|
wrap: true,
|
||||||
|
skipCurrent: true,
|
||||||
|
caseSensitive: caseSensitive,
|
||||||
|
});
|
||||||
|
updateMatchPosition();
|
||||||
|
};
|
||||||
|
|
||||||
|
cancelButton.onclick = () => {
|
||||||
|
editor.getSelection().clearSelection();
|
||||||
|
input.value = "";
|
||||||
|
clearMarkers();
|
||||||
|
updateMatchPosition();
|
||||||
|
document.getElementById("searchBar").style.display = "none";
|
||||||
|
};
|
||||||
|
|
||||||
|
caseSensitiveButton.onclick = () => {
|
||||||
|
caseSensitive = !caseSensitive;
|
||||||
|
highlightAllMatches(input.value);
|
||||||
|
caseSensitiveButton.classList.toggle("active", caseSensitive);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Filter by log levels
|
||||||
|
let logLines = "";
|
||||||
|
function filterLogs() {
|
||||||
|
const selectedLevels = Array.from(document.querySelectorAll('input[type="checkbox"]:checked')).map((cb) => cb.value);
|
||||||
|
|
||||||
|
if (selectedLevels.length === 0) {
|
||||||
|
// If no level is selected, show all
|
||||||
|
editor.setValue(logLines, -1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = logLines
|
||||||
|
.split("\n")
|
||||||
|
.filter((line) => {
|
||||||
|
return selectedLevels.some((level) => line.includes(level));
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
editor.setValue(filtered, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
|
||||||
|
cb.addEventListener("change", filterLogs);
|
||||||
|
});
|
||||||
0
debuglogs-viewer/app/src/main/assets/log.txt
Normal file
0
debuglogs-viewer/app/src/main/assets/log.txt
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.debuglogsviewer.app
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.widget.Button
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import org.signal.debuglogsviewer.app.webview.setupWebView
|
||||||
|
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
setContentView(R.layout.activity_main)
|
||||||
|
|
||||||
|
val webview: WebView = findViewById(R.id.webview)
|
||||||
|
val findButton: Button = findViewById(R.id.findButton)
|
||||||
|
val filterLevelButton: Button = findViewById(R.id.filterLevelButton)
|
||||||
|
val editButton: Button = findViewById(R.id.editButton)
|
||||||
|
val cancelEditButton: Button = findViewById(R.id.cancelEditButton)
|
||||||
|
val copyButton: Button = findViewById(R.id.copyButton)
|
||||||
|
|
||||||
|
setupWebView(this, webview, findButton, filterLevelButton, editButton, cancelEditButton, copyButton)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.debuglogsviewer.app.webview
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.view.View
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import android.widget.Button
|
||||||
|
import org.signal.debuglogsviewer.app.R
|
||||||
|
|
||||||
|
fun setupWebView(
|
||||||
|
context: Context,
|
||||||
|
webview: WebView,
|
||||||
|
findButton: Button,
|
||||||
|
filterLevelButton: Button,
|
||||||
|
editButton: Button,
|
||||||
|
cancelEditButton: Button,
|
||||||
|
copyButton: Button
|
||||||
|
) {
|
||||||
|
val originalContent = org.json.JSONObject.quote(getLogText(context))
|
||||||
|
var readOnly = true
|
||||||
|
|
||||||
|
webview.settings.apply {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
builtInZoomControls = true
|
||||||
|
displayZoomControls = false
|
||||||
|
}
|
||||||
|
|
||||||
|
webview.loadUrl("file:///android_asset/debuglogs-viewer.html")
|
||||||
|
|
||||||
|
webview.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
// Set the debug log lines
|
||||||
|
webview.evaluateJavascript("editor.setValue($originalContent, -1); logLines = $originalContent;", null)
|
||||||
|
|
||||||
|
// Set dark mode colors if in dark mode
|
||||||
|
val isDarkMode = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
if (isDarkMode) {
|
||||||
|
webview.evaluateJavascript("document.body.classList.add('dark');", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
findButton.setOnClickListener {
|
||||||
|
webview.evaluateJavascript("document.getElementById('searchBar').style.display = 'block';", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
filterLevelButton.setOnClickListener {
|
||||||
|
webview.evaluateJavascript("document.getElementById('filterLevel').style.display = 'block';", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
editButton.setOnClickListener {
|
||||||
|
readOnly = !readOnly
|
||||||
|
cancelEditButton.visibility = if (!readOnly) View.VISIBLE else View.GONE
|
||||||
|
editButton.text = if (readOnly) "Enable Edit" else "Save Edit"
|
||||||
|
webview.evaluateJavascript("editor.setReadOnly($readOnly);", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelEditButton.setOnClickListener {
|
||||||
|
readOnly = !readOnly
|
||||||
|
cancelEditButton.visibility = View.GONE
|
||||||
|
editButton.text = if (readOnly) "Enable Edit" else "Save Edit"
|
||||||
|
webview.evaluateJavascript("editor.setReadOnly($readOnly);", null)
|
||||||
|
webview.evaluateJavascript("editor.setValue($originalContent, -1);", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
copyButton.setOnClickListener { // In Signal app, use Util.writeTextToClipboard(context, value) instead
|
||||||
|
webview.evaluateJavascript ("editor.getValue();") { value ->
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText(context.getString(R.string.app_name), value)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLogText(context: Context): String {
|
||||||
|
return try {
|
||||||
|
context.assets.open("log.txt").bufferedReader().use { it.readText() }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
"Error loading file: ${e.message}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:aapt="http://schemas.android.com/aapt"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
|
||||||
|
<aapt:attr name="android:fillColor">
|
||||||
|
<gradient
|
||||||
|
android:endX="85.84757"
|
||||||
|
android:endY="92.4963"
|
||||||
|
android:startX="42.9492"
|
||||||
|
android:startY="49.59793"
|
||||||
|
android:type="linear">
|
||||||
|
<item
|
||||||
|
android:color="#44000000"
|
||||||
|
android:offset="0.0" />
|
||||||
|
<item
|
||||||
|
android:color="#00000000"
|
||||||
|
android:offset="1.0" />
|
||||||
|
</gradient>
|
||||||
|
</aapt:attr>
|
||||||
|
</path>
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:fillType="nonZero"
|
||||||
|
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:strokeColor="#00000000" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="108dp"
|
||||||
|
android:height="108dp"
|
||||||
|
android:viewportWidth="108"
|
||||||
|
android:viewportHeight="108">
|
||||||
|
<path
|
||||||
|
android:fillColor="#3DDC84"
|
||||||
|
android:pathData="M0,0h108v108h-108z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M9,0L9,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,0L19,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,0L29,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,0L39,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,0L49,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,0L59,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,0L69,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,0L79,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M89,0L89,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M99,0L99,108"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,9L108,9"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,19L108,19"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,29L108,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,39L108,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,49L108,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,59L108,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,69L108,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,79L108,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,89L108,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M0,99L108,99"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,29L89,29"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,39L89,39"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,49L89,49"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,59L89,59"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,69L89,69"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M19,79L89,79"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M29,19L29,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M39,19L39,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M49,19L49,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M59,19L59,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M69,19L69,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#00000000"
|
||||||
|
android:pathData="M79,19L79,89"
|
||||||
|
android:strokeWidth="0.8"
|
||||||
|
android:strokeColor="#33FFFFFF" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:viewportWidth="20"
|
||||||
|
android:viewportHeight="20">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M14.7,5.3A6.7,6.7 0,1 0,10 16.7a6.7,6.7 0,0 0,6.4 -5H14.7A5,5 0,1 1,10 5a4.9,4.9 0,0 1,3.5 1.5L10.8,9.2h5.9V3.3Z"/>
|
||||||
|
</vector>
|
||||||
64
debuglogs-viewer/app/src/main/res/layout/activity_main.xml
Normal file
64
debuglogs-viewer/app/src/main/res/layout/activity_main.xml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!--
|
||||||
|
~ Copyright 2025 Signal Messenger, LLC
|
||||||
|
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/buttonRow"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_gravity="top|end"
|
||||||
|
android:padding="10dp"
|
||||||
|
android:layout_marginTop="25dp"
|
||||||
|
android:gravity="end">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/findButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:text="Find" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/filterLevelButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:text="Filter by Level" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/editButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:text="Enable Edit" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/cancelEditButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:text="Cancel Edit"
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/copyButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:minWidth="0dp"
|
||||||
|
android:text="Copy" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<WebView
|
||||||
|
android:id="@+id/webview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
</resources>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.DebugLogs" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
3
debuglogs-viewer/app/src/main/res/values/colors.xml
Normal file
3
debuglogs-viewer/app/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
</resources>
|
||||||
3
debuglogs-viewer/app/src/main/res/values/strings.xml
Normal file
3
debuglogs-viewer/app/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
<resources>
|
||||||
|
<string name="app_name" translatable="false">DebugLogs Viewer</string>
|
||||||
|
</resources>
|
||||||
8
debuglogs-viewer/app/src/main/res/values/themes.xml
Normal file
8
debuglogs-viewer/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<!-- Base application theme. -->
|
||||||
|
<style name="Theme.DebugLogs" parent="Theme.MaterialComponents.DayNight.NoActionBar">
|
||||||
|
<!-- Status bar color. -->
|
||||||
|
<item name="android:statusBarColor" tools:targetApi="l">?attr/colorPrimaryVariant</item>
|
||||||
|
<!-- Customize your theme here. -->
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
27
debuglogs-viewer/lib/build.gradle.kts
Normal file
27
debuglogs-viewer/lib/build.gradle.kts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
plugins {
|
||||||
|
id("signal-library")
|
||||||
|
id("kotlin-parcelize")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "org.signal.debuglogsviewer"
|
||||||
|
|
||||||
|
buildFeatures {
|
||||||
|
buildConfig = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":core-util"))
|
||||||
|
|
||||||
|
implementation(libs.kotlin.reflect)
|
||||||
|
implementation(libs.jackson.module.kotlin)
|
||||||
|
implementation(libs.jackson.core)
|
||||||
|
|
||||||
|
testImplementation(testLibs.robolectric.robolectric) {
|
||||||
|
exclude(group = "com.google.protobuf", module = "protobuf-java")
|
||||||
|
}
|
||||||
|
|
||||||
|
api(libs.google.play.services.wallet)
|
||||||
|
api(libs.square.okhttp3)
|
||||||
|
}
|
||||||
5
debuglogs-viewer/lib/src/main/AndroidManifest.xml
Normal file
5
debuglogs-viewer/lib/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
</manifest>
|
||||||
42
debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.html
Normal file
42
debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.html
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright 2025 Signal Messenger, LLC
|
||||||
|
~ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<style>
|
||||||
|
#container { position: absolute; top: 0; bottom: 0; left: 0; right: 0; height: 100%; width: 100%; }
|
||||||
|
|
||||||
|
/* Scrollbar Setup */
|
||||||
|
.ace_scrollbar::-webkit-scrollbar { width: 0; height: 0; }
|
||||||
|
.show-scrollbar .ace_scrollbar::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||||
|
.ace_scrollbar::-webkit-scrollbar-thumb { background-color: #999999; }
|
||||||
|
|
||||||
|
/* Light Mode: Line color based on log level */
|
||||||
|
.ace_editor { background-color: #FBFCFF; }
|
||||||
|
.ace_none { color: #000000; }
|
||||||
|
.ace_verbose { color: #515151; }
|
||||||
|
.ace_debug { color: #089314; }
|
||||||
|
.ace_info { color: #0a7087; }
|
||||||
|
.ace_warning { color: #b58c12; }
|
||||||
|
.ace_error { color: #af0d0a; }
|
||||||
|
|
||||||
|
/* Dark Mode: Line color based on log level */
|
||||||
|
body.dark .ace_editor { background-color: #1B1C1F; }
|
||||||
|
body.dark .ace_none { color: #ffffff; }
|
||||||
|
body.dark .ace_verbose { color: #8a8a8a; }
|
||||||
|
body.dark .ace_debug { color: #5ca72b; }
|
||||||
|
body.dark .ace_info { color: #46bbb9; }
|
||||||
|
body.dark .ace_warning { color: #cdd637; }
|
||||||
|
body.dark .ace_error { color: #ff6b68; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="container"></div>
|
||||||
|
<script src="https://www.unpkg.com/ace-builds@latest/src-noconflict/ace.js"></script>
|
||||||
|
<script src="debuglogs-viewer.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
49
debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.js
Normal file
49
debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.js
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
// Create custom text mode to color different levels of debug log lines
|
||||||
|
const TextMode = ace.require("ace/mode/text").Mode;
|
||||||
|
const TextHighlightRules = ace.require("ace/mode/text_highlight_rules").TextHighlightRules;
|
||||||
|
|
||||||
|
function CustomHighlightRules() {
|
||||||
|
this.$rules = {
|
||||||
|
start: [
|
||||||
|
{ token: "verbose", regex: "^.*\\sV\\s.*$" },
|
||||||
|
{ token: "debug", regex: "^.*\\sD\\s.*$" },
|
||||||
|
{ token: "info", regex: "^.*\\sI\\s.*$" },
|
||||||
|
{ token: "warning", regex: "^.*\\sW\\s.*$" },
|
||||||
|
{ token: "error", regex: "^.*\\sE\\s.*$" },
|
||||||
|
{ token: "none", regex: ".*" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomHighlightRules.prototype = new TextHighlightRules();
|
||||||
|
|
||||||
|
function CustomMode() {
|
||||||
|
TextMode.call(this);
|
||||||
|
this.HighlightRules = CustomHighlightRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
CustomMode.prototype = Object.create(TextMode.prototype);
|
||||||
|
CustomMode.prototype.constructor = CustomMode;
|
||||||
|
|
||||||
|
// Create Ace Editor using the custom mode
|
||||||
|
let editor = ace.edit("container", {
|
||||||
|
mode: new CustomMode(),
|
||||||
|
theme: "ace/theme/textmate",
|
||||||
|
wrap: false, // Allow for horizontal scrolling
|
||||||
|
readOnly: true,
|
||||||
|
showGutter: false,
|
||||||
|
highlightActiveLine: false,
|
||||||
|
highlightSelectedWord: false, // Prevent Ace Editor from automatically highlighting all instances of a selected word (really laggy!)
|
||||||
|
showPrintMargin: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Show scrollbar that fades after a second since last scroll
|
||||||
|
let timeout;
|
||||||
|
function showScrollBar() {
|
||||||
|
editor.container.classList.add("show-scrollbar");
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => editor.container.classList.remove("show-scrollbar"), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
editor.session.on("changeScrollTop", showScrollBar);
|
||||||
|
editor.session.on("changeScrollLeft", showScrollBar);
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2025 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.debuglogsviewer
|
||||||
|
|
||||||
|
import android.content.ClipData
|
||||||
|
import android.content.ClipboardManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import kotlinx.coroutines.Runnable
|
||||||
|
import org.json.JSONObject
|
||||||
|
|
||||||
|
var readOnly = true
|
||||||
|
|
||||||
|
object DebugLogsViewer {
|
||||||
|
@JvmStatic
|
||||||
|
fun initWebView(webview: WebView, context: Context, onFinished: Runnable) {
|
||||||
|
webview.settings.apply {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
builtInZoomControls = true
|
||||||
|
displayZoomControls = false
|
||||||
|
}
|
||||||
|
webview.isVerticalScrollBarEnabled = false
|
||||||
|
webview.isHorizontalScrollBarEnabled = false
|
||||||
|
|
||||||
|
webview.loadUrl("file:///android_asset/debuglogs-viewer.html")
|
||||||
|
|
||||||
|
webview.webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
// Set dark mode colors if in dark mode
|
||||||
|
val isDarkMode = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
if (isDarkMode) {
|
||||||
|
webview.evaluateJavascript("document.body.classList.add('dark');", null)
|
||||||
|
}
|
||||||
|
onFinished.run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun presentLines(webview: WebView, lines: String) {
|
||||||
|
// Set the debug log lines
|
||||||
|
val escaped = JSONObject.quote(lines)
|
||||||
|
webview.evaluateJavascript("editor.insert($escaped);", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun scrollToTop(webview: WebView) {
|
||||||
|
webview.evaluateJavascript("editor.scrollToRow(0);", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun scrollToBottom(webview: WebView) {
|
||||||
|
webview.evaluateJavascript("editor.scrollToRow(editor.session.getLength() - 1);", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun onFind(webview: WebView) {
|
||||||
|
webview.evaluateJavascript("document.getElementById('searchBar').style.display = 'block';", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun onFilter(webview: WebView) {
|
||||||
|
webview.evaluateJavascript("document.getElementById('filterLevel').style.display = 'block';", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun onEdit(webview: WebView) {
|
||||||
|
readOnly = !readOnly
|
||||||
|
webview.evaluateJavascript("editor.setReadOnly($readOnly);", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun onCancelEdit(webview: WebView, lines: String) {
|
||||||
|
readOnly = !readOnly
|
||||||
|
webview.evaluateJavascript("editor.setReadOnly($readOnly);", null)
|
||||||
|
webview.evaluateJavascript("editor.setValue($lines, -1);", null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun onCopy(webview: WebView, context: Context, appName: String) {
|
||||||
|
webview.evaluateJavascript ("editor.getValue();") { value ->
|
||||||
|
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||||
|
val clip = ClipData.newPlainText(appName, value)
|
||||||
|
clipboard.setPrimaryClip(clip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -68,6 +68,8 @@ include(":image-editor")
|
|||||||
include(":image-editor-app")
|
include(":image-editor-app")
|
||||||
include(":donations")
|
include(":donations")
|
||||||
include(":donations-app")
|
include(":donations-app")
|
||||||
|
include(":debuglogs-viewer")
|
||||||
|
include(":debuglogs-viewer-app")
|
||||||
include(":spinner")
|
include(":spinner")
|
||||||
include(":spinner-app")
|
include(":spinner-app")
|
||||||
include(":contacts")
|
include(":contacts")
|
||||||
@@ -96,6 +98,9 @@ project(":image-editor-app").projectDir = file("image-editor/app")
|
|||||||
project(":donations").projectDir = file("donations/lib")
|
project(":donations").projectDir = file("donations/lib")
|
||||||
project(":donations-app").projectDir = file("donations/app")
|
project(":donations-app").projectDir = file("donations/app")
|
||||||
|
|
||||||
|
project(":debuglogs-viewer").projectDir = file("debuglogs-viewer/lib")
|
||||||
|
project(":debuglogs-viewer-app").projectDir = file("debuglogs-viewer/app")
|
||||||
|
|
||||||
project(":spinner").projectDir = file("spinner/lib")
|
project(":spinner").projectDir = file("spinner/lib")
|
||||||
project(":spinner-app").projectDir = file("spinner/app")
|
project(":spinner-app").projectDir = file("spinner/app")
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user