diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e30c8c7137..2fc587e570 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1288,6 +1288,12 @@ + + + + + + diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index ff9a15f1a3..e4645b9a13 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -83,6 +83,7 @@ import org.thoughtcrime.securesms.ratelimit.RateLimitUtil; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.registration.RegistrationUtil; import org.thoughtcrime.securesms.ringrtc.RingRtcLogger; +import org.thoughtcrime.securesms.service.AnalyzeDatabaseAlarmListener; import org.thoughtcrime.securesms.service.DirectoryRefreshListener; import org.thoughtcrime.securesms.service.KeyCachingService; import org.thoughtcrime.securesms.service.LocalBackupListener; @@ -419,6 +420,7 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr LocalBackupListener.schedule(this); RotateSenderCertificateListener.schedule(this); RoutineMessageFetchReceiver.startOrUpdateAlarm(this); + AnalyzeDatabaseAlarmListener.schedule(this); if (BuildConfig.MANAGES_APP_UPDATES) { ApkUpdateRefreshListener.schedule(this); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/AnalyzeDatabaseJob.kt b/app/src/main/java/org/thoughtcrime/securesms/jobs/AnalyzeDatabaseJob.kt new file mode 100644 index 0000000000..9feac49dea --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/AnalyzeDatabaseJob.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2024 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.jobs + +import org.signal.core.util.getAllTables +import org.signal.core.util.logTime +import org.signal.core.util.logging.Log +import org.thoughtcrime.securesms.database.SignalDatabase +import org.thoughtcrime.securesms.jobmanager.Job +import org.thoughtcrime.securesms.jobmanager.JsonJobData +import kotlin.time.Duration.Companion.days +import kotlin.time.Duration.Companion.seconds + +/** + * Analyzes the database, updating statistics to ensure that sqlite is using the best indices possible for different queries. + * + * Given that analysis can be slow, this job will only analyze one table at a time, retrying itself so long as there are more tables to analyze. + * This should help protect it against getting canceled by the system for running for too long, while also giving it the ability to save it's place. + */ +class AnalyzeDatabaseJob private constructor( + parameters: Parameters, + private var lastCompletedTable: String? +) : Job(parameters) { + + companion object { + val TAG = Log.tag(AnalyzeDatabaseJob::class.java) + + const val KEY = "AnalyzeDatabaseJob" + + private const val KEY_LAST_COMPLETED_TABLE = "last_completed_table" + } + + constructor() : this( + Parameters.Builder() + .setMaxInstancesForFactory(1) + .setLifespan(1.days.inWholeMilliseconds) + .setMaxAttempts(Parameters.UNLIMITED) + .build(), + null + ) + + override fun serialize(): ByteArray? { + return JsonJobData.Builder() + .putString(KEY_LAST_COMPLETED_TABLE, lastCompletedTable) + .build() + .serialize() + } + + override fun getFactoryKey(): String = KEY + + override fun run(): Result { + val tables = SignalDatabase.rawDatabase.getAllTables() + .sorted() + .filterNot { it.startsWith("sqlite_") || it.contains("fts_") } + + if (tables.isEmpty()) { + Log.w(TAG, "Table list is empty!") + return Result.success() + } + + val startingIndex = if (lastCompletedTable != null) { + tables.indexOf(lastCompletedTable) + 1 + } else { + 0 + } + + if (startingIndex >= tables.size) { + Log.i(TAG, "Already finished all of the tables!") + return Result.success() + } + + val table = tables[startingIndex] + + logTime(TAG, "analyze-$table", decimalPlaces = 2) { + SignalDatabase.rawDatabase.execSQL("PRAGMA analysis_limit=1000") + SignalDatabase.rawDatabase.execSQL("ANALYZE $table") + } + + if (startingIndex >= tables.size - 1) { + Log.i(TAG, "Finished all of the tables!") + return Result.success() + } + + lastCompletedTable = table + return Result.retry(1.seconds.inWholeMilliseconds) + } + + override fun onFailure() = Unit + + class Factory : Job.Factory { + override fun create(parameters: Parameters, data: ByteArray?): AnalyzeDatabaseJob { + val builder = JsonJobData.deserialize(data) + + return AnalyzeDatabaseJob(parameters, builder.getStringOrDefault(KEY_LAST_COMPLETED_TABLE, null)) + } + } +} 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 98df8e82bf..cb14eba729 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -99,6 +99,7 @@ public final class JobManagerFactories { public static Map getJobFactories(@NonNull Application application) { return new HashMap() {{ put(AccountConsistencyWorkerJob.KEY, new AccountConsistencyWorkerJob.Factory()); + put(AnalyzeDatabaseJob.KEY, new AnalyzeDatabaseJob.Factory()); put(AttachmentCompressionJob.KEY, new AttachmentCompressionJob.Factory()); put(AttachmentCopyJob.KEY, new AttachmentCopyJob.Factory()); put(AttachmentDownloadJob.KEY, new AttachmentDownloadJob.Factory()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt index d1aeeec4aa..9a2aaecfaa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/MiscellaneousValues.kt @@ -35,6 +35,7 @@ internal class MiscellaneousValues internal constructor(store: KeyValueStore) : private const val LAST_CDS_FOREGROUND_SYNC = "misc.last_cds_foreground_sync" private const val LINKED_DEVICE_LAST_ACTIVE_CHECK_TIME = "misc.linked_device.last_active_check_time" private const val LEAST_ACTIVE_LINKED_DEVICE = "misc.linked_device.least_active" + private const val NEXT_DATABASE_ANALYSIS_TIME = "misc.next_database_analysis_time" } public override fun onFirstEverAppLaunch() { @@ -236,4 +237,9 @@ internal class MiscellaneousValues internal constructor(store: KeyValueStore) : * Details about the least-active linked device. */ var leastActiveLinkedDevice: LeastActiveLinkedDevice? by protoValue(LEAST_ACTIVE_LINKED_DEVICE, LeastActiveLinkedDevice.ADAPTER) + + /** + * When the next scheduled database analysis is. + */ + var nextDatabaseAnalysisTime: Long by longValue(NEXT_DATABASE_ANALYSIS_TIME, 0) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/AnalyzeDatabaseAlarmListener.kt b/app/src/main/java/org/thoughtcrime/securesms/service/AnalyzeDatabaseAlarmListener.kt new file mode 100644 index 0000000000..9414b40726 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/AnalyzeDatabaseAlarmListener.kt @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.service + +import android.content.Context +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies +import org.thoughtcrime.securesms.jobs.AnalyzeDatabaseJob +import org.thoughtcrime.securesms.keyvalue.SignalStore +import org.thoughtcrime.securesms.util.toMillis +import java.time.LocalDateTime + +/** + * Schedules database analysis to happen everyday at 3am. + */ +class AnalyzeDatabaseAlarmListener : PersistentAlarmManagerListener() { + companion object { + @JvmStatic + fun schedule(context: Context?) { + AnalyzeDatabaseAlarmListener().onReceive(context, getScheduleIntent()) + } + } + + override fun shouldScheduleExact(): Boolean { + return true + } + + override fun getNextScheduledExecutionTime(context: Context): Long { + var nextTime = SignalStore.misc().nextDatabaseAnalysisTime + + if (nextTime == 0L) { + nextTime = getNextTime() + SignalStore.misc().nextDatabaseAnalysisTime = nextTime + } + + return nextTime + } + + override fun onAlarm(context: Context, scheduledTime: Long): Long { + ApplicationDependencies.getJobManager().add(AnalyzeDatabaseJob()) + + val nextTime = getNextTime() + SignalStore.misc().nextDatabaseAnalysisTime = nextTime + + return nextTime + } + + private fun getNextTime(): Long { + return LocalDateTime + .now() + .plusDays(1) + .withHour(3) + .withMinute(0) + .withSecond(0) + .toMillis() + } +}