From 7b3035104df8df308b784205f39d203c4357bfce Mon Sep 17 00:00:00 2001 From: Ian Macdonald Date: Sat, 18 Sep 2021 22:17:26 +0200 Subject: [PATCH 01/10] Display the service node's IP address after its country. --- .../main/java/org/thoughtcrime/securesms/util/IP2Country.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt b/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt index 5f8678a58c..4ff45e8f01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/IP2Country.kt @@ -99,7 +99,7 @@ class IP2Country private constructor(private val context: Context) { val bestMatchCountry = comps.lastOrNull { it.key <= Ipv4Int(ip) }?.let { (_, code) -> if (code != null) { - countryToNames[code] + countryToNames[code] + " [" + ip + "]" } else { null } @@ -126,4 +126,4 @@ class IP2Country private constructor(private val context: Context) { } } // endregion -} \ No newline at end of file +} From 17b58b09e3c336193338f73380391d7b3198321d Mon Sep 17 00:00:00 2001 From: Harris Date: Thu, 23 Sep 2021 10:27:57 +1000 Subject: [PATCH 02/10] feat: add persistent logger and integrate it to the loggers initialization --- app/build.gradle | 2 +- .../securesms/ApplicationContext.java | 23 +- .../securesms/logging/PersistentLogger.java | 245 ++++++++++++++++++ 3 files changed, 258 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.java diff --git a/app/build.gradle b/app/build.gradle index 89327b29d5..f81c02db2d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -153,7 +153,7 @@ dependencies { testImplementation 'org.robolectric:shadows-multidex:4.4' } -def canonicalVersionCode = 222 +def canonicalVersionCode = 223 def canonicalVersionName = "1.11.10" def postFixSize = 10 diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 363343696c..1643f63bbb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -15,6 +15,9 @@ */ package org.thoughtcrime.securesms; +import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant; +import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant; + import android.app.Application; import android.content.Context; import android.content.Intent; @@ -48,24 +51,23 @@ import org.signal.aesgcmprovider.AesGcmProvider; import org.thoughtcrime.securesms.components.TypingStatusSender; import org.thoughtcrime.securesms.crypto.KeyPairUtilities; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.LokiAPIDatabase; import org.thoughtcrime.securesms.dependencies.InjectableType; import org.thoughtcrime.securesms.dependencies.SignalCommunicationModule; +import org.thoughtcrime.securesms.groups.OpenGroupManager; +import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.jobmanager.DependencyInjector; import org.thoughtcrime.securesms.jobmanager.JobManager; import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer; import org.thoughtcrime.securesms.jobs.FastJobStorage; import org.thoughtcrime.securesms.jobs.JobManagerFactories; import org.thoughtcrime.securesms.logging.AndroidLogger; +import org.thoughtcrime.securesms.logging.PersistentLogger; import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger; -import org.thoughtcrime.securesms.home.HomeActivity; import org.thoughtcrime.securesms.notifications.BackgroundPollWorker; -import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager; -import org.thoughtcrime.securesms.groups.OpenGroupManager; -import org.thoughtcrime.securesms.database.LokiAPIDatabase; -import org.thoughtcrime.securesms.util.Broadcaster; -import org.thoughtcrime.securesms.notifications.FcmUtils; -import org.thoughtcrime.securesms.util.UiModeUtilities; import org.thoughtcrime.securesms.notifications.DefaultMessageNotifier; +import org.thoughtcrime.securesms.notifications.FcmUtils; +import org.thoughtcrime.securesms.notifications.LokiPushNotificationManager; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.notifications.OptimizedMessageNotifier; import org.thoughtcrime.securesms.providers.BlobProvider; @@ -75,6 +77,8 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener; import org.thoughtcrime.securesms.sskenvironment.ProfileManager; import org.thoughtcrime.securesms.sskenvironment.ReadReceiptManager; import org.thoughtcrime.securesms.sskenvironment.TypingStatusRepository; +import org.thoughtcrime.securesms.util.Broadcaster; +import org.thoughtcrime.securesms.util.UiModeUtilities; import org.thoughtcrime.securesms.util.dynamiclanguage.LocaleParseHelper; import org.webrtc.PeerConnectionFactory; import org.webrtc.PeerConnectionFactory.InitializationOptions; @@ -93,9 +97,6 @@ import kotlin.Unit; import kotlinx.coroutines.Job; import network.loki.messenger.BuildConfig; -import static nl.komponents.kovenant.android.KovenantAndroid.startKovenant; -import static nl.komponents.kovenant.android.KovenantAndroid.stopKovenant; - /** * Will be called once when the TextSecure process is created. *

@@ -276,7 +277,7 @@ public class ApplicationContext extends Application implements DependencyInjecto } private void initializeLogging() { - Log.initialize(new AndroidLogger()); + Log.initialize(new AndroidLogger(), new PersistentLogger(this)); } private void initializeCrashHandling() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.java b/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.java new file mode 100644 index 0000000000..ab0b42e210 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.java @@ -0,0 +1,245 @@ +package org.thoughtcrime.securesms.logging; + +import android.content.Context; + +import androidx.annotation.AnyThread; +import androidx.annotation.WorkerThread; + +import org.session.libsignal.utilities.ListenableFuture; +import org.session.libsignal.utilities.Log; +import org.session.libsignal.utilities.NoExternalStorageException; +import org.session.libsignal.utilities.SettableFuture; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.PrintStream; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +public class PersistentLogger extends Log.Logger { + + private static final String TAG = PersistentLogger.class.getSimpleName(); + + private static final String LOG_V = "V"; + private static final String LOG_D = "D"; + private static final String LOG_I = "I"; + private static final String LOG_W = "W"; + private static final String LOG_E = "E"; + private static final String LOG_WTF = "A"; + + private static final String LOG_DIRECTORY = "log"; + private static final String FILENAME_PREFIX = "log-"; + private static final int MAX_LOG_FILES = 5; + private static final int MAX_LOG_SIZE = 300 * 1024; + private static final int MAX_LOG_EXPORT = 10_000; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS zzz"); + + private final Context context; + private final Executor executor; + private final byte[] secret; + + private LogFile.Writer writer; + + public PersistentLogger(Context context) { + this.context = context.getApplicationContext(); + this.secret = LogSecretProvider.getOrCreateAttachmentSecret(context); + this.executor = Executors.newSingleThreadExecutor(r -> { + Thread thread = new Thread(r, "PersistentLogger"); + thread.setPriority(Thread.MIN_PRIORITY); + return thread; + }); + + executor.execute(this::initializeWriter); + } + + @Override + public void v(String tag, String message, Throwable t) { + write(LOG_V, tag, message, t); + } + + @Override + public void d(String tag, String message, Throwable t) { + write(LOG_D, tag, message, t); + } + + @Override + public void i(String tag, String message, Throwable t) { + write(LOG_I, tag, message, t); + } + + @Override + public void w(String tag, String message, Throwable t) { + write(LOG_W, tag, message, t); + } + + @Override + public void e(String tag, String message, Throwable t) { + write(LOG_E, tag, message, t); + } + + @Override + public void wtf(String tag, String message, Throwable t) { + write(LOG_WTF, tag, message, t); + } + + @Override + public void blockUntilAllWritesFinished() { + CountDownLatch latch = new CountDownLatch(1); + + executor.execute(latch::countDown); + + try { + latch.await(); + } catch (InterruptedException e) { + android.util.Log.w(TAG, "Failed to wait for all writes."); + } + } + + @WorkerThread + public ListenableFuture getLogs() { + final SettableFuture future = new SettableFuture<>(); + + executor.execute(() -> { + StringBuilder builder = new StringBuilder(); + + try { + File[] logs = getSortedLogFiles(); + for (int i = logs.length - 1; i >= 0; i--) { + try { + LogFile.Reader reader = new LogFile.Reader(secret, logs[i]); + builder.append(reader.readAll()); + } catch (IOException e) { + android.util.Log.w(TAG, "Failed to read log at index " + i + ". Removing reference."); + logs[i].delete(); + } + } + + future.set(builder.toString()); + } catch (NoExternalStorageException e) { + future.setException(e); + } + }); + + return future; + } + + @WorkerThread + private void initializeWriter() { + try { + writer = new LogFile.Writer(secret, getOrCreateActiveLogFile()); + } catch (NoExternalStorageException | IOException e) { + android.util.Log.e(TAG, "Failed to initialize writer.", e); + } + } + + @AnyThread + private void write(String level, String tag, String message, Throwable t) { + executor.execute(() -> { + try { + if (writer == null) { + return; + } + + if (writer.getLogSize() >= MAX_LOG_SIZE) { + writer.close(); + writer = new LogFile.Writer(secret, createNewLogFile()); + trimLogFilesOverMax(); + } + + for (String entry : buildLogEntries(level, tag, message, t)) { + writer.writeEntry(entry); + } + + } catch (NoExternalStorageException e) { + android.util.Log.w(TAG, "Cannot persist logs.", e); + } catch (IOException e) { + android.util.Log.w(TAG, "Failed to write line. Deleting all logs and starting over."); + deleteAllLogs(); + initializeWriter(); + } + }); + } + + private void trimLogFilesOverMax() throws NoExternalStorageException { + File[] logs = getSortedLogFiles(); + if (logs.length > MAX_LOG_FILES) { + for (int i = MAX_LOG_FILES; i < logs.length; i++) { + logs[i].delete(); + } + } + } + + private void deleteAllLogs() { + try { + File[] logs = getSortedLogFiles(); + for (File log : logs) { + log.delete(); + } + } catch (NoExternalStorageException e) { + android.util.Log.w(TAG, "Was unable to delete logs.", e); + } + } + + private File getOrCreateActiveLogFile() throws NoExternalStorageException { + File[] logs = getSortedLogFiles(); + if (logs.length > 0) { + return logs[0]; + } + + return createNewLogFile(); + } + + private File createNewLogFile() throws NoExternalStorageException { + return new File(getOrCreateLogDirectory(), FILENAME_PREFIX + System.currentTimeMillis()); + } + + private File[] getSortedLogFiles() throws NoExternalStorageException { + File[] logs = getOrCreateLogDirectory().listFiles(); + if (logs != null) { + Arrays.sort(logs, (o1, o2) -> o2.getName().compareTo(o1.getName())); + return logs; + } + return new File[0]; + } + + private File getOrCreateLogDirectory() throws NoExternalStorageException { + File logDir = new File(context.getCacheDir(), LOG_DIRECTORY); + if (!logDir.exists() && !logDir.mkdir()) { + throw new NoExternalStorageException("Unable to create log directory."); + } + + return logDir; + } + + private List buildLogEntries(String level, String tag, String message, Throwable t) { + List entries = new LinkedList<>(); + Date date = new Date(); + + entries.add(buildEntry(level, tag, message, date)); + + if (t != null) { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + t.printStackTrace(new PrintStream(outputStream)); + + String trace = new String(outputStream.toByteArray()); + String[] lines = trace.split("\\n"); + + for (String line : lines) { + entries.add(buildEntry(level, tag, line, date)); + } + } + + return entries; + } + + private String buildEntry(String level, String tag, String message, Date date) { + return DATE_FORMAT.format(date) + ' ' + level + ' ' + tag + ": " + message; + } +} From a295cc384cfe7b5d2c50ea77d68ef0967a22efc4 Mon Sep 17 00:00:00 2001 From: Harris Date: Thu, 23 Sep 2021 13:49:32 +1000 Subject: [PATCH 03/10] feat: add share logs dialogs into settings activity --- .../securesms/logging/LogFile.java | 2 +- .../securesms/logging/PersistentLogger.java | 9 ++- .../securesms/preferences/SettingsActivity.kt | 5 ++ .../securesms/preferences/ShareLogsDialog.kt | 38 +++++++++++ app/src/main/res/layout/activity_settings.xml | 12 ++++ app/src/main/res/layout/dialog_share_logs.xml | 64 +++++++++++++++++++ app/src/main/res/values/strings.xml | 3 + 7 files changed, 130 insertions(+), 3 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt create mode 100644 app/src/main/res/layout/dialog_share_logs.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java b/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java index 64702cc988..f0c083ca1d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/LogFile.java @@ -123,7 +123,7 @@ class LogFile { return builder.toString(); } - private String readEntry() throws IOException { + String readEntry() throws IOException { try { Util.readFully(inputStream, ivBuffer); Util.readFully(inputStream, intBuffer); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.java b/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.java index ab0b42e210..9fd5968f6a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.java @@ -108,13 +108,18 @@ public class PersistentLogger extends Log.Logger { executor.execute(() -> { StringBuilder builder = new StringBuilder(); + long entriesWritten = 0; try { File[] logs = getSortedLogFiles(); - for (int i = logs.length - 1; i >= 0; i--) { + for (int i = logs.length - 1; i >= 0 && entriesWritten <= MAX_LOG_EXPORT; i--) { try { LogFile.Reader reader = new LogFile.Reader(secret, logs[i]); - builder.append(reader.readAll()); + String entry; + while ((entry = reader.readEntry()) != null) { + entriesWritten++; + builder.append(entry).append('\n'); + } } catch (IOException e) { android.util.Log.w(TAG, "Failed to read log at index " + i + ". Removing reference."); logs[i].delete(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index de059b217b..a45efbc290 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -85,6 +85,7 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { helpTranslateButton.setOnClickListener { helpTranslate() } seedButton.setOnClickListener { showSeed() } clearAllDataButton.setOnClickListener { clearAllData() } + supportButton.setOnClickListener { shareLogs() } val isLightMode = UiModeUtilities.isDayUiMode(this) oxenLogoImageView.setImageResource(if (isLightMode) R.drawable.oxen_light_mode else R.drawable.oxen_dark_mode) versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") @@ -321,6 +322,10 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog") } + private fun shareLogs() { + ShareLogsDialog().show(supportFragmentManager,"Share Logs Dialog") + } + // endregion private inner class DisplayNameEditActionModeCallback: ActionMode.Callback { diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt new file mode 100644 index 0000000000..58e0bc8df4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ShareLogsDialog.kt @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.preferences + +import android.view.LayoutInflater +import androidx.appcompat.app.AlertDialog +import androidx.lifecycle.lifecycleScope +import kotlinx.android.synthetic.main.dialog_share_logs.view.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import network.loki.messenger.R +import org.thoughtcrime.securesms.conversation.v2.utilities.BaseDialog + +class ShareLogsDialog : BaseDialog() { + + private var shareJob: Job? = null + + override fun setContentView(builder: AlertDialog.Builder) { + val contentView = + LayoutInflater.from(requireContext()).inflate(R.layout.dialog_share_logs, null) + contentView.cancelButton.setOnClickListener { + dismiss() + } + contentView.shareButton.setOnClickListener { + // start the export and share + shareLogs() + } + builder.setView(contentView) + builder.setCancelable(false) + } + + private fun shareLogs() { + shareJob?.cancel() + shareJob = lifecycleScope.launch(Dispatchers.IO) { + + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 39c229df9c..706e4961b0 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -227,6 +227,18 @@ android:gravity="center" android:text="@string/activity_settings_survey_feedback" /> + + + + + + + + + + +