Move logging into a database.

fork-5.53.8
Greyson Parrelli 2021-07-19 18:30:04 -04:00
rodzic 0b85852621
commit 7419da7247
27 zmienionych plików z 723 dodań i 442 usunięć

Wyświetl plik

@ -27,14 +27,11 @@ import androidx.multidex.MultiDexApplication;
import com.google.android.gms.security.ProviderInstaller;
import net.sqlcipher.database.SQLiteDatabase;
import org.conscrypt.Conscrypt;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.logging.PersistentLogger;
import org.signal.core.util.tracing.Tracer;
import org.signal.glide.SignalGlideCodecs;
import org.signal.ringrtc.CallManager;
@ -56,7 +53,7 @@ import org.thoughtcrime.securesms.jobs.RefreshPreKeysJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.logging.CustomSignalProtocolLogger;
import org.thoughtcrime.securesms.logging.LogSecretProvider;
import org.thoughtcrime.securesms.logging.PersistentLogger;
import org.thoughtcrime.securesms.messageprocessingalarm.MessageProcessReceiver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
@ -74,7 +71,6 @@ import org.thoughtcrime.securesms.service.UpdateApkRefreshListener;
import org.thoughtcrime.securesms.storage.StorageSyncHelper;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.AppStartup;
import org.thoughtcrime.securesms.util.ByteUnit;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.FeatureFlags;
import org.thoughtcrime.securesms.util.SignalUncaughtExceptionHandler;
@ -248,7 +244,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
}
private void initializeLogging() {
persistentLogger = new PersistentLogger(this, LogSecretProvider.getOrCreateAttachmentSecret(this), BuildConfig.VERSION_NAME, FeatureFlags.internalUser() ? 15 : 7, ByteUnit.KILOBYTES.toBytes(300));
persistentLogger = new PersistentLogger(this, TimeUnit.DAYS.toMillis(2));
org.signal.core.util.logging.Log.initialize(FeatureFlags::internalUser, new AndroidLogger(), persistentLogger);
SignalProtocolLoggerProvider.setProvider(new CustomSignalProtocolLogger());

Wyświetl plik

@ -61,7 +61,7 @@ public class KeyValueDatabase extends SQLiteOpenHelper implements SignalDatabase
return instance;
}
public KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
private KeyValueDatabase(@NonNull Application application, @NonNull DatabaseSecret databaseSecret) {
super(application, DATABASE_NAME, null, DATABASE_VERSION, new SqlCipherDatabaseHook(), new SqlCipherErrorHandler(DATABASE_NAME));
this.application = application;

Wyświetl plik

@ -0,0 +1,166 @@
package org.thoughtcrime.securesms.database
import android.annotation.SuppressLint
import android.app.Application
import android.content.ContentValues
import android.database.Cursor
import net.sqlcipher.database.SQLiteDatabase
import net.sqlcipher.database.SQLiteOpenHelper
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.crypto.DatabaseSecret
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
import org.thoughtcrime.securesms.database.model.LogEntry
import org.thoughtcrime.securesms.util.CursorUtil
import org.thoughtcrime.securesms.util.SqlUtil
import java.io.Closeable
/**
* Stores logs.
*
* Logs are very performance-critical, particularly inserts and deleting old entries.
*
* This is it's own separate physical database, so it cannot do joins or queries with any other
* tables.
*/
class LogDatabase private constructor(
application: Application,
private val databaseSecret: DatabaseSecret
) : SQLiteOpenHelper(
application,
DATABASE_NAME,
null,
DATABASE_VERSION,
SqlCipherDatabaseHook(),
SqlCipherErrorHandler(DATABASE_NAME)
),
SignalDatabase {
companion object {
private val TAG = Log.tag(LogDatabase::class.java)
private const val DATABASE_VERSION = 1
private const val DATABASE_NAME = "signal-logs.db"
private const val TABLE_NAME = "log"
private const val ID = "_id"
private const val CREATED_AT = "created_at"
private const val EXPIRES_AT = "expires_at"
private const val BODY = "body"
private const val SIZE = "size"
private val CREATE_TABLE = """
CREATE TABLE $TABLE_NAME (
$ID INTEGER PRIMARY KEY,
$CREATED_AT INTEGER,
$EXPIRES_AT INTEGER,
$BODY TEXT,
$SIZE INTEGER
)
""".trimIndent()
private val CREATE_INDEXES = arrayOf(
"CREATE INDEX log_expires_at_index ON $TABLE_NAME ($EXPIRES_AT)"
)
@SuppressLint("StaticFieldLeak") // We hold an Application context, not a view context
@Volatile
private var instance: LogDatabase? = null
@JvmStatic
fun getInstance(context: Application): LogDatabase {
if (instance == null) {
synchronized(LogDatabase::class.java) {
if (instance == null) {
instance = LogDatabase(context, DatabaseSecretProvider.getOrCreateDatabaseSecret(context))
}
}
}
return instance!!
}
}
override fun onCreate(db: SQLiteDatabase) {
Log.i(TAG, "onCreate()")
db.execSQL(CREATE_TABLE)
CREATE_INDEXES.forEach { db.execSQL(it) }
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
Log.i(TAG, "onUpgrade($oldVersion, $newVersion)")
}
override fun getSqlCipherDatabase(): SQLiteDatabase {
return writableDatabase
}
fun insert(logs: List<LogEntry>, currentTime: Long) {
val db = writableDatabase
db.beginTransaction()
try {
logs.forEach { log ->
db.insert(TABLE_NAME, null, buildValues(log))
}
db.delete(TABLE_NAME, "$EXPIRES_AT < ?", SqlUtil.buildArgs(currentTime))
db.setTransactionSuccessful()
} finally {
db.endTransaction()
}
}
fun getAllBeforeTime(time: Long): Reader {
return Reader(readableDatabase.query(TABLE_NAME, arrayOf(BODY), "$CREATED_AT < ?", SqlUtil.buildArgs(time), null, null, null))
}
fun getRangeBeforeTime(start: Int, length: Int, time: Long): List<String> {
val lines = mutableListOf<String>()
readableDatabase.query(TABLE_NAME, arrayOf(BODY), "$CREATED_AT < ?", SqlUtil.buildArgs(time), null, null, null, "$start,$length").use { cursor ->
while (cursor.moveToNext()) {
lines.add(CursorUtil.requireString(cursor, BODY))
}
}
return lines
}
fun getLogCountBeforeTime(time: Long): Int {
readableDatabase.query(TABLE_NAME, arrayOf("COUNT(*)"), "$CREATED_AT < ?", SqlUtil.buildArgs(time), null, null, null).use { cursor ->
return if (cursor.moveToFirst()) {
cursor.getInt(0)
} else {
0
}
}
}
private fun buildValues(log: LogEntry): ContentValues {
return ContentValues().apply {
put(CREATED_AT, log.createdAt)
put(EXPIRES_AT, log.createdAt + log.lifespan)
put(BODY, log.body)
put(SIZE, log.body.length)
}
}
private val readableDatabase: SQLiteDatabase
get() = getReadableDatabase(databaseSecret.asString())
private val writableDatabase: SQLiteDatabase
get() = getWritableDatabase(databaseSecret.asString())
class Reader(private val cursor: Cursor) : Iterator<String>, Closeable {
override fun hasNext(): Boolean {
return !cursor.isLast
}
override fun next(): String {
cursor.moveToNext()
return CursorUtil.requireString(cursor, BODY)
}
override fun close() {
cursor.close()
}
}
}

Wyświetl plik

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.database.model
data class LogEntry(
val createdAt: Long,
val lifespan: Long,
val body: String
)

Wyświetl plik

@ -19,29 +19,19 @@ public class HelpViewModel extends ViewModel {
private static final int MINIMUM_PROBLEM_CHARS = 10;
private MutableLiveData<Boolean> problemMeetsLengthRequirements = new MutableLiveData<>();
private MutableLiveData<Boolean> hasLines = new MutableLiveData<>(false);
private MutableLiveData<Integer> categoryIndex = new MutableLiveData<>(0);
private LiveData<Boolean> isFormValid = Transformations.map(new LiveDataPair<>(problemMeetsLengthRequirements, hasLines), this::transformValidationData);
private final MutableLiveData<Boolean> problemMeetsLengthRequirements;
private final MutableLiveData<Integer> categoryIndex;
private final LiveData<Boolean> isFormValid;
private final SubmitDebugLogRepository submitDebugLogRepository;
private List<LogLine> logLines;
public HelpViewModel() {
submitDebugLogRepository = new SubmitDebugLogRepository();
submitDebugLogRepository = new SubmitDebugLogRepository();
problemMeetsLengthRequirements = new MutableLiveData<>();
categoryIndex = new MutableLiveData<>(0);
submitDebugLogRepository.getLogLines(lines -> {
logLines = lines;
hasLines.postValue(true);
});
LiveData<Boolean> firstValid = LiveDataUtil.combineLatest(problemMeetsLengthRequirements, hasLines, (validLength, validLines) -> {
return validLength == Boolean.TRUE && validLines == Boolean.TRUE;
});
isFormValid = LiveDataUtil.combineLatest(firstValid, categoryIndex, (valid, index) -> {
return valid == Boolean.TRUE && index > 0;
isFormValid = LiveDataUtil.combineLatest(problemMeetsLengthRequirements, categoryIndex, (meetsLengthRequirements, index) -> {
return meetsLengthRequirements == Boolean.TRUE && index > 0;
});
}
@ -65,7 +55,7 @@ public class HelpViewModel extends ViewModel {
MutableLiveData<SubmitResult> resultLiveData = new MutableLiveData<>();
if (includeDebugLogs) {
submitDebugLogRepository.submitLog(logLines, result -> resultLiveData.postValue(new SubmitResult(result, result.isPresent())));
submitDebugLogRepository.buildAndSubmitLog(result -> resultLiveData.postValue(new SubmitResult(result, result.isPresent())));
} else {
resultLiveData.postValue(new SubmitResult(Optional.absent(), false));
}
@ -73,10 +63,6 @@ public class HelpViewModel extends ViewModel {
return resultLiveData;
}
private boolean transformValidationData(Pair<Boolean, Boolean> validationData) {
return validationData.first() == Boolean.TRUE && validationData.second() == Boolean.TRUE;
}
static class SubmitResult {
private final Optional<String> debugLogUrl;
private final boolean isError;

Wyświetl plik

@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.migrations.BackupNotificationMigrationJob;
import org.thoughtcrime.securesms.migrations.BlobStorageLocationMigrationJob;
import org.thoughtcrime.securesms.migrations.CachedAttachmentsMigrationJob;
import org.thoughtcrime.securesms.migrations.DatabaseMigrationJob;
import org.thoughtcrime.securesms.migrations.DeleteDeprecatedLogsMigrationJob;
import org.thoughtcrime.securesms.migrations.DirectoryRefreshMigrationJob;
import org.thoughtcrime.securesms.migrations.KbsEnclaveMigrationJob;
import org.thoughtcrime.securesms.migrations.LegacyMigrationJob;
@ -175,6 +176,7 @@ public final class JobManagerFactories {
put(BlobStorageLocationMigrationJob.KEY, new BlobStorageLocationMigrationJob.Factory());
put(CachedAttachmentsMigrationJob.KEY, new CachedAttachmentsMigrationJob.Factory());
put(DatabaseMigrationJob.KEY, new DatabaseMigrationJob.Factory());
put(DeleteDeprecatedLogsMigrationJob.KEY, new DeleteDeprecatedLogsMigrationJob.Factory());
put(DirectoryRefreshMigrationJob.KEY, new DirectoryRefreshMigrationJob.Factory());
put(KbsEnclaveMigrationJob.KEY, new KbsEnclaveMigrationJob.Factory());
put(LegacyMigrationJob.KEY, new LegacyMigrationJob.Factory());

Wyświetl plik

@ -0,0 +1,206 @@
package org.thoughtcrime.securesms.logging
import android.app.Application
import android.os.Looper
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.database.model.LogEntry
import java.io.ByteArrayOutputStream
import java.io.PrintStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* A logger that will persist log entries in [LogDatabase].
*
* We log everywhere, and we never want it to slow down the app, so performance is critical here.
* This class takes special care to do as little as possible on the main thread, instead letting the background thread do the work.
*
* The process looks something like:
* - Main thread creates a [LogRequest] object and puts it in a queue
* - The [WriteThread] constantly pulls from that queue, formats the logs, and writes them to the database.
*/
class PersistentLogger(
application: Application,
defaultLifespan: Long
) : Log.Logger() {
companion object {
private const val LOG_V = "V"
private const val LOG_D = "D"
private const val LOG_I = "I"
private const val LOG_W = "W"
private const val LOG_E = "E"
private const val LOG_WTF = "A"
}
private val logEntries = LogRequests()
private val logDatabase = LogDatabase.getInstance(application)
private val cachedThreadString: ThreadLocal<String> = ThreadLocal()
init {
WriteThread(logEntries, logDatabase, defaultLifespan).apply {
priority = Thread.MIN_PRIORITY
}.start()
}
override fun v(tag: String?, message: String?, t: Throwable?) {
write(LOG_V, tag, message, t)
}
override fun d(tag: String?, message: String?, t: Throwable?) {
write(LOG_D, tag, message, t)
}
override fun i(tag: String?, message: String?, t: Throwable?) {
write(LOG_I, tag, message, t)
}
override fun w(tag: String?, message: String?, t: Throwable?) {
write(LOG_W, tag, message, t)
}
override fun e(tag: String?, message: String?, t: Throwable?) {
write(LOG_E, tag, message, t)
}
override fun wtf(tag: String?, message: String?, t: Throwable?) {
write(LOG_WTF, tag, message, t)
}
override fun flush() {
logEntries.blockForFlushed()
}
private fun write(level: String, tag: String?, message: String?, t: Throwable?) {
logEntries.add(LogRequest(level, tag ?: "null", message, Date(), getThreadString(), t))
}
private fun getThreadString(): String {
var threadString = cachedThreadString.get()
if (cachedThreadString.get() == null) {
threadString = if (Looper.myLooper() == Looper.getMainLooper()) {
"main "
} else {
String.format("%-5s", Thread.currentThread().id)
}
cachedThreadString.set(threadString)
}
return threadString!!
}
private data class LogRequest(
val level: String,
val tag: String,
val message: String?,
val date: Date,
val threadString: String,
val throwable: Throwable?
)
private class WriteThread(
private val requests: LogRequests,
private val db: LogDatabase,
private val defaultLifespan: Long
) : Thread("signal-logger") {
private val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS zzz", Locale.US)
private val buffer = mutableListOf<LogRequest>()
override fun run() {
while (true) {
requests.blockForRequests(buffer)
db.insert(buffer.flatMap { requestToEntries(it) }, System.currentTimeMillis())
buffer.clear()
requests.notifyFlushed()
}
}
fun requestToEntries(request: LogRequest): List<LogEntry> {
val out = mutableListOf<LogEntry>()
out.add(
LogEntry(
createdAt = request.date.time,
lifespan = defaultLifespan,
body = formatBody(request.threadString, request.date, request.level, request.tag, request.message)
)
)
if (request.throwable != null) {
val outputStream = ByteArrayOutputStream()
request.throwable.printStackTrace(PrintStream(outputStream))
val trace = String(outputStream.toByteArray())
val lines = trace.split("\\n".toRegex()).toTypedArray()
val entries = lines.map { line ->
LogEntry(
createdAt = request.date.time,
lifespan = defaultLifespan,
body = formatBody(request.threadString, request.date, request.level, request.tag, line)
)
}
out.addAll(entries)
}
return out
}
fun formatBody(threadString: String, date: Date, level: String, tag: String, message: String?): String {
return "[${BuildConfig.VERSION_NAME}] [$threadString] ${dateFormat.format(date)} $level $tag: $message"
}
}
private class LogRequests {
val logs = mutableListOf<LogRequest>()
val logLock = Object()
var flushed = false
val flushedLock = Object()
fun add(entry: LogRequest) {
synchronized(logLock) {
logs.add(entry)
logLock.notify()
}
}
/**
* Blocks until requests are available. When they are, the [buffer] will be populated with all pending requests.
* Note: This method gets hit a *lot*, which is why we're using a buffer instead of spamming out new lists every time.
*/
fun blockForRequests(buffer: MutableList<LogRequest>) {
synchronized(logLock) {
while (logs.isEmpty()) {
logLock.wait()
}
buffer.addAll(logs)
logs.clear()
flushed = false
}
}
fun blockForFlushed() {
synchronized(flushedLock) {
while (!flushed) {
flushedLock.wait()
}
}
}
fun notifyFlushed() {
synchronized(flushedLock) {
flushed = true
flushedLock.notify()
}
}
}
}

Wyświetl plik

@ -0,0 +1,42 @@
package org.thoughtcrime.securesms.logsubmit
import android.app.Application
import org.signal.paging.PagedDataSource
import org.thoughtcrime.securesms.database.LogDatabase
import org.thoughtcrime.securesms.logsubmit.util.Scrubber
/**
* Retrieves logs to show in the [SubmitDebugLogActivity].
*
* @param prefixLines A static list of lines to show before all of the lines retrieved from [LogDatabase]
* @param untilTime Only show logs before this time. This is our way of making sure the set of logs we show on this screen doesn't grow.
*/
class LogDataSource(
application: Application,
private val prefixLines: List<LogLine>,
private val untilTime: Long
) :
PagedDataSource<LogLine> {
val logDatabase = LogDatabase.getInstance(application)
override fun size(): Int {
return prefixLines.size + logDatabase.getLogCountBeforeTime(untilTime)
}
override fun load(start: Int, length: Int, cancellationSignal: PagedDataSource.CancellationSignal): List<LogLine> {
if (start + length < prefixLines.size) {
return prefixLines.subList(start, start + length)
} else if (start < prefixLines.size) {
return prefixLines.subList(start, prefixLines.size) +
logDatabase.getRangeBeforeTime(0, length - (prefixLines.size - start), untilTime).map { convertToLogLine(it) }
} else {
return logDatabase.getRangeBeforeTime(start - prefixLines.size, length, untilTime).map { convertToLogLine(it) }
}
}
private fun convertToLogLine(raw: String): LogLine {
val scrubbed: String = Scrubber.scrub(raw).toString()
return SimpleLogLine(scrubbed, LogStyleParser.parseStyle(scrubbed), LogLine.Placeholder.NONE)
}
}

Wyświetl plik

@ -4,9 +4,10 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.ApplicationContext;
public class LogSectionLogger implements LogSection {
/**
* Because the actual contents of this section are paged from the database, this class just has a header and no content.
*/
public class LogSectionLoggerHeader implements LogSection {
@Override
public @NonNull String getTitle() {
@ -15,7 +16,6 @@ public class LogSectionLogger implements LogSection {
@Override
public @NonNull CharSequence getContent(@NonNull Context context) {
CharSequence logs = ApplicationContext.getInstance(context).getPersistentLogger().getLogs();
return logs != null ? logs : "Unable to retrieve logs.";
return "";
}
}

Wyświetl plik

@ -60,6 +60,8 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setTitle(R.string.HelpSettingsFragment__debug_log);
this.viewModel = ViewModelProviders.of(this, new SubmitDebugLogViewModel.Factory()).get(SubmitDebugLogViewModel.class);
initView();
initViewModel();
}
@ -115,16 +117,13 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
public boolean onOptionsItemSelected(MenuItem item) {
super.onOptionsItemSelected(item);
switch (item.getItemId()) {
case android.R.id.home:
finish();
return true;
case R.id.menu_edit_log:
viewModel.onEditButtonPressed();
break;
case R.id.menu_done_editing_log:
viewModel.onDoneEditingButtonPressed();
break;
if (item.getItemId() == android.R.id.home) {
finish();
return true;
} else if (item.getItemId() == R.id.menu_edit_log) {
viewModel.onEditButtonPressed();
} else if (item.getItemId() == R.id.menu_done_editing_log) {
viewModel.onDoneEditingButtonPressed();
}
return false;
@ -150,10 +149,11 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
this.scrollToBottomButton = findViewById(R.id.debug_log_scroll_to_bottom);
this.scrollToTopButton = findViewById(R.id.debug_log_scroll_to_top);
this.adapter = new SubmitDebugLogAdapter(this);
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());
@ -181,8 +181,6 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
}
private void initViewModel() {
this.viewModel = ViewModelProviders.of(this, new SubmitDebugLogViewModel.Factory()).get(SubmitDebugLogViewModel.class);
viewModel.getLines().observe(this, this::presentLines);
viewModel.getMode().observe(this, this::presentMode);
}
@ -196,7 +194,7 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
submitButton.setVisibility(View.VISIBLE);
}
adapter.setLines(lines);
adapter.submitList(lines);
}
private void presentMode(@NonNull SubmitDebugLogViewModel.Mode mode) {
@ -204,9 +202,10 @@ public class SubmitDebugLogActivity extends BaseActivity implements SubmitDebugL
case NORMAL:
editBanner.setVisibility(View.GONE);
adapter.setEditing(false);
editMenuItem.setVisible(true);
doneMenuItem.setVisible(false);
searchMenuItem.setVisible(true);
// TODO [greyson][log] Not yet implemented
// editMenuItem.setVisible(true);
// doneMenuItem.setVisible(false);
// searchMenuItem.setVisible(true);
break;
case SUBMITTING:
editBanner.setVisibility(View.GONE);

Wyświetl plik

@ -10,8 +10,7 @@ import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.RecyclerView;
import com.annimon.stream.Stream;
import org.signal.paging.PagingController;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ListenableHorizontalScrollView;
@ -21,26 +20,52 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class SubmitDebugLogAdapter extends RecyclerView.Adapter<SubmitDebugLogAdapter.LineViewHolder> {
private static final int MAX_LINE_LENGTH = 1000;
private static final int LINE_LENGTH = 500;
private final List<LogLine> lines;
private final ScrollManager scrollManager;
private final Listener listener;
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<LogLine> lines;
private boolean editing;
private int longestLine;
public SubmitDebugLogAdapter(@NonNull Listener listener) {
this.listener = listener;
this.lines = new ArrayList<>();
this.scrollManager = new ScrollManager();
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) {
return lines.get(position).getId();
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<LogLine> list) {
this.lines.clear();
this.lines.addAll(list);
notifyDataSetChanged();
}
@Override
public int getItemCount() {
return lines.size();
}
@Override
@ -50,7 +75,13 @@ public class SubmitDebugLogAdapter extends RecyclerView.Adapter<SubmitDebugLogAd
@Override
public void onBindViewHolder(@NonNull LineViewHolder holder, int position) {
holder.bind(lines.get(position), longestLine, editing, scrollManager, listener);
LogLine item = getItem(position);
if (item == null) {
item = SimpleLogLine.EMPTY;
}
holder.bind(item, LINE_LENGTH, editing, scrollManager, listener);
}
@Override
@ -58,21 +89,6 @@ public class SubmitDebugLogAdapter extends RecyclerView.Adapter<SubmitDebugLogAd
holder.unbind(scrollManager);
}
@Override
public int getItemCount() {
return lines.size();
}
public void setLines(@NonNull List<LogLine> lines) {
this.lines.clear();
this.lines.addAll(lines);
this.longestLine = Stream.of(lines).reduce(0, (currentMax, line) -> Math.max(currentMax, line.getText().length()));
this.longestLine = Math.min(longestLine, MAX_LINE_LENGTH);
notifyDataSetChanged();
}
public void setEditing(boolean editing) {
this.editing = editing;
notifyDataSetChanged();

Wyświetl plik

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.logsubmit;
import android.app.Application;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -11,20 +14,26 @@ import com.annimon.stream.Stream;
import org.json.JSONException;
import org.json.JSONObject;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.core.util.tracing.Tracer;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logsubmit.util.Scrubber;
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.whispersystems.libsignal.util.guava.Optional;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.regex.Pattern;
import java.util.zip.GZIPOutputStream;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
@ -33,6 +42,9 @@ import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import okhttp3.ResponseBody;
import okio.BufferedSink;
import okio.Okio;
import okio.Source;
/**
* Handles retrieving, scrubbing, and uploading of all debug logs.
@ -68,10 +80,10 @@ public class SubmitDebugLogRepository {
add(new LogSectionThreads());
add(new LogSectionBlockedThreads());
add(new LogSectionLogcat());
add(new LogSectionLogger());
add(new LogSectionLoggerHeader());
}};
private final Context context;
private final Application context;
private final ExecutorService executor;
public SubmitDebugLogRepository() {
@ -79,44 +91,88 @@ public class SubmitDebugLogRepository {
this.executor = SignalExecutors.SERIAL;
}
public void getLogLines(@NonNull Callback<List<LogLine>> callback) {
executor.execute(() -> callback.onResult(getLogLinesInternal()));
public void getPrefixLogLines(@NonNull Callback<List<LogLine>> callback) {
executor.execute(() -> callback.onResult(getPrefixLogLinesInternal()));
}
public void submitLog(@NonNull List<LogLine> lines, Callback<Optional<String>> callback) {
SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(lines, null)));
public void buildAndSubmitLog(@NonNull Callback<Optional<String>> callback) {
SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(System.currentTimeMillis(), getPrefixLogLinesInternal(), Tracer.getInstance().serialize())));
}
public void submitLog(@NonNull List<LogLine> lines, @Nullable byte[] trace, Callback<Optional<String>> callback) {
SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(lines, trace)));
/**
* 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<LogLine> prefixLines, @Nullable byte[] trace, Callback<Optional<String>> callback) {
SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(untilTime, prefixLines, trace)));
}
@WorkerThread
private @NonNull Optional<String> submitLogInternal(@NonNull List<LogLine> lines, @Nullable byte[] trace) {
private @NonNull Optional<String> submitLogInternal(long untilTime, @NonNull List<LogLine> prefixLines, @Nullable byte[] trace) {
String traceUrl = null;
if (trace != null) {
try {
traceUrl = uploadContent("application/octet-stream", trace);
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.absent();
}
}
StringBuilder bodyBuilder = new StringBuilder();
for (LogLine line : lines) {
StringBuilder prefixStringBuilder = new StringBuilder();
for (LogLine line : prefixLines) {
switch (line.getPlaceholderType()) {
case NONE:
bodyBuilder.append(line.getText()).append('\n');
prefixStringBuilder.append(line.getText()).append('\n');
break;
case TRACE:
bodyBuilder.append(traceUrl).append('\n');
prefixStringBuilder.append(traceUrl).append('\n');
break;
}
}
try {
String logUrl = uploadContent("text/plain", bodyBuilder.toString().getBytes());
ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
Uri gzipUri = BlobProvider.getInstance()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType("application/gzip")
.createForSingleSessionOnDiskAsync(context, null, null);
OutputStream gzipOutput = new GZIPOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
gzipOutput.write(prefixStringBuilder.toString().getBytes());
try (LogDatabase.Reader reader = LogDatabase.getInstance(context).getAllBeforeTime(untilTime)) {
while (reader.hasNext()) {
gzipOutput.write(Scrubber.scrub(reader.next()).toString().getBytes());
gzipOutput.write("\n".getBytes());
}
}
StreamUtil.close(gzipOutput);
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);
}
});
BlobProvider.getInstance().delete(context, gzipUri);
return Optional.of(logUrl);
} catch (IOException e) {
Log.w(TAG, "Error during log upload.", e);
@ -125,7 +181,7 @@ public class SubmitDebugLogRepository {
}
@WorkerThread
private @NonNull String uploadContent(@NonNull String contentType, @NonNull byte[] content) throws IOException {
private @NonNull String uploadContent(@NonNull String contentType, @NonNull RequestBody requestBody) throws IOException {
try {
OkHttpClient client = new OkHttpClient.Builder().addInterceptor(new StandardUserAgentInterceptor()).dns(SignalServiceNetworkAccess.DNS).build();
Response response = client.newCall(new Request.Builder().url(API_ENDPOINT).get().build()).execute();
@ -149,7 +205,7 @@ public class SubmitDebugLogRepository {
post.addFormDataPart(key, fields.getString(key));
}
post.addFormDataPart("file", "file", RequestBody.create(MediaType.parse(contentType), content));
post.addFormDataPart("file", "file", requestBody);
Response postResponse = client.newCall(new Request.Builder().url(url).post(post.build()).build()).execute();
@ -165,7 +221,7 @@ public class SubmitDebugLogRepository {
}
@WorkerThread
private @NonNull List<LogLine> getLogLinesInternal() {
private @NonNull List<LogLine> getPrefixLogLinesInternal() {
long startTime = System.currentTimeMillis();
int maxTitleLength = Stream.of(SECTIONS).reduce(0, (max, section) -> Math.max(max, section.getTitle().length()));

Wyświetl plik

@ -1,42 +1,60 @@
package org.thoughtcrime.securesms.logsubmit;
import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import com.annimon.stream.Stream;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.tracing.Tracer;
import org.signal.paging.PagedData;
import org.signal.paging.PagingConfig;
import org.signal.paging.PagingController;
import org.signal.paging.ProxyPagingController;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.whispersystems.libsignal.util.guava.Optional;
import java.util.Collections;
import java.util.ArrayList;
import java.util.List;
public class SubmitDebugLogViewModel extends ViewModel {
private final SubmitDebugLogRepository repo;
private final DefaultValueLiveData<List<LogLine>> lines;
private final MutableLiveData<Mode> mode;
private final SubmitDebugLogRepository repo;
private final MutableLiveData<Mode> mode;
private final ProxyPagingController pagingController;
private final List<LogLine> staticLines;
private final MediatorLiveData<List<LogLine>> lines;
private final long firstViewTime;
private final byte[] trace;
private List<LogLine> sourceLines;
private byte[] trace;
private SubmitDebugLogViewModel() {
this.repo = new SubmitDebugLogRepository();
this.lines = new DefaultValueLiveData<>(Collections.emptyList());
this.mode = new MutableLiveData<>();
this.trace = Tracer.getInstance().serialize();
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<>();
repo.getLogLines(result -> {
sourceLines = result;
mode.postValue(Mode.NORMAL);
lines.postValue(sourceLines);
repo.getPrefixLogLines(staticLines -> {
this.staticLines.addAll(staticLines);
LogDataSource dataSource = new LogDataSource(ApplicationDependencies.getApplication(), staticLines, firstViewTime);
PagingConfig config = new PagingConfig.Builder().setPageSize(100)
.setBufferPages(3)
.setStartIndex(0)
.build();
PagedData<LogLine> pagedData = PagedData.create(dataSource, config);
ThreadUtil.runOnMain(() -> {
pagingController.set(pagedData.getController());
lines.addSource(pagedData.getData(), lines::setValue);
mode.setValue(Mode.NORMAL);
});
});
}
@ -44,6 +62,10 @@ public class SubmitDebugLogViewModel extends ViewModel {
return lines;
}
@NonNull PagingController getPagingController() {
return pagingController;
}
@NonNull LiveData<Mode> getMode() {
return mode;
}
@ -53,7 +75,7 @@ public class SubmitDebugLogViewModel extends ViewModel {
MutableLiveData<Optional<String>> result = new MutableLiveData<>();
repo.submitLog(lines.getValue(), trace, value -> {
repo.submitLogWithPrefixLines(firstViewTime, staticLines, trace, value -> {
mode.postValue(Mode.NORMAL);
result.postValue(value);
});
@ -62,35 +84,23 @@ public class SubmitDebugLogViewModel extends ViewModel {
}
void onQueryUpdated(@NonNull String query) {
if (TextUtils.isEmpty(query)) {
lines.postValue(sourceLines);
} else {
List<LogLine> filtered = Stream.of(sourceLines)
.filter(l -> l.getText().toLowerCase().contains(query.toLowerCase()))
.toList();
lines.postValue(filtered);
}
throw new UnsupportedOperationException("Not yet implemented.");
}
void onSearchClosed() {
lines.postValue(sourceLines);
throw new UnsupportedOperationException("Not yet implemented.");
}
void onEditButtonPressed() {
mode.setValue(Mode.EDIT);
throw new UnsupportedOperationException("Not yet implemented.");
}
void onDoneEditingButtonPressed() {
mode.setValue(Mode.NORMAL);
throw new UnsupportedOperationException("Not yet implemented.");
}
void onLogDeleted(@NonNull LogLine line) {
sourceLines.remove(line);
List<LogLine> logs = lines.getValue();
logs.remove(line);
lines.postValue(logs);
throw new UnsupportedOperationException("Not yet implemented.");
}
boolean onBackPressed() {

Wyświetl plik

@ -78,9 +78,10 @@ public class ApplicationMigrations {
static final int SENDER_KEY_2 = 36;
static final int DB_AUTOINCREMENT = 37;
static final int ATTACHMENT_CLEANUP = 38;
static final int LOG_CLEANUP = 39;
}
public static final int CURRENT_VERSION = 38;
public static final int CURRENT_VERSION = 39;
/**
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
@ -342,6 +343,10 @@ public class ApplicationMigrations {
jobs.put(Version.ATTACHMENT_CLEANUP, new AttachmentCleanupMigrationJob());
}
if (lastSeenVersion < Version.LOG_CLEANUP) {
jobs.put(Version.LOG_CLEANUP, new DeleteDeprecatedLogsMigrationJob());
}
return jobs;
}

Wyświetl plik

@ -0,0 +1,72 @@
package org.thoughtcrime.securesms.migrations;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.jobmanager.Data;
import org.thoughtcrime.securesms.jobmanager.Job;
import java.io.File;
/**
* We moved from storing logs in encrypted files to just storing them in an encrypted database. So we need to delete the leftover files.
*/
public class DeleteDeprecatedLogsMigrationJob extends MigrationJob {
private static final String TAG = Log.tag(DeleteDeprecatedLogsMigrationJob.class);
public static final String KEY = "DeleteDeprecatedLogsMigrationJob";
public DeleteDeprecatedLogsMigrationJob() {
this(new Parameters.Builder().build());
}
private DeleteDeprecatedLogsMigrationJob(@NonNull Parameters parameters) {
super(parameters);
}
@Override
public @NonNull String getFactoryKey() {
return KEY;
}
@Override
boolean isUiBlocking() {
return false;
}
@Override
void performMigration() {
File logDir = new File(context.getCacheDir(), "log");
if (logDir.exists()) {
File[] files = logDir.listFiles();
int count = 0;
if (files != null) {
for (File f : files) {
count += f.delete() ? 1 : 0;
}
}
if (!logDir.delete()) {
Log.w(TAG, "Failed to delete log directory.");
}
Log.i(TAG, "Deleted " + count + " log files.");
} else {
Log.w(TAG, "Log directory does not exist.");
}
}
@Override
boolean shouldRetry(@NonNull Exception e) {
return false;
}
public static final class Factory implements Job.Factory<DeleteDeprecatedLogsMigrationJob> {
@Override
public @NonNull DeleteDeprecatedLogsMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new DeleteDeprecatedLogsMigrationJob(parameters);
}
}
}

Wyświetl plik

@ -89,22 +89,18 @@ public final class ShakeToReport implements ShakeDetector.Listener {
Log.i(TAG, "Submitting log...");
repo.getLogLines(lines -> {
Log.i(TAG, "Retrieved log lines...");
repo.buildAndSubmitLog(url -> {
Log.i(TAG, "Logs uploaded!");
repo.submitLog(lines, Tracer.getInstance().serialize(), url -> {
Log.i(TAG, "Logs uploaded!");
ThreadUtil.runOnMain(() -> {
spinner.dismiss();
ThreadUtil.runOnMain(() -> {
spinner.dismiss();
if (url.isPresent()) {
showPostSubmitDialog(activity, url.get());
} else {
Toast.makeText(activity, R.string.ShakeToReport_failed_to_submit, Toast.LENGTH_SHORT).show();
enableIfVisible();
}
});
if (url.isPresent()) {
showPostSubmitDialog(activity, url.get());
} else {
Toast.makeText(activity, R.string.ShakeToReport_failed_to_submit, Toast.LENGTH_SHORT).show();
enableIfVisible();
}
});
});
}

Wyświetl plik

@ -11,7 +11,7 @@
android:layout_width="0dp"
android:layout_height="wrap_content"
android:padding="8dp"
android:text="@string/SubmitDebugLogActivity_this_log_will_be_posted_publicly_online"
android:text="@string/SubmitDebugLogActivity_this_log_will_be_posted_publicly_online_for_contributors"
android:textColor="@color/core_black"
android:background="@color/core_yellow"
android:visibility="gone"
@ -47,9 +47,8 @@
android:id="@+id/debug_log_lines"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="@id/debug_log_header_barrier"
app:layout_constraintBottom_toTopOf="@id/debug_log_submit_button"/>

Wyświetl plik

@ -3,6 +3,8 @@
android:id="@+id/log_item_scroll"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:scrollbars="none">
<TextView

Wyświetl plik

@ -1573,7 +1573,7 @@
<string name="SubmitDebugLogActivity_success">Success!</string>
<string name="SubmitDebugLogActivity_copy_this_url_and_add_it_to_your_issue">Copy this URL and add it to your issue report or support email:\n\n<b>%1$s</b></string>
<string name="SubmitDebugLogActivity_share">Share</string>
<string name="SubmitDebugLogActivity_this_log_will_be_posted_publicly_online">This log will be posted publicly online for contributors to view. You may examine and edit it before uploading.</string>
<string name="SubmitDebugLogActivity_this_log_will_be_posted_publicly_online_for_contributors">This log will be posted publicly online for contributors to view. You may examine it before uploading.</string>
<!-- SupportEmailUtil -->
<string name="SupportEmailUtil_support_email" translatable="false">support@signal.org</string>
@ -2020,9 +2020,6 @@
<!-- giphy_fragment -->
<string name="giphy_fragment__nothing_found">Nothing found</string>
<!-- log_submit_activity -->
<string name="log_submit_activity__this_log_will_be_posted_online">This log will be posted publicly online for contributors to view, you may examine and edit it before submitting.</string>
<!-- database_migration_activity -->
<string name="database_migration_activity__would_you_like_to_import_your_existing_text_messages">Would you like to import your existing text messages into Signal\'s encrypted database?</string>
<string name="database_migration_activity__the_default_system_database_will_not_be_modified">The default system database will not be modified or altered in any way.</string>

Wyświetl plik

@ -22,5 +22,5 @@ public class EmptyLogger extends Log.Logger {
public void wtf(String tag, String message, Throwable t) { }
@Override
public void blockUntilAllWritesFinished() { }
public void flush() { }
}

Wyświetl plik

@ -48,7 +48,7 @@ public final class LogRecorder extends Log.Logger {
}
@Override
public void blockUntilAllWritesFinished() {
public void flush() {
}
public List<Entry> getVerbose() {

Wyświetl plik

@ -34,7 +34,7 @@ public final class SystemOutLogger extends Log.Logger {
}
@Override
public void blockUntilAllWritesFinished() { }
public void flush() { }
private void printlnFormatted(char level, String tag, String message, Throwable t) {
System.out.println(format(level, tag, message, t));

Wyświetl plik

@ -36,6 +36,6 @@ public final class AndroidLogger extends Log.Logger {
}
@Override
public void blockUntilAllWritesFinished() {
public void flush() {
}
}

Wyświetl plik

@ -57,9 +57,9 @@ class CompoundLogger extends Log.Logger {
}
@Override
public void blockUntilAllWritesFinished() {
public void flush() {
for (Log.Logger logger : loggers) {
logger.blockUntilAllWritesFinished();
logger.flush();
}
}
}

Wyświetl plik

@ -5,8 +5,6 @@ import android.annotation.SuppressLint;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import java.util.logging.Logger;
@SuppressLint("LogNotSignal")
public final class Log {
@ -124,7 +122,7 @@ public final class Log {
}
public static void blockUntilAllWritesFinished() {
logger.blockUntilAllWritesFinished();
logger.flush();
}
public static abstract class Logger {
@ -134,7 +132,7 @@ public final class Log {
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);
public abstract void blockUntilAllWritesFinished();
public abstract void flush();
public void v(String tag, String message) {
v(tag, message, null);

Wyświetl plik

@ -23,5 +23,5 @@ class NoopLogger extends Log.Logger {
public void wtf(String tag, String message, Throwable t) { }
@Override
public void blockUntilAllWritesFinished() { }
public void flush() { }
}

Wyświetl plik

@ -1,274 +0,0 @@
package org.signal.core.util.logging;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Looper;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
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.Locale;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
@SuppressLint("LogNotSignal")
public final 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 SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS zzz", Locale.US);
private final Context context;
private final Executor executor;
private final byte[] secret;
private final String logTag;
private final int maxLogFiles;
private final long maxLogFileSize;
private LogFile.Writer writer;
private final ThreadLocal<String> cachedThreadString;
public PersistentLogger(@NonNull Context context, @NonNull byte[] secret, @NonNull String logTag, int maxLogFiles, long maxLogFileSize) {
this.context = context.getApplicationContext();
this.secret = secret;
this.logTag = logTag;
this.maxLogFiles = maxLogFiles;
this.maxLogFileSize = maxLogFileSize;
this.cachedThreadString = new ThreadLocal<>();
this.executor = Executors.newSingleThreadExecutor(r -> {
Thread thread = new Thread(r, "signal-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 @Nullable CharSequence getLogs() {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<CharSequence> logs = new AtomicReference<>();
executor.execute(() -> {
StringBuilder builder = new StringBuilder();
try {
File[] logFiles = getSortedLogFiles();
for (int i = logFiles.length - 1; i >= 0; i--) {
try {
LogFile.Reader reader = new LogFile.Reader(secret, logFiles[i]);
builder.append(reader.readAll());
} catch (IOException e) {
android.util.Log.w(TAG, "Failed to read log at index " + i + ". Removing reference.");
logFiles[i].delete();
}
}
logs.set(builder);
} catch (IOException e) {
logs.set(null);
}
latch.countDown();
});
try {
latch.await();
return logs.get();
} catch (InterruptedException e) {
android.util.Log.w(TAG, "Failed to wait for logs to be retrieved.");
return null;
}
}
@WorkerThread
private void initializeWriter() {
try {
writer = new LogFile.Writer(secret, getOrCreateActiveLogFile());
} catch (IOException e) {
android.util.Log.e(TAG, "Failed to initialize writer.", e);
}
}
@AnyThread
private void write(String level, String tag, String message, Throwable t) {
String threadString = cachedThreadString.get();
if (cachedThreadString.get() == null) {
if (Looper.myLooper() == Looper.getMainLooper()) {
threadString = "main ";
} else {
threadString = String.format("%-5s", Thread.currentThread().getId());
}
cachedThreadString.set(threadString);
}
final String finalThreadString = threadString;
executor.execute(() -> {
try {
if (writer == null) {
return;
}
if (writer.getLogSize() >= maxLogFileSize) {
writer.close();
writer = new LogFile.Writer(secret, createNewLogFile());
trimLogFilesOverMax();
}
for (String entry : buildLogEntries(level, tag, message, t, finalThreadString)) {
writer.writeEntry(entry);
}
} catch (IOException e) {
android.util.Log.w(TAG, "Failed to write line. Deleting all logs and starting over.");
deleteAllLogs();
initializeWriter();
}
});
}
private void trimLogFilesOverMax() throws IOException {
File[] logs = getSortedLogFiles();
if (logs.length > maxLogFiles) {
for (int i = maxLogFiles; i < logs.length; i++) {
logs[i].delete();
}
}
}
private void deleteAllLogs() {
try {
File[] logs = getSortedLogFiles();
for (File log : logs) {
log.delete();
}
} catch (IOException e) {
android.util.Log.w(TAG, "Was unable to delete logs.", e);
}
}
private File getOrCreateActiveLogFile() throws IOException {
File[] logs = getSortedLogFiles();
if (logs.length > 0) {
return logs[0];
}
return createNewLogFile();
}
private File createNewLogFile() throws IOException {
return new File(getOrCreateLogDirectory(), FILENAME_PREFIX + System.currentTimeMillis());
}
private File[] getSortedLogFiles() throws IOException {
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 IOException {
File logDir = new File(context.getCacheDir(), LOG_DIRECTORY);
if (!logDir.exists() && !logDir.mkdir()) {
throw new IOException("Unable to create log directory.");
}
return logDir;
}
private List<String> buildLogEntries(String level, String tag, String message, Throwable t, String threadString) {
List<String> entries = new LinkedList<>();
Date date = new Date();
entries.add(buildEntry(level, tag, message, date, threadString));
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, threadString));
}
}
return entries;
}
private String buildEntry(String level, String tag, String message, Date date, String threadString) {
return '[' + logTag + "] [" + threadString + "] " + DATE_FORMAT.format(date) + ' ' + level + ' ' + tag + ": " + message;
}
}