diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6512b7ff89..075cf7ca95 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -501,6 +501,7 @@ dependencies { implementation(project(":device-transfer")) implementation(project(":image-editor")) implementation(project(":donations")) + implementation(project(":debuglogs-viewer")) implementation(project(":contacts")) implementation(project(":qr")) implementation(project(":sticky-header-grid")) diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt deleted file mode 100644 index 374e36d5c1..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt +++ /dev/null @@ -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, - private val untilTime: Long -) : - PagedDataSource { - - 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 { - 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) - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java index f41aa1c6f6..68199b6f90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java @@ -11,21 +11,20 @@ import android.text.util.Linkify; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.webkit.WebView; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.widget.SearchView; import androidx.core.app.ShareCompat; 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.signal.debuglogsviewer.DebugLogsViewer; import org.thoughtcrime.securesms.BaseActivity; import org.thoughtcrime.securesms.R; 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.views.CircularProgressMaterialButton; +import java.util.ArrayList; 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 RecyclerView lineList; - private SubmitDebugLogAdapter adapter; + private WebView logWebView; private SubmitDebugLogViewModel viewModel; private View warningBanner; @@ -58,6 +57,9 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL private MenuItem searchMenuItem; private MenuItem saveMenuItem; + private boolean isWebViewLoaded; + private boolean hasPresentedLines; + private final DynamicTheme dynamicTheme = new DynamicTheme(); @Override @@ -89,35 +91,33 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL this.searchMenuItem = menu.findItem(R.id.menu_search); this.saveMenuItem = menu.findItem(R.id.menu_save); - SearchView searchView = (SearchView) searchMenuItem.getActionView(); - SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() { - @Override - public boolean onQueryTextSubmit(String query) { - viewModel.onQueryUpdated(query); - return true; - } - - @Override - public boolean onQueryTextChange(String query) { - viewModel.onQueryUpdated(query); - return true; - } - }; - - searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { - @Override - public boolean onMenuItemActionExpand(MenuItem item) { - searchView.setOnQueryTextListener(queryListener); - return true; - } - - @Override - public boolean onMenuItemActionCollapse(MenuItem item) { - searchView.setOnQueryTextListener(null); - viewModel.onSearchClosed(); - return true; - } - }); + // TODO [lisa][debug-log-search] +// SearchView searchView = (SearchView) searchMenuItem.getActionView(); +// SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() { +// @Override +// public boolean onQueryTextSubmit(String query) { +// return true; +// } +// +// @Override +// public boolean onQueryTextChange(String query) { +// return true; +// } +// }; +// +// searchMenuItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { +// @Override +// public boolean onMenuItemActionExpand(MenuItem item) { +// searchView.setOnQueryTextListener(queryListener); +// return true; +// } +// +// @Override +// public boolean onMenuItemActionCollapse(MenuItem item) { +// searchView.setOnQueryTextListener(null); +// 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() { - 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.editBanner = findViewById(R.id.debug_log_edit_banner); 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.progressCard = findViewById(R.id.debug_log_progress_card); - this.adapter = new SubmitDebugLogAdapter(this, viewModel.getPagingController()); - - this.lineList.setLayoutManager(new LinearLayoutManager(this)); - this.lineList.setAdapter(adapter); - this.lineList.setItemAnimator(null); + DebugLogsViewer.initWebView(logWebView, this, () -> { + isWebViewLoaded = true; + presentLines((viewModel.getLines().getValue() != null) ? viewModel.getLines().getValue() : new ArrayList<>()); + }); 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); - } private void initViewModel() { @@ -217,21 +193,35 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL } private void presentLines(@NonNull List lines) { - if (progressCard != null && lines.size() > 0) { - progressCard.setVisibility(View.GONE); - - warningBanner.setVisibility(View.VISIBLE); - submitButton.setVisibility(View.VISIBLE); + if (!isWebViewLoaded || hasPresentedLines) { + return; } - 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) { switch (mode) { case NORMAL: editBanner.setVisibility(View.GONE); - adapter.setEditing(false); + // TODO [lisa][debug-log-editing] +// setEditing(false); saveMenuItem.setVisible(true); // TODO [greyson][log] Not yet implemented // editMenuItem.setVisible(true); @@ -240,7 +230,7 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL break; case SUBMITTING: editBanner.setVisibility(View.GONE); - adapter.setEditing(false); +// setEditing(false); editMenuItem.setVisible(false); doneMenuItem.setVisible(false); searchMenuItem.setVisible(false); @@ -248,7 +238,7 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL break; case EDIT: editBanner.setVisibility(View.VISIBLE); - adapter.setEditing(true); +// setEditing(true); editMenuItem.setVisible(false); doneMenuItem.setVisible(true); searchMenuItem.setVisible(true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java deleted file mode 100644 index 880a154e43..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java +++ /dev/null @@ -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 { - - 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 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 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 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); - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java index 842c05c1bf..39f4d10806 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java @@ -11,13 +11,9 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; 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.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; @@ -32,42 +28,43 @@ public class SubmitDebugLogViewModel extends ViewModel { private final SubmitDebugLogRepository repo; private final MutableLiveData mode; - private final ProxyPagingController pagingController; private final List staticLines; private final MediatorLiveData> lines; private final SingleLiveEvent event; private final long firstViewTime; private final byte[] trace; - + private final List allLines; private SubmitDebugLogViewModel() { 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<>(); this.event = new SingleLiveEvent<>(); + this.allLines = new ArrayList<>(); repo.getPrefixLogLines(staticLines -> { this.staticLines.addAll(staticLines); Log.blockUntilAllWritesFinished(); LogDatabase.getInstance(AppDependencies.getApplication()).logs().trimToSize(); + SignalExecutors.UNBOUNDED.execute(() -> { + allLines.clear(); + allLines.addAll(staticLines); - LogDataSource dataSource = new LogDataSource(AppDependencies.getApplication(), staticLines, firstViewTime); - PagingConfig config = new PagingConfig.Builder().setPageSize(100) - .setBufferPages(3) - .setStartIndex(0) - .build(); + try (LogDatabase.LogTable.CursorReader logReader = (LogDatabase.LogTable.CursorReader) LogDatabase.getInstance(AppDependencies.getApplication()).logs().getAllBeforeTime(firstViewTime)) { + while (logReader.hasNext()) { + String next = logReader.next(); + allLines.add(new SimpleLogLine(next, LogStyleParser.parseStyle(next), LogLine.Placeholder.NONE)); + } + } - LivePagedData pagedData = PagedData.createForLiveData(dataSource, config); - - ThreadUtil.runOnMain(() -> { - pagingController.set(pagedData.getController()); - lines.addSource(pagedData.getData(), lines::setValue); - mode.setValue(Mode.NORMAL); + ThreadUtil.runOnMain(() -> { + lines.setValue(allLines); + mode.setValue(Mode.NORMAL); + }); }); }); } @@ -76,10 +73,6 @@ public class SubmitDebugLogViewModel extends ViewModel { return lines; } - @NonNull PagingController getPagingController() { - return pagingController; - } - @NonNull LiveData getMode() { return mode; } @@ -161,4 +154,4 @@ public class SubmitDebugLogViewModel extends ViewModel { return modelClass.cast(new SubmitDebugLogViewModel()); } } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt index 935d533c1b..cb8d597982 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/megaphone/RemoteMegaphoneRepository.kt @@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord.ActionId import org.thoughtcrime.securesms.dependencies.AppDependencies import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.megaphone.RemoteMegaphoneRepository.Action import org.thoughtcrime.securesms.providers.BlobProvider import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.LocaleRemoteConfig diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ContextUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ContextUtil.java index 63cccc66da..47e2f3eb35 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ContextUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ContextUtil.java @@ -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) { return Settings.Global.getFloat(context.getContentResolver(), Settings.Global.ANIMATOR_DURATION_SCALE, 1.0f); diff --git a/app/src/main/res/layout/submit_debug_log_activity.xml b/app/src/main/res/layout/submit_debug_log_activity.xml index a696a32b48..ca12467610 100644 --- a/app/src/main/res/layout/submit_debug_log_activity.xml +++ b/app/src/main/res/layout/submit_debug_log_activity.xml @@ -44,7 +44,7 @@ app:barrierDirection="bottom" app:constraint_referenced_ids="debug_log_warning_banner,debug_log_edit_banner" /> - + + + + + + + + + + + + + + + diff --git a/debuglogs-viewer/app/src/main/assets/WebView.html b/debuglogs-viewer/app/src/main/assets/WebView.html new file mode 100644 index 0000000000..24c945bebf --- /dev/null +++ b/debuglogs-viewer/app/src/main/assets/WebView.html @@ -0,0 +1,59 @@ + + + + + + + + + + +
+
+ + + + + + +
+
+
Loading...
+ + + + \ No newline at end of file diff --git a/debuglogs-viewer/app/src/main/assets/WebView.js b/debuglogs-viewer/app/src/main/assets/WebView.js new file mode 100644 index 0000000000..01fe740e33 --- /dev/null +++ b/debuglogs-viewer/app/src/main/assets/WebView.js @@ -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); +}); \ No newline at end of file diff --git a/debuglogs-viewer/app/src/main/assets/log.txt b/debuglogs-viewer/app/src/main/assets/log.txt new file mode 100644 index 0000000000..e69de29bb2 diff --git a/debuglogs-viewer/app/src/main/java/org/signal/debuglogsviewer/app/MainActivity.kt b/debuglogs-viewer/app/src/main/java/org/signal/debuglogsviewer/app/MainActivity.kt new file mode 100644 index 0000000000..8cf83050b5 --- /dev/null +++ b/debuglogs-viewer/app/src/main/java/org/signal/debuglogsviewer/app/MainActivity.kt @@ -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) + } +} \ No newline at end of file diff --git a/debuglogs-viewer/app/src/main/java/org/signal/debuglogsviewer/app/webview/WebView.kt b/debuglogs-viewer/app/src/main/java/org/signal/debuglogsviewer/app/webview/WebView.kt new file mode 100644 index 0000000000..5fddb55b46 --- /dev/null +++ b/debuglogs-viewer/app/src/main/java/org/signal/debuglogsviewer/app/webview/WebView.kt @@ -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}" + } +} \ No newline at end of file diff --git a/debuglogs-viewer/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/debuglogs-viewer/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000000..18c0565852 --- /dev/null +++ b/debuglogs-viewer/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/debuglogs-viewer/app/src/main/res/drawable/ic_launcher_background.xml b/debuglogs-viewer/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000000..c71b77f4d2 --- /dev/null +++ b/debuglogs-viewer/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/debuglogs-viewer/app/src/main/res/drawable/ic_refresh_20.xml b/debuglogs-viewer/app/src/main/res/drawable/ic_refresh_20.xml new file mode 100644 index 0000000000..915e0bbbca --- /dev/null +++ b/debuglogs-viewer/app/src/main/res/drawable/ic_refresh_20.xml @@ -0,0 +1,9 @@ + + + diff --git a/debuglogs-viewer/app/src/main/res/layout/activity_main.xml b/debuglogs-viewer/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000000..b266e69d6e --- /dev/null +++ b/debuglogs-viewer/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,64 @@ + + + + + + + +