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 7736d87166..f41aa1c6f6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java @@ -11,8 +11,6 @@ import android.text.util.Linkify; import android.view.Menu; import android.view.MenuItem; import android.view.View; -import android.webkit.WebView; -import android.webkit.WebViewClient; import android.widget.TextView; import android.widget.Toast; @@ -21,13 +19,13 @@ import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.widget.SearchView; import androidx.core.app.ShareCompat; -import androidx.core.content.ContextCompat; import androidx.core.text.util.LinkifyCompat; import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; import com.google.android.material.dialog.MaterialAlertDialogBuilder; -import org.json.JSONObject; import org.thoughtcrime.securesms.BaseActivity; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.ProgressCard; @@ -40,13 +38,13 @@ import org.thoughtcrime.securesms.util.views.CircularProgressMaterialButton; import java.util.List; -public class SubmitDebugLogActivity extends BaseActivity { +public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugLogAdapter.Listener { private static final int CODE_SAVE = 24601; - private WebView logWebView; + private RecyclerView lineList; + private SubmitDebugLogAdapter adapter; private SubmitDebugLogViewModel viewModel; - private boolean isPageLoaded; private View warningBanner; private View editBanner; @@ -95,12 +93,13 @@ public class SubmitDebugLogActivity extends BaseActivity { SearchView.OnQueryTextListener queryListener = new SearchView.OnQueryTextListener() { @Override public boolean onQueryTextSubmit(String query) { + viewModel.onQueryUpdated(query); return true; } @Override public boolean onQueryTextChange(String query) { - onQueryChanged(query); + viewModel.onQueryUpdated(query); return true; } }; @@ -115,7 +114,7 @@ public class SubmitDebugLogActivity extends BaseActivity { @Override public boolean onMenuItemActionCollapse(MenuItem item) { searchView.setOnQueryTextListener(null); - onQueryChanged(""); + viewModel.onSearchClosed(); return true; } }); @@ -166,32 +165,13 @@ public class SubmitDebugLogActivity extends BaseActivity { } } - // TODO [lisa][debug-log-delete] -// public void onLogDeleted(@NonNull LogLine logLine) { -// viewModel.onLogDeleted(logLine); -// } - - private void initWebView() { - logWebView.getSettings().setBuiltInZoomControls(true); - logWebView.getSettings().setDisplayZoomControls(false); - logWebView.getSettings().setUseWideViewPort(true); - logWebView.getSettings().setJavaScriptEnabled(true); - logWebView.setHorizontalScrollBarEnabled(true); - - logWebView.setWebViewClient(new WebViewClient() { - @Override - public void onPageFinished(WebView view, String url) { - isPageLoaded = true; - } - }); - } - - private String intToCssHex(int color) { - return String.format("#%06X", 0xFFFFFF & color); + @Override + public void onLogDeleted(@NonNull LogLine logLine) { + viewModel.onLogDeleted(logLine); } private void initView() { - this.logWebView = findViewById(R.id.debug_log_lines); + this.lineList = findViewById(R.id.debug_log_lines); this.warningBanner = findViewById(R.id.debug_log_warning_banner); this.editBanner = findViewById(R.id.debug_log_edit_banner); this.submitButton = findViewById(R.id.debug_log_submit_button); @@ -199,27 +179,35 @@ public class SubmitDebugLogActivity extends BaseActivity { this.scrollToTopButton = findViewById(R.id.debug_log_scroll_to_top); this.progressCard = findViewById(R.id.debug_log_progress_card); - initWebView(); + this.adapter = new SubmitDebugLogAdapter(this, viewModel.getPagingController()); + + this.lineList.setLayoutManager(new LinearLayoutManager(this)); + this.lineList.setAdapter(adapter); + this.lineList.setItemAnimator(null); submitButton.setOnClickListener(v -> onSubmitClicked()); - scrollToBottomButton.setOnClickListener(v -> logWebView.pageDown(true)); - scrollToTopButton.setOnClickListener(v -> logWebView.pageUp(true)); + scrollToBottomButton.setOnClickListener(v -> lineList.scrollToPosition(adapter.getItemCount() - 1)); + scrollToTopButton.setOnClickListener(v -> lineList.scrollToPosition(0)); - logWebView.getViewTreeObserver().addOnScrollChangedListener(() -> { - if (logWebView.getScrollY() + logWebView.getHeight() < logWebView.getContentHeight() * logWebView.getScale() - 10) { - scrollToBottomButton.setVisibility(View.VISIBLE); - } else { - scrollToBottomButton.setVisibility(View.GONE); - } + lineList.addOnScrollListener(new RecyclerView.OnScrollListener() { + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + if (((LinearLayoutManager) recyclerView.getLayoutManager()).findLastVisibleItemPosition() < adapter.getItemCount() - 10) { + scrollToBottomButton.setVisibility(View.VISIBLE); + } else { + scrollToBottomButton.setVisibility(View.GONE); + } - if (logWebView.getScrollY() > 10) { - scrollToTopButton.setVisibility(View.VISIBLE); - } else { - scrollToTopButton.setVisibility(View.GONE); + if (((LinearLayoutManager) recyclerView.getLayoutManager()).findFirstVisibleItemPosition() > 10) { + scrollToTopButton.setVisibility(View.VISIBLE); + } else { + scrollToTopButton.setVisibility(View.GONE); + } } }); this.progressCard.setVisibility(View.VISIBLE); + } private void initViewModel() { @@ -229,10 +217,6 @@ public class SubmitDebugLogActivity extends BaseActivity { } private void presentLines(@NonNull List lines) { - if (!isPageLoaded) { - initWebView(); - } - if (progressCard != null && lines.size() > 0) { progressCard.setVisibility(View.GONE); @@ -240,108 +224,23 @@ public class SubmitDebugLogActivity extends BaseActivity { submitButton.setVisibility(View.VISIBLE); } - StringBuilder body = new StringBuilder(); - - int backgroundColor = ContextCompat.getColor(this, R.color.signal_colorBackground); - int noneColor = ContextCompat.getColor(this, R.color.debuglog_color_none); - int verboseColor = ContextCompat.getColor(this, R.color.debuglog_color_verbose); - int debugColor = ContextCompat.getColor(this, R.color.debuglog_color_debug); - int infoColor = ContextCompat.getColor(this, R.color.debuglog_color_info); - int warningColor = ContextCompat.getColor(this, R.color.debuglog_color_warn); - int errorColor = ContextCompat.getColor(this, R.color.debuglog_color_error); - - String css = String.format(""" - - """, - intToCssHex(backgroundColor), - intToCssHex(noneColor), - intToCssHex(verboseColor), - intToCssHex(debugColor), - intToCssHex(infoColor), - intToCssHex(warningColor), - intToCssHex(errorColor) - ); - - String js = """ - - """; - - body.append(String.format("%s%s
", css, js)); - - for (LogLine line : lines) { - if (line == null) continue; - - String newLine = line.getText(); - String lineClass = switch (line.getStyle()) { - case VERBOSE -> "verbose"; - case DEBUG -> "debug"; - case INFO -> "info"; - case WARNING -> "warning"; - case ERROR -> "error"; - default -> "none"; - }; - - body.append(String.format("
%s
", lineClass, newLine)); - } - - body.append("
"); - - String htmlContent = body.toString(); - - logWebView.loadDataWithBaseURL(null, htmlContent, "text/html", "UTF-8", null); - } - - private void onQueryChanged(String query) { - String script = String.format("filterLogLines(%s);\n", JSONObject.quote(query)); - - logWebView.evaluateJavascript(script, null); + adapter.submitList(lines); } private void presentMode(@NonNull SubmitDebugLogViewModel.Mode mode) { switch (mode) { case NORMAL: editBanner.setVisibility(View.GONE); - // TODO [lisa][debug-log-editing] -// setEditing(false); + adapter.setEditing(false); saveMenuItem.setVisible(true); // TODO [greyson][log] Not yet implemented // editMenuItem.setVisible(true); // doneMenuItem.setVisible(false); - searchMenuItem.setVisible(true); +// searchMenuItem.setVisible(true); break; case SUBMITTING: editBanner.setVisibility(View.GONE); -// setEditing(false); + adapter.setEditing(false); editMenuItem.setVisible(false); doneMenuItem.setVisible(false); searchMenuItem.setVisible(false); @@ -349,7 +248,7 @@ public class SubmitDebugLogActivity extends BaseActivity { break; case EDIT: editBanner.setVisibility(View.VISIBLE); -// setEditing(true); + adapter.setEditing(true); editMenuItem.setVisible(false); doneMenuItem.setVisible(true); searchMenuItem.setVisible(true); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java new file mode 100644 index 0000000000..880a154e43 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java @@ -0,0 +1,188 @@ +package org.thoughtcrime.securesms.logsubmit; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import org.signal.paging.PagingController; +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.ListenableHorizontalScrollView; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +public class SubmitDebugLogAdapter extends RecyclerView.Adapter { + + 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 ba36fdcf24..842c05c1bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java @@ -13,6 +13,11 @@ import androidx.lifecycle.ViewModelProvider; import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; import org.signal.core.util.tracing.Tracer; +import org.signal.paging.LivePagedData; +import org.signal.paging.PagedData; +import org.signal.paging.PagingConfig; +import org.signal.paging.PagingController; +import org.signal.paging.ProxyPagingController; import org.thoughtcrime.securesms.database.LogDatabase; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.util.SingleLiveEvent; @@ -27,6 +32,7 @@ public class SubmitDebugLogViewModel extends ViewModel { private final SubmitDebugLogRepository repo; private final MutableLiveData mode; + private final ProxyPagingController pagingController; private final List staticLines; private final MediatorLiveData> lines; private final SingleLiveEvent event; @@ -38,6 +44,7 @@ public class SubmitDebugLogViewModel extends ViewModel { this.repo = new SubmitDebugLogRepository(); this.mode = new MutableLiveData<>(); this.trace = Tracer.getInstance().serialize(); + this.pagingController = new ProxyPagingController<>(); this.firstViewTime = System.currentTimeMillis(); this.staticLines = new ArrayList<>(); this.lines = new MediatorLiveData<>(); @@ -49,12 +56,17 @@ public class SubmitDebugLogViewModel extends ViewModel { Log.blockUntilAllWritesFinished(); LogDatabase.getInstance(AppDependencies.getApplication()).logs().trimToSize(); - LogDataSource dataSource = new LogDataSource(AppDependencies.getApplication(), staticLines, firstViewTime); - int size = dataSource.size(); - List allLogLines = new ArrayList<>(dataSource.load(0, size, size, () -> false)); + LogDataSource dataSource = new LogDataSource(AppDependencies.getApplication(), staticLines, firstViewTime); + PagingConfig config = new PagingConfig.Builder().setPageSize(100) + .setBufferPages(3) + .setStartIndex(0) + .build(); + + LivePagedData pagedData = PagedData.createForLiveData(dataSource, config); ThreadUtil.runOnMain(() -> { - lines.setValue(allLogLines); + pagingController.set(pagedData.getController()); + lines.addSource(pagedData.getData(), lines::setValue); mode.setValue(Mode.NORMAL); }); }); @@ -64,6 +76,10 @@ public class SubmitDebugLogViewModel extends ViewModel { return lines; } + @NonNull PagingController getPagingController() { + return pagingController; + } + @NonNull LiveData getMode() { return mode; } 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 ca12467610..a696a32b48 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" /> -