mirror of
https://github.com/oxen-io/session-android.git
synced 2023-12-14 02:53:01 +01:00
Added new logger.
Added a new logger that persists logs for a longer duration to the user's cache directory. Logs are encrypted. The new logs are sent in addition to the user's logcat output.
This commit is contained in:
parent
b7d83c7a1f
commit
acb40c6133
13 changed files with 728 additions and 20 deletions
|
@ -43,6 +43,10 @@ import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
|||
import org.thoughtcrime.securesms.jobs.requirements.MasterSecretRequirementProvider;
|
||||
import org.thoughtcrime.securesms.jobs.requirements.ServiceRequirementProvider;
|
||||
import org.thoughtcrime.securesms.jobs.requirements.SqlCipherMigrationRequirementProvider;
|
||||
import org.thoughtcrime.securesms.logging.AndroidLogger;
|
||||
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
|
||||
import org.thoughtcrime.securesms.logging.PersistentLogger;
|
||||
import org.thoughtcrime.securesms.logging.UncaughtExceptionLogger;
|
||||
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
|
||||
import org.thoughtcrime.securesms.service.DirectoryRefreshListener;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
|
@ -55,7 +59,6 @@ import org.webrtc.PeerConnectionFactory.InitializationOptions;
|
|||
import org.webrtc.voiceengine.WebRtcAudioManager;
|
||||
import org.webrtc.voiceengine.WebRtcAudioUtils;
|
||||
import org.whispersystems.libsignal.logging.SignalProtocolLoggerProvider;
|
||||
import org.whispersystems.libsignal.util.AndroidSignalProtocolLogger;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
@ -73,11 +76,12 @@ import dagger.ObjectGraph;
|
|||
*/
|
||||
public class ApplicationContext extends MultiDexApplication implements DependencyInjector, DefaultLifecycleObserver {
|
||||
|
||||
private static final String TAG = ApplicationContext.class.getName();
|
||||
private static final String TAG = ApplicationContext.class.getSimpleName();
|
||||
|
||||
private ExpiringMessageManager expiringMessageManager;
|
||||
private JobManager jobManager;
|
||||
private ObjectGraph objectGraph;
|
||||
private PersistentLogger persistentLogger;
|
||||
|
||||
private volatile boolean isAppVisible;
|
||||
|
||||
|
@ -90,6 +94,7 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||
super.onCreate();
|
||||
initializeRandomNumberFix();
|
||||
initializeLogging();
|
||||
initializeCrashHandling();
|
||||
initializeDependencyInjection();
|
||||
initializeJobManager();
|
||||
initializeExpiringMessageManager();
|
||||
|
@ -105,7 +110,6 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||
public void onStart(@NonNull LifecycleOwner owner) {
|
||||
isAppVisible = true;
|
||||
Log.i(TAG, "App is now visible.");
|
||||
|
||||
executePendingContactSync();
|
||||
}
|
||||
|
||||
|
@ -134,12 +138,24 @@ public class ApplicationContext extends MultiDexApplication implements Dependenc
|
|||
return isAppVisible;
|
||||
}
|
||||
|
||||
public PersistentLogger getPersistentLogger() {
|
||||
return persistentLogger;
|
||||
}
|
||||
|
||||
private void initializeRandomNumberFix() {
|
||||
PRNGFixes.apply();
|
||||
}
|
||||
|
||||
private void initializeLogging() {
|
||||
SignalProtocolLoggerProvider.setProvider(new AndroidSignalProtocolLogger());
|
||||
persistentLogger = new PersistentLogger(this);
|
||||
org.thoughtcrime.securesms.logging.Log.initialize(new AndroidLogger(), persistentLogger);
|
||||
|
||||
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());
|
||||
}
|
||||
|
||||
private void initializeCrashHandling() {
|
||||
final Thread.UncaughtExceptionHandler originalHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionLogger(originalHandler, persistentLogger));
|
||||
}
|
||||
|
||||
private void initializeJobManager() {
|
||||
|
|
|
@ -152,7 +152,7 @@ public class KeyStoreHelper {
|
|||
}
|
||||
}
|
||||
|
||||
static SealedData fromString(@NonNull String value) {
|
||||
public static SealedData fromString(@NonNull String value) {
|
||||
try {
|
||||
return JsonUtils.fromJson(value, SealedData.class);
|
||||
} catch (IOException e) {
|
||||
|
|
34
src/org/thoughtcrime/securesms/logging/AndroidLogger.java
Normal file
34
src/org/thoughtcrime/securesms/logging/AndroidLogger.java
Normal file
|
@ -0,0 +1,34 @@
|
|||
package org.thoughtcrime.securesms.logging;
|
||||
|
||||
public class AndroidLogger extends Log.Logger {
|
||||
|
||||
@Override
|
||||
public void v(String tag, String message, Throwable t) {
|
||||
android.util.Log.v(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void d(String tag, String message, Throwable t) {
|
||||
android.util.Log.d(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void i(String tag, String message, Throwable t) {
|
||||
android.util.Log.i(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void w(String tag, String message, Throwable t) {
|
||||
android.util.Log.w(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void e(String tag, String message, Throwable t) {
|
||||
android.util.Log.e(tag, message, t);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void wtf(String tag, String message, Throwable t) {
|
||||
android.util.Log.wtf(tag, message, t);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.thoughtcrime.securesms.logging;
|
||||
|
||||
import org.whispersystems.libsignal.logging.SignalProtocolLogger;
|
||||
|
||||
public class CustomSignalProtocolLogger implements SignalProtocolLogger {
|
||||
@Override
|
||||
public void log(int priority, String tag, String message) {
|
||||
switch (priority) {
|
||||
case VERBOSE:
|
||||
Log.v(tag, message);
|
||||
break;
|
||||
case DEBUG:
|
||||
Log.d(tag, message);
|
||||
break;
|
||||
case INFO:
|
||||
Log.i(tag, message);
|
||||
break;
|
||||
case WARN:
|
||||
Log.w(tag, message);
|
||||
break;
|
||||
case ERROR:
|
||||
Log.e(tag, message);
|
||||
break;
|
||||
case ASSERT:
|
||||
Log.wtf(tag, message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
13
src/org/thoughtcrime/securesms/logging/GrowingBuffer.java
Normal file
13
src/org/thoughtcrime/securesms/logging/GrowingBuffer.java
Normal file
|
@ -0,0 +1,13 @@
|
|||
package org.thoughtcrime.securesms.logging;
|
||||
|
||||
public class GrowingBuffer {
|
||||
|
||||
private byte[] buffer;
|
||||
|
||||
public byte[] get(int minLength) {
|
||||
if (buffer == null || buffer.length < minLength) {
|
||||
buffer = new byte[minLength];
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
}
|
131
src/org/thoughtcrime/securesms/logging/Log.java
Normal file
131
src/org/thoughtcrime/securesms/logging/Log.java
Normal file
|
@ -0,0 +1,131 @@
|
|||
package org.thoughtcrime.securesms.logging;
|
||||
|
||||
import android.support.annotation.MainThread;
|
||||
|
||||
public class Log {
|
||||
|
||||
private static Logger[] loggers;
|
||||
|
||||
@MainThread
|
||||
public static void initialize(Logger... loggers) {
|
||||
Log.loggers = loggers;
|
||||
}
|
||||
|
||||
public static void v(String tag, String message) {
|
||||
v(tag, message, null);
|
||||
}
|
||||
|
||||
public static void d(String tag, String message) {
|
||||
d(tag, message, null);
|
||||
}
|
||||
|
||||
public static void i(String tag, String message) {
|
||||
i(tag, message, null);
|
||||
}
|
||||
|
||||
public static void w(String tag, String message) {
|
||||
w(tag, message, null);
|
||||
}
|
||||
|
||||
public static void e(String tag, String message) {
|
||||
e(tag, message, null);
|
||||
}
|
||||
|
||||
public static void wtf(String tag, String message) {
|
||||
wtf(tag, message, null);
|
||||
}
|
||||
|
||||
public static void v(String tag, Throwable t) {
|
||||
v(tag, null, t);
|
||||
}
|
||||
|
||||
public static void d(String tag, Throwable t) {
|
||||
d(tag, null, t);
|
||||
}
|
||||
|
||||
public static void i(String tag, Throwable t) {
|
||||
i(tag, null, t);
|
||||
}
|
||||
|
||||
public static void w(String tag, Throwable t) {
|
||||
w(tag, null, t);
|
||||
}
|
||||
|
||||
public static void e(String tag, Throwable t) {
|
||||
e(tag, null, t);
|
||||
}
|
||||
|
||||
public static void wtf(String tag, Throwable t) {
|
||||
wtf(tag, null, t);
|
||||
}
|
||||
|
||||
public static void v(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.v(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.v(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void d(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.d(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.d(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void i(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.i(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.i(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void w(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.w(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.w(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void e(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.e(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.e(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
public static void wtf(String tag, String message, Throwable t) {
|
||||
if (loggers != null) {
|
||||
for (Logger logger : loggers) {
|
||||
logger.wtf(tag, message, t);
|
||||
}
|
||||
} else {
|
||||
android.util.Log.wtf(tag, message, t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static abstract class Logger {
|
||||
public abstract void v(String tag, String message, Throwable t);
|
||||
public abstract void d(String tag, String message, Throwable t);
|
||||
public abstract void i(String tag, String message, Throwable t);
|
||||
public abstract void w(String tag, String message, Throwable t);
|
||||
public abstract void e(String tag, String message, Throwable t);
|
||||
public abstract void wtf(String tag, String message, Throwable t);
|
||||
}
|
||||
}
|
139
src/org/thoughtcrime/securesms/logging/LogFile.java
Normal file
139
src/org/thoughtcrime/securesms/logging/LogFile.java
Normal file
|
@ -0,0 +1,139 @@
|
|||
package org.thoughtcrime.securesms.logging;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Conversions;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.EOFException;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PushbackInputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.ShortBufferException;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
class LogFile {
|
||||
|
||||
public static class Writer {
|
||||
|
||||
private final byte[] ivBuffer = new byte[16];
|
||||
private final GrowingBuffer ciphertextBuffer = new GrowingBuffer();
|
||||
|
||||
private final byte[] secret;
|
||||
private final File file;
|
||||
private final Cipher cipher;
|
||||
private final BufferedOutputStream outputStream;
|
||||
|
||||
Writer(@NonNull byte[] secret, @NonNull File file) throws IOException {
|
||||
this.secret = secret;
|
||||
this.file = file;
|
||||
this.outputStream = new BufferedOutputStream(new FileOutputStream(file, true));
|
||||
|
||||
try {
|
||||
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
void writeEntry(@NonNull String entry) throws IOException {
|
||||
new SecureRandom().nextBytes(ivBuffer);
|
||||
|
||||
byte[] plaintext = entry.getBytes();
|
||||
try {
|
||||
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer));
|
||||
|
||||
int cipherLength = cipher.getOutputSize(plaintext.length);
|
||||
byte[] ciphertext = ciphertextBuffer.get(cipherLength);
|
||||
cipherLength = cipher.doFinal(plaintext, 0, plaintext.length, ciphertext);
|
||||
|
||||
outputStream.write(ivBuffer);
|
||||
outputStream.write(Conversions.intToByteArray(cipherLength));
|
||||
outputStream.write(ciphertext, 0, cipherLength);
|
||||
|
||||
outputStream.flush();
|
||||
} catch (ShortBufferException | InvalidAlgorithmParameterException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
long getLogSize() {
|
||||
return file.length();
|
||||
}
|
||||
|
||||
void close() {
|
||||
Util.close(outputStream);
|
||||
}
|
||||
}
|
||||
|
||||
static class Reader {
|
||||
|
||||
private final byte[] ivBuffer = new byte[16];
|
||||
private final byte[] intBuffer = new byte[4];
|
||||
private final GrowingBuffer ciphertextBuffer = new GrowingBuffer();
|
||||
|
||||
private final byte[] secret;
|
||||
private final Cipher cipher;
|
||||
private final BufferedInputStream inputStream;
|
||||
|
||||
Reader(@NonNull byte[] secret, @NonNull File file) throws IOException {
|
||||
this.secret = secret;
|
||||
this.inputStream = new BufferedInputStream(new FileInputStream(file));
|
||||
|
||||
try {
|
||||
this.cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
}
|
||||
|
||||
String readAll() throws IOException {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
|
||||
String entry;
|
||||
while ((entry = readEntry()) != null) {
|
||||
builder.append(entry).append('\n');
|
||||
}
|
||||
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String readEntry() throws IOException {
|
||||
try {
|
||||
Util.readFully(inputStream, ivBuffer);
|
||||
Util.readFully(inputStream, intBuffer);
|
||||
|
||||
int length = Conversions.byteArrayToInt(intBuffer);
|
||||
byte[] ciphertext = ciphertextBuffer.get(length);
|
||||
|
||||
Util.readFully(inputStream, ciphertext, length);
|
||||
|
||||
try {
|
||||
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(secret, "AES"), new IvParameterSpec(ivBuffer));
|
||||
byte[] plaintext = cipher.doFinal(ciphertext, 0, length);
|
||||
|
||||
return new String(plaintext);
|
||||
} catch (InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
} catch (EOFException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package org.thoughtcrime.securesms.logging;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.security.SecureRandom;
|
||||
|
||||
class LogSecretProvider {
|
||||
|
||||
static byte[] getOrCreateAttachmentSecret(@NonNull Context context) {
|
||||
String unencryptedSecret = TextSecurePreferences.getLogUnencryptedSecret(context);
|
||||
String encryptedSecret = TextSecurePreferences.getLogEncryptedSecret(context);
|
||||
|
||||
if (unencryptedSecret != null) return parseUnencryptedSecret(unencryptedSecret);
|
||||
else if (encryptedSecret != null) return parseEncryptedSecret(encryptedSecret);
|
||||
else return createAndStoreSecret(context);
|
||||
}
|
||||
|
||||
private static byte[] parseUnencryptedSecret(String secret) {
|
||||
try {
|
||||
return Base64.decode(secret);
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Failed to decode the unecrypted secret.");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] parseEncryptedSecret(String secret) {
|
||||
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(secret);
|
||||
return KeyStoreHelper.unseal(encryptedSecret);
|
||||
} else {
|
||||
throw new AssertionError("OS downgrade not supported. KeyStore sealed data exists on platform < M!");
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] createAndStoreSecret(@NonNull Context context) {
|
||||
SecureRandom random = new SecureRandom();
|
||||
byte[] secret = new byte[32];
|
||||
random.nextBytes(secret);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(secret);
|
||||
TextSecurePreferences.setLogEncryptedSecret(context, encryptedSecret.serialize());
|
||||
} else {
|
||||
TextSecurePreferences.setLogUnencryptedSecret(context, Base64.encodeBytes(secret));
|
||||
}
|
||||
|
||||
return secret;
|
||||
}
|
||||
}
|
228
src/org/thoughtcrime/securesms/logging/PersistentLogger.java
Normal file
228
src/org/thoughtcrime/securesms/logging/PersistentLogger.java
Normal file
|
@ -0,0 +1,228 @@
|
|||
package org.thoughtcrime.securesms.logging;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.annotation.AnyThread;
|
||||
import android.support.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.database.NoExternalStorageException;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.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.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 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, "logger");
|
||||
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);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public ListenableFuture<String> getLogs() {
|
||||
final SettableFuture<String> 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<String> buildLogEntries(String level, String tag, String message, Throwable t) {
|
||||
List<String> 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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
package org.thoughtcrime.securesms.logging;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
public class UncaughtExceptionLogger implements Thread.UncaughtExceptionHandler {
|
||||
|
||||
private static final String TAG = UncaughtExceptionLogger.class.getSimpleName();
|
||||
|
||||
private final Thread.UncaughtExceptionHandler originalHandler;
|
||||
private final PersistentLogger persistentLogger;
|
||||
|
||||
public UncaughtExceptionLogger(@NonNull Thread.UncaughtExceptionHandler originalHandler, @NonNull PersistentLogger persistentLogger) {
|
||||
this.originalHandler = originalHandler;
|
||||
this.persistentLogger = persistentLogger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(Thread t, Throwable e) {
|
||||
Log.e(TAG, "", e);
|
||||
originalHandler.uncaughtException(t, e);
|
||||
}
|
||||
}
|
|
@ -48,6 +48,7 @@ import android.widget.Toast;
|
|||
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logsubmit.util.Scrubber;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
|
@ -58,6 +59,7 @@ import java.io.InputStreamReader;
|
|||
import java.lang.ref.WeakReference;
|
||||
import java.util.Iterator;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.MultipartBody;
|
||||
|
@ -77,8 +79,13 @@ import okhttp3.ResponseBody;
|
|||
*
|
||||
*/
|
||||
public class SubmitLogFragment extends Fragment {
|
||||
|
||||
private static final String TAG = SubmitLogFragment.class.getSimpleName();
|
||||
|
||||
private static final String HEADER_SYSINFO = "========== SYSINFO ========";
|
||||
private static final String HEADER_LOGCAT = "========== LOGCAT ========";
|
||||
private static final String HEADER_LOGGER = "========== LOGGER ========";
|
||||
|
||||
private EditText logPreview;
|
||||
private Button okButton;
|
||||
private Button cancelButton;
|
||||
|
@ -309,7 +316,22 @@ public class SubmitLogFragment extends Fragment {
|
|||
Context context = weakContext.get();
|
||||
if (context == null) return null;
|
||||
|
||||
return buildDescription(context) + "\n" + new Scrubber().scrub(grabLogcat());
|
||||
Scrubber scrubber = new Scrubber();
|
||||
|
||||
String newLogs;
|
||||
try {
|
||||
newLogs = scrubber.scrub(ApplicationContext.getInstance(context).getPersistentLogger().getLogs().get());
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
android.util.Log.w(TAG, "Failed to retrieve new logs.", e);
|
||||
newLogs = "Failed to retrieve logs.";
|
||||
}
|
||||
|
||||
return HEADER_SYSINFO + "\n\n" +
|
||||
buildDescription(context) + "\n\n\n" +
|
||||
HEADER_LOGCAT + "\n\n" +
|
||||
scrubber.scrub(grabLogcat()) + "\n\n\n" +
|
||||
HEADER_LOGGER + "\n\n" +
|
||||
newLogs;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.hardware.Camera.CameraInfo;
|
||||
|
@ -157,6 +158,9 @@ public class TextSecurePreferences {
|
|||
private static final String LAST_FULL_CONTACT_SYNC_TIME = "pref_last_full_contact_sync_time";
|
||||
private static final String NEEDS_FULL_CONTACT_SYNC = "pref_needs_full_contact_sync";
|
||||
|
||||
private static final String LOG_ENCRYPTED_SECRET = "pref_log_encrypted_secret";
|
||||
private static final String LOG_UNENCRYPTED_SECRET = "pref_log_unencrypted_secret";
|
||||
|
||||
public static boolean isScreenLockEnabled(@NonNull Context context) {
|
||||
return getBooleanPreference(context, SCREEN_LOCK, false);
|
||||
}
|
||||
|
@ -942,6 +946,22 @@ public class TextSecurePreferences {
|
|||
setBooleanPreference(context, NEEDS_FULL_CONTACT_SYNC, needsSync);
|
||||
}
|
||||
|
||||
public static void setLogEncryptedSecret(Context context, String base64Secret) {
|
||||
setStringPreference(context, LOG_ENCRYPTED_SECRET, base64Secret);
|
||||
}
|
||||
|
||||
public static String getLogEncryptedSecret(Context context) {
|
||||
return getStringPreference(context, LOG_ENCRYPTED_SECRET, null);
|
||||
}
|
||||
|
||||
public static void setLogUnencryptedSecret(Context context, String base64Secret) {
|
||||
setStringPreference(context, LOG_UNENCRYPTED_SECRET, base64Secret);
|
||||
}
|
||||
|
||||
public static String getLogUnencryptedSecret(Context context) {
|
||||
return getStringPreference(context, LOG_UNENCRYPTED_SECRET, null);
|
||||
}
|
||||
|
||||
public static void setBooleanPreference(Context context, String key, boolean value) {
|
||||
PreferenceManager.getDefaultSharedPreferences(context).edit().putBoolean(key, value).apply();
|
||||
}
|
||||
|
|
|
@ -54,6 +54,8 @@ import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
|
|||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.Closeable;
|
||||
import java.io.EOFException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
@ -171,17 +173,9 @@ public class Util {
|
|||
}
|
||||
}
|
||||
|
||||
public static void close(InputStream in) {
|
||||
public static void close(Closeable closeable) {
|
||||
try {
|
||||
in.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void close(OutputStream out) {
|
||||
try {
|
||||
out.close();
|
||||
closeable.close();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
@ -208,14 +202,18 @@ public class Util {
|
|||
}
|
||||
|
||||
public static void readFully(InputStream in, byte[] buffer) throws IOException {
|
||||
readFully(in, buffer, buffer.length);
|
||||
}
|
||||
|
||||
public static void readFully(InputStream in, byte[] buffer, int len) throws IOException {
|
||||
int offset = 0;
|
||||
|
||||
for (;;) {
|
||||
int read = in.read(buffer, offset, buffer.length - offset);
|
||||
if (read == -1) throw new IOException("Stream ended early");
|
||||
int read = in.read(buffer, offset, len - offset);
|
||||
if (read == -1) throw new EOFException("Stream ended early");
|
||||
|
||||
if (read + offset < buffer.length) offset += read;
|
||||
else return;
|
||||
if (read + offset < len) offset += read;
|
||||
else return;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue