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 e5f9ef6898..8b46a85b37 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java @@ -376,18 +376,28 @@ public class SubmitDebugLogActivity extends BaseActivity { switch (mode) { case LOADING: + searchNav.setVisibility(View.GONE); + saveMenuItem.setVisible(false); + searchMenuItem.setVisible(false); progressCard.setVisibility(View.VISIBLE); + submitButton.setEnabled(false); + logWebView.setAlpha(0.25f); break; case NORMAL: searchNav.setVisibility(View.GONE); saveMenuItem.setVisible(true); searchMenuItem.setVisible(true); progressCard.setVisibility(View.GONE); + submitButton.setEnabled(true); + logWebView.setAlpha(1f); break; case SUBMITTING: - searchMenuItem.setVisible(false); + searchNav.setVisibility(View.GONE); saveMenuItem.setVisible(false); - progressCard.setVisibility(View.VISIBLE); + searchMenuItem.setVisible(false); + progressCard.setVisibility(View.GONE); + submitButton.setSpinning(); + logWebView.setAlpha(1f); break; } } @@ -447,7 +457,7 @@ public class SubmitDebugLogActivity extends BaseActivity { private void onSubmitClicked() { submitButton.setSpinning(); - viewModel.onSubmitClicked().observe(this, result -> { + viewModel.onSubmitClicked(DebugLogsViewer.readLogs(logWebView)).observe(this, result -> { if (result.isPresent()) { presentResultDialog(result.get()); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java index 7d33da1be9..6cad44cfe8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java @@ -19,6 +19,7 @@ import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.signal.core.util.logging.Scrubber; import org.signal.core.util.tracing.Tracer; +import org.signal.debuglogsviewer.DebugLogsViewer; import org.thoughtcrime.securesms.database.LogDatabase; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor; @@ -125,15 +126,8 @@ public class SubmitDebugLogRepository { }); } - /** - * Submits a log with the provided prefix lines. - * - * @param untilTime Only submit logs from {@link LogDatabase} if they were created before this time. This is our way of making sure that the logs we submit - * only include the logs that we've already shown the user. It's possible some old logs may have been trimmed off in the meantime, but no - * new ones could pop up. - */ - public void submitLogWithPrefixLines(long untilTime, @NonNull List prefixLines, @Nullable byte[] trace, Callback> callback) { - SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(untilTime, prefixLines, trace))); + public void submitLogFromReader(DebugLogsViewer.LogReader logReader, @Nullable byte[] trace, Callback> callback) { + SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogFromReaderInternal(logReader, trace))); } public void writeLogToDisk(@NonNull Uri uri, long untilTime, Callback callback) { @@ -168,6 +162,79 @@ public class SubmitDebugLogRepository { }); } + @WorkerThread + private @NonNull Optional submitLogFromReaderInternal(DebugLogsViewer.LogReader logReader, @Nullable byte[] trace) { + Stopwatch stopwatch = new Stopwatch("log-upload"); + String traceUrl = null; + if (trace != null) { + try { + traceUrl = uploadContent("application/octet-stream", RequestBody.create(MediaType.get("application/octet-stream"), trace)); + } catch (IOException e) { + Log.w(TAG, "Error during trace upload.", e); + return Optional.empty(); + } + } + stopwatch.split("trace"); + + try { + + ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe(); + Future futureUri = BlobProvider.getInstance() + .forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0) + .withMimeType("application/gzip") + .createForSingleSessionOnDiskAsync(context); + + OutputStream gzipOutput = new GZIPOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1])); + + boolean traceFound = trace == null; + String next; + while ((next = logReader.nextChunk(10_000)) != null) { + if (!traceFound) { + int traceIndex = next.indexOf(""); + if (traceIndex != -1) { + next = next.replace("", traceUrl); + traceFound = true; + } + } + + gzipOutput.write(next.getBytes()); + gzipOutput.write("\n".getBytes()); + } + + StreamUtil.close(gzipOutput); + Uri gzipUri = futureUri.get(); + + stopwatch.split("body"); + + String logUrl = uploadContent("application/gzip", new RequestBody() { + @Override + public @NonNull MediaType contentType() { + return MediaType.get("application/gzip"); + } + + @Override public long contentLength() { + return BlobProvider.getInstance().calculateFileSize(context, gzipUri); + } + + @Override + public void writeTo(@NonNull BufferedSink sink) throws IOException { + Source source = Okio.source(BlobProvider.getInstance().getStream(context, gzipUri)); + sink.writeAll(source); + } + }); + + stopwatch.split("upload"); + stopwatch.stop(TAG); + + BlobProvider.getInstance().delete(context, gzipUri); + + return Optional.of(logUrl); + } catch (IOException | RuntimeException | ExecutionException | InterruptedException e) { + Log.w(TAG, "Error during log upload.", e); + return Optional.empty(); + } + } + @WorkerThread private @NonNull Optional submitLogInternal(long untilTime, @NonNull List prefixLines, @Nullable byte[] trace) { String traceUrl = null; 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 a5d719a7df..b3a7a370fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java @@ -10,10 +10,9 @@ import androidx.lifecycle.ViewModel; import androidx.lifecycle.ViewModelProvider; import org.signal.core.util.Stopwatch; -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.debuglogsviewer.DebugLogsViewer; import org.thoughtcrime.securesms.database.LogDatabase; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.util.SingleLiveEvent; @@ -120,17 +119,14 @@ public class SubmitDebugLogViewModel extends ViewModel { return mode; } - @NonNull LiveData> onSubmitClicked() { + @NonNull LiveData> onSubmitClicked(DebugLogsViewer.LogReader logReader) { mode.postValue(Mode.SUBMITTING); MutableLiveData> result = new MutableLiveData<>(); - // Get prefix lines for submission - this is a quick operation so it's ok to do it here - repo.getPrefixLogLines(prefixLines -> { - repo.submitLogWithPrefixLines(firstViewTime, prefixLines, trace, value -> { - mode.postValue(Mode.NORMAL); - result.postValue(value); - }); + repo.submitLogFromReader(logReader, trace, value -> { + mode.postValue(Mode.NORMAL); + result.postValue(value); }); return result; diff --git a/debuglogs-viewer/lib/build.gradle.kts b/debuglogs-viewer/lib/build.gradle.kts index a5a04a515d..b7a4c1177d 100644 --- a/debuglogs-viewer/lib/build.gradle.kts +++ b/debuglogs-viewer/lib/build.gradle.kts @@ -13,6 +13,7 @@ android { dependencies { implementation(project(":core-util")) + implementation(project(":core-util-jvm")) implementation(libs.kotlin.reflect) implementation(libs.jackson.module.kotlin) diff --git a/debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.js b/debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.js index d5b89c6cb5..bfd47ebf01 100644 --- a/debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.js +++ b/debuglogs-viewer/lib/src/main/assets/debuglogs-viewer.js @@ -220,4 +220,13 @@ function applyFilter() { editor.setValue(filtered, -1); } +function readLines(offset, limit) { + const lines = logLines.split("\n") + if (offset >= lines.length) { + return "<>"; + } + + return lines.slice(offset, offset + limit).join("\n") +} + main(); \ No newline at end of file diff --git a/debuglogs-viewer/lib/src/main/java/org/signal/debuglogsviewer/DebugLogsViewer.kt b/debuglogs-viewer/lib/src/main/java/org/signal/debuglogsviewer/DebugLogsViewer.kt index 2ab95c04c9..29f842c744 100644 --- a/debuglogs-viewer/lib/src/main/java/org/signal/debuglogsviewer/DebugLogsViewer.kt +++ b/debuglogs-viewer/lib/src/main/java/org/signal/debuglogsviewer/DebugLogsViewer.kt @@ -11,8 +11,10 @@ import android.webkit.ValueCallback import android.webkit.WebView import android.webkit.WebViewClient import kotlinx.coroutines.Runnable +import org.json.JSONArray import org.json.JSONObject import org.signal.core.util.ThreadUtil +import java.util.concurrent.CountDownLatch import java.util.function.Consumer object DebugLogsViewer { @@ -47,6 +49,27 @@ object DebugLogsViewer { ThreadUtil.runOnMain { webview.evaluateJavascript("editor.insert($escaped); logLines+=$escaped;", null) } } + @JvmStatic + fun readLogs(webview: WebView): LogReader { + var position = 0 + return LogReader { size -> + val latch = CountDownLatch(1) + var result: String? = null + ThreadUtil.runOnMain { + webview.evaluateJavascript("readLines($position, $size)") { value -> + // Annoying, string returns from javascript land are always encoded as JSON strings (wrapped in quotes, various strings escaped, etc) + val parsed = JSONArray("[$value]").getString(0) + result = parsed.takeUnless { it == "<>" } + position += size + latch.countDown() + } + } + + latch.await() + result + } + } + @JvmStatic fun scrollToTop(webview: WebView) { webview.evaluateJavascript("editor.scrollToRow(0);", null) @@ -107,4 +130,9 @@ object DebugLogsViewer { fun onSearchClose(webview: WebView) { webview.evaluateJavascript("onSearchClose();", null) } + + fun interface LogReader { + /** Returns the next bit of log, containing at most [size] lines (but may be less), or null if there are no logs remaining. */ + fun nextChunk(size: Int): String? + } }