diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index e4998d134..d1a5e0984 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java index f3a493922..c48ea8b24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/KeyValueDatabase.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LogDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LogDatabase.kt new file mode 100644 index 000000000..30cf96349 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LogDatabase.kt @@ -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, 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 { + val lines = mutableListOf() + + 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, Closeable { + override fun hasNext(): Boolean { + return !cursor.isLast + } + + override fun next(): String { + cursor.moveToNext() + return CursorUtil.requireString(cursor, BODY) + } + + override fun close() { + cursor.close() + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/LogEntry.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/LogEntry.kt new file mode 100644 index 000000000..c886d8bf6 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/LogEntry.kt @@ -0,0 +1,7 @@ +package org.thoughtcrime.securesms.database.model + +data class LogEntry( + val createdAt: Long, + val lifespan: Long, + val body: String +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java index 5b8203e1b..2aa6a63e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/help/HelpViewModel.java @@ -19,29 +19,19 @@ public class HelpViewModel extends ViewModel { private static final int MINIMUM_PROBLEM_CHARS = 10; - private MutableLiveData problemMeetsLengthRequirements = new MutableLiveData<>(); - private MutableLiveData hasLines = new MutableLiveData<>(false); - private MutableLiveData categoryIndex = new MutableLiveData<>(0); - private LiveData isFormValid = Transformations.map(new LiveDataPair<>(problemMeetsLengthRequirements, hasLines), this::transformValidationData); + private final MutableLiveData problemMeetsLengthRequirements; + private final MutableLiveData categoryIndex; + private final LiveData isFormValid; private final SubmitDebugLogRepository submitDebugLogRepository; - private List 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 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 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 validationData) { - return validationData.first() == Boolean.TRUE && validationData.second() == Boolean.TRUE; - } - static class SubmitResult { private final Optional debugLogUrl; private final boolean isError; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index 1eb999fd9..77c82f2cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -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()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.kt b/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.kt new file mode 100644 index 000000000..30676ca90 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logging/PersistentLogger.kt @@ -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 = 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() + + 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 { + val out = mutableListOf() + + 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() + 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) { + 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() + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt new file mode 100644 index 000000000..eb2053346 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogDataSource.kt @@ -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, + private val untilTime: Long +) : + PagedDataSource { + + 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 { + 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) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLogger.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLoggerHeader.java similarity index 51% rename from app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLogger.java rename to app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLoggerHeader.java index fcb148af1..8a7360bfa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLogger.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/LogSectionLoggerHeader.java @@ -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 ""; } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java index fa79a0a7e..2c48d27b0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogActivity.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java index 7f1329eb2..880a154e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogAdapter.java @@ -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 { - private static final int MAX_LINE_LENGTH = 1000; + private static final int LINE_LENGTH = 500; - private final List 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 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 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 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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java index b134b1bd0..309f2252b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogRepository.java @@ -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> callback) { - executor.execute(() -> callback.onResult(getLogLinesInternal())); + public void getPrefixLogLines(@NonNull Callback> callback) { + executor.execute(() -> callback.onResult(getPrefixLogLinesInternal())); } - public void submitLog(@NonNull List lines, Callback> callback) { - SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(lines, null))); + public void buildAndSubmitLog(@NonNull Callback> callback) { + SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(System.currentTimeMillis(), getPrefixLogLinesInternal(), Tracer.getInstance().serialize()))); } - public void submitLog(@NonNull List lines, @Nullable byte[] trace, Callback> 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 prefixLines, @Nullable byte[] trace, Callback> callback) { + SignalExecutors.UNBOUNDED.execute(() -> callback.onResult(submitLogInternal(untilTime, prefixLines, trace))); } @WorkerThread - private @NonNull Optional submitLogInternal(@NonNull List lines, @Nullable byte[] trace) { + private @NonNull Optional submitLogInternal(long untilTime, @NonNull List 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 getLogLinesInternal() { + private @NonNull List getPrefixLogLinesInternal() { long startTime = System.currentTimeMillis(); int maxTitleLength = Stream.of(SECTIONS).reduce(0, (max, section) -> Math.max(max, section.getTitle().length())); diff --git a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java index 8cc4d406e..73b31ec3d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/logsubmit/SubmitDebugLogViewModel.java @@ -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> lines; - private final MutableLiveData mode; + private final SubmitDebugLogRepository repo; + private final MutableLiveData mode; + private final ProxyPagingController pagingController; + private final List staticLines; + private final MediatorLiveData> lines; + private final long firstViewTime; + private final byte[] trace; - private List 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 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 getMode() { return mode; } @@ -53,7 +75,7 @@ public class SubmitDebugLogViewModel extends ViewModel { MutableLiveData> 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 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 logs = lines.getValue(); - logs.remove(line); - - lines.postValue(logs); + throw new UnsupportedOperationException("Not yet implemented."); } boolean onBackPressed() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 90ab7807f..94704727a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/DeleteDeprecatedLogsMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/DeleteDeprecatedLogsMigrationJob.java new file mode 100644 index 000000000..8b2ac716a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/DeleteDeprecatedLogsMigrationJob.java @@ -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 { + @Override + public @NonNull DeleteDeprecatedLogsMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new DeleteDeprecatedLogsMigrationJob(parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/shakereport/ShakeToReport.java b/app/src/main/java/org/thoughtcrime/securesms/shakereport/ShakeToReport.java index e4b18be7a..8265a658a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/shakereport/ShakeToReport.java +++ b/app/src/main/java/org/thoughtcrime/securesms/shakereport/ShakeToReport.java @@ -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(); + } }); }); } diff --git a/app/src/main/res/layout/submit_debug_log_activity.xml b/app/src/main/res/layout/submit_debug_log_activity.xml index 8e8c16160..f6d470584 100644 --- a/app/src/main/res/layout/submit_debug_log_activity.xml +++ b/app/src/main/res/layout/submit_debug_log_activity.xml @@ -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"/> diff --git a/app/src/main/res/layout/submit_debug_log_line_item.xml b/app/src/main/res/layout/submit_debug_log_line_item.xml index 8bfa296f2..8f0991360 100644 --- a/app/src/main/res/layout/submit_debug_log_line_item.xml +++ b/app/src/main/res/layout/submit_debug_log_line_item.xml @@ -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"> Success! Copy this URL and add it to your issue report or support email:\n\n%1$s Share - This log will be posted publicly online for contributors to view. You may examine and edit it before uploading. + This log will be posted publicly online for contributors to view. You may examine it before uploading. support@signal.org @@ -2020,9 +2020,6 @@ Nothing found - - This log will be posted publicly online for contributors to view, you may examine and edit it before submitting. - Would you like to import your existing text messages into Signal\'s encrypted database? The default system database will not be modified or altered in any way. diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/EmptyLogger.java b/app/src/test/java/org/thoughtcrime/securesms/testutil/EmptyLogger.java index 5e26f30eb..b3dc0f57f 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/EmptyLogger.java +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/EmptyLogger.java @@ -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() { } } diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java b/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java index af3aa9512..d8f440291 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/LogRecorder.java @@ -48,7 +48,7 @@ public final class LogRecorder extends Log.Logger { } @Override - public void blockUntilAllWritesFinished() { + public void flush() { } public List getVerbose() { diff --git a/app/src/test/java/org/thoughtcrime/securesms/testutil/SystemOutLogger.java b/app/src/test/java/org/thoughtcrime/securesms/testutil/SystemOutLogger.java index 2efb3ebf8..6d7a76f4e 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/testutil/SystemOutLogger.java +++ b/app/src/test/java/org/thoughtcrime/securesms/testutil/SystemOutLogger.java @@ -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)); diff --git a/core-util/src/main/java/org/signal/core/util/logging/AndroidLogger.java b/core-util/src/main/java/org/signal/core/util/logging/AndroidLogger.java index 3f755a4e6..0d54cd786 100644 --- a/core-util/src/main/java/org/signal/core/util/logging/AndroidLogger.java +++ b/core-util/src/main/java/org/signal/core/util/logging/AndroidLogger.java @@ -36,6 +36,6 @@ public final class AndroidLogger extends Log.Logger { } @Override - public void blockUntilAllWritesFinished() { + public void flush() { } } diff --git a/core-util/src/main/java/org/signal/core/util/logging/CompoundLogger.java b/core-util/src/main/java/org/signal/core/util/logging/CompoundLogger.java index 24c198fd3..317eeb4c2 100644 --- a/core-util/src/main/java/org/signal/core/util/logging/CompoundLogger.java +++ b/core-util/src/main/java/org/signal/core/util/logging/CompoundLogger.java @@ -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(); } } } diff --git a/core-util/src/main/java/org/signal/core/util/logging/Log.java b/core-util/src/main/java/org/signal/core/util/logging/Log.java index c2b660a99..fc183b2a4 100644 --- a/core-util/src/main/java/org/signal/core/util/logging/Log.java +++ b/core-util/src/main/java/org/signal/core/util/logging/Log.java @@ -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); diff --git a/core-util/src/main/java/org/signal/core/util/logging/NoopLogger.java b/core-util/src/main/java/org/signal/core/util/logging/NoopLogger.java index 4b4b318ba..a56cbef51 100644 --- a/core-util/src/main/java/org/signal/core/util/logging/NoopLogger.java +++ b/core-util/src/main/java/org/signal/core/util/logging/NoopLogger.java @@ -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() { } } diff --git a/core-util/src/main/java/org/signal/core/util/logging/PersistentLogger.java b/core-util/src/main/java/org/signal/core/util/logging/PersistentLogger.java deleted file mode 100644 index e7d042a9f..000000000 --- a/core-util/src/main/java/org/signal/core/util/logging/PersistentLogger.java +++ /dev/null @@ -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 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 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 buildLogEntries(String level, String tag, String message, Throwable t, String threadString) { - List 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; - } -}