diff --git a/app/build.gradle b/app/build.gradle index 89327b29d..f81c02db2 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 363343696..1643f63bb 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 000000000..ab0b42e21 --- /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; + } +}