Add restore local backupv2 infra.
This commit is contained in:
parent
00d20a1917
commit
a8bf03af89
41 changed files with 615 additions and 324 deletions
|
@ -365,8 +365,7 @@ object BackupRepository {
|
|||
private fun import(
|
||||
backupKey: BackupKey,
|
||||
frameReader: BackupImportReader,
|
||||
selfData: SelfData,
|
||||
importExtras: ((EventTimer) -> Unit)? = null
|
||||
selfData: SelfData
|
||||
): ImportResult {
|
||||
val eventTimer = EventTimer()
|
||||
|
||||
|
@ -444,23 +443,27 @@ object BackupRepository {
|
|||
eventTimer.emit("chatItem")
|
||||
}
|
||||
|
||||
importExtras?.invoke(eventTimer)
|
||||
|
||||
importState.chatIdToLocalThreadId.values.forEach {
|
||||
SignalDatabase.threads.update(it, unarchive = false, allowDeletion = false)
|
||||
}
|
||||
}
|
||||
|
||||
SignalDatabase.groups.getGroups().use { groups ->
|
||||
while (groups.hasNext()) {
|
||||
val group = groups.next()
|
||||
if (group.id.isV2) {
|
||||
AppDependencies.jobManager.add(RequestGroupV2InfoJob(group.id as GroupId.V2))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Log.d(TAG, "import() ${eventTimer.stop().summary}")
|
||||
|
||||
val groupJobs = SignalDatabase.groups.getGroups().use { groups ->
|
||||
groups
|
||||
.asSequence()
|
||||
.mapNotNull { group ->
|
||||
if (group.id.isV2) {
|
||||
RequestGroupV2InfoJob(group.id as GroupId.V2)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
}
|
||||
AppDependencies.jobManager.addAll(groupJobs)
|
||||
|
||||
return ImportResult.Success(backupTime = header.backupTimeMs)
|
||||
}
|
||||
|
||||
|
|
|
@ -63,6 +63,10 @@ class ArchiveFileSystem private constructor(private val context: Context, root:
|
|||
fun fromFile(context: Context, backupDirectory: File): ArchiveFileSystem {
|
||||
return ArchiveFileSystem(context, DocumentFile.fromFile(backupDirectory))
|
||||
}
|
||||
|
||||
fun openInputStream(context: Context, uri: Uri): InputStream? {
|
||||
return context.contentResolver.openInputStream(uri)
|
||||
}
|
||||
}
|
||||
|
||||
private val signalBackups: DocumentFile
|
||||
|
@ -284,29 +288,22 @@ class FilesFileSystem(private val context: Context, private val root: DocumentFi
|
|||
* undefined and should be avoided.
|
||||
*/
|
||||
fun fileOutputStream(mediaName: MediaName): OutputStream? {
|
||||
val subFileDirectoryName = mediaName.name.substring(0..1)
|
||||
val subFileDirectory = subFolders[subFileDirectoryName]!!
|
||||
val subFileDirectory = subFileDirectoryFor(mediaName)
|
||||
val file = subFileDirectory.createFile("application/octet-stream", mediaName.name)
|
||||
return file?.outputStream(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a [file], open and return an [InputStream].
|
||||
*/
|
||||
fun fileInputStream(file: DocumentFileInfo): InputStream? {
|
||||
return file.documentFile.inputStream(context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a file for the given [mediaName] if it exists.
|
||||
*
|
||||
* @return true if deleted, false if not, null if not found
|
||||
*/
|
||||
fun delete(mediaName: MediaName): Boolean? {
|
||||
val subFileDirectoryName = mediaName.name.substring(0..1)
|
||||
val subFileDirectory = subFolders[subFileDirectoryName]!!
|
||||
return subFileDirectoryFor(mediaName).delete(context, mediaName.name)
|
||||
}
|
||||
|
||||
return subFileDirectory.delete(context, mediaName.name)
|
||||
private fun subFileDirectoryFor(mediaName: MediaName): DocumentFile {
|
||||
return subFolders[mediaName.name.substring(0..1)]!!
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,13 +9,14 @@ import androidx.annotation.DrawableRes
|
|||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.LinearProgressIndicator
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
|
@ -45,11 +46,13 @@ private const val NONE = -1
|
|||
@Composable
|
||||
fun BackupStatus(
|
||||
data: BackupStatusData,
|
||||
onActionClick: () -> Unit = {}
|
||||
onActionClick: () -> Unit = {},
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(contentPadding)
|
||||
.border(1.dp, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.38f), shape = RoundedCornerShape(12.dp))
|
||||
.fillMaxWidth()
|
||||
.padding(14.dp)
|
||||
|
@ -71,7 +74,8 @@ fun BackupStatus(
|
|||
) {
|
||||
Text(
|
||||
text = data.title,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface
|
||||
)
|
||||
|
||||
if (data.progress >= 0f) {
|
||||
|
@ -108,17 +112,19 @@ fun BackupStatus(
|
|||
@Composable
|
||||
fun BackupStatusPreview() {
|
||||
Previews.Preview {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Column {
|
||||
BackupStatus(
|
||||
data = BackupStatusData.CouldNotCompleteBackup
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatus(
|
||||
data = BackupStatusData.NotEnoughFreeSpace("12 GB")
|
||||
)
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
BackupStatus(
|
||||
data = BackupStatusData.RestoringMedia(50, 100)
|
||||
)
|
||||
|
@ -201,7 +207,7 @@ sealed interface BackupStatusData {
|
|||
)
|
||||
|
||||
override val statusRes: Int = when (status) {
|
||||
Status.NONE -> R.string.default_error_msg
|
||||
Status.NONE -> NONE
|
||||
Status.LOW_BATTERY -> R.string.default_error_msg
|
||||
Status.WAITING_FOR_INTERNET -> R.string.default_error_msg
|
||||
Status.WAITING_FOR_WIFI -> R.string.default_error_msg
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.thoughtcrime.securesms.banner
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
|
@ -44,5 +45,5 @@ abstract class Banner {
|
|||
* @see [org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner]
|
||||
*/
|
||||
@Composable
|
||||
abstract fun DisplayBanner()
|
||||
abstract fun DisplayBanner(contentPadding: PaddingValues)
|
||||
}
|
||||
|
|
|
@ -6,11 +6,14 @@
|
|||
package org.thoughtcrime.securesms.banner
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.ViewCompositionStrategy
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.logging.Log
|
||||
|
||||
/**
|
||||
|
@ -47,8 +50,10 @@ class BannerManager @JvmOverloads constructor(
|
|||
|
||||
val bannerToDisplay = state.value.firstOrNull()
|
||||
if (bannerToDisplay != null) {
|
||||
Box {
|
||||
bannerToDisplay.DisplayBanner()
|
||||
SignalTheme {
|
||||
Box {
|
||||
bannerToDisplay.DisplayBanner(PaddingValues(horizontal = 12.dp, vertical = 8.dp))
|
||||
}
|
||||
}
|
||||
|
||||
onNewBannerShownListener()
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -21,7 +22,7 @@ class BubbleOptOutBanner(inBubble: Boolean, private val actionListener: (Boolean
|
|||
override val enabled: Boolean = inBubble && !SignalStore.tooltips.hasSeenBubbleOptOutTooltip() && Build.VERSION.SDK_INT > 29
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
DefaultBanner(
|
||||
title = null,
|
||||
body = stringResource(id = R.string.BubbleOptOutTooltip__description),
|
||||
|
@ -32,7 +33,8 @@ class BubbleOptOutBanner(inBubble: Boolean, private val actionListener: (Boolean
|
|||
Action(R.string.BubbleOptOutTooltip__not_now) {
|
||||
actionListener(false)
|
||||
}
|
||||
)
|
||||
),
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.fragment.app.FragmentManager
|
||||
|
@ -24,7 +25,7 @@ class CdsPermanentErrorBanner(private val fragmentManager: FragmentManager) : Ba
|
|||
override val enabled: Boolean = SignalStore.misc.isCdsBlocked && timeUntilUnblock >= PERMANENT_TIME_CUTOFF
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
DefaultBanner(
|
||||
title = null,
|
||||
body = stringResource(id = R.string.reminder_cds_permanent_error_body),
|
||||
|
@ -33,7 +34,8 @@ class CdsPermanentErrorBanner(private val fragmentManager: FragmentManager) : Ba
|
|||
Action(R.string.reminder_cds_permanent_error_learn_more) {
|
||||
CdsPermanentErrorBottomSheet.show(fragmentManager)
|
||||
}
|
||||
)
|
||||
),
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.fragment.app.FragmentManager
|
||||
|
@ -23,7 +24,7 @@ class CdsTemporaryErrorBanner(private val fragmentManager: FragmentManager) : Ba
|
|||
override val enabled: Boolean = SignalStore.misc.isCdsBlocked && timeUntilUnblock < CdsPermanentErrorBanner.PERMANENT_TIME_CUTOFF
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
DefaultBanner(
|
||||
title = null,
|
||||
body = stringResource(id = R.string.reminder_cds_warning_body),
|
||||
|
@ -32,7 +33,8 @@ class CdsTemporaryErrorBanner(private val fragmentManager: FragmentManager) : Ba
|
|||
Action(R.string.reminder_cds_warning_learn_more) {
|
||||
CdsTemporaryErrorBottomSheet.show(fragmentManager)
|
||||
}
|
||||
)
|
||||
),
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.banner.banners
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -25,23 +26,24 @@ class DozeBanner(private val context: Context, val dismissed: Boolean, private v
|
|||
Build.VERSION.SDK_INT >= 23 && !SignalStore.account.fcmEnabled && !TextSecurePreferences.hasPromptedOptimizeDoze(context) && !ServiceUtil.getPowerManager(context).isIgnoringBatteryOptimizations(context.packageName)
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
if (Build.VERSION.SDK_INT < 23) {
|
||||
throw IllegalStateException("Showing a Doze banner for an OS prior to Android 6.0")
|
||||
}
|
||||
DefaultBanner(
|
||||
title = stringResource(id = R.string.DozeReminder_optimize_for_missing_play_services),
|
||||
body = stringResource(id = R.string.DozeReminder_this_device_does_not_support_play_services_tap_to_disable_system_battery),
|
||||
onDismissListener = {
|
||||
TextSecurePreferences.setPromptedOptimizeDoze(context, true)
|
||||
onDismiss()
|
||||
},
|
||||
actions = listOf(
|
||||
Action(android.R.string.ok) {
|
||||
TextSecurePreferences.setPromptedOptimizeDoze(context, true)
|
||||
PowerManagerCompat.requestIgnoreBatteryOptimizations(context)
|
||||
}
|
||||
),
|
||||
onDismissListener = {
|
||||
TextSecurePreferences.setPromptedOptimizeDoze(context, true)
|
||||
onDismiss()
|
||||
}
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -21,7 +22,7 @@ class EnclaveFailureBanner(enclaveFailed: Boolean, private val context: Context)
|
|||
override val enabled: Boolean = enclaveFailed
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
DefaultBanner(
|
||||
title = null,
|
||||
body = stringResource(id = R.string.EnclaveFailureReminder_update_signal),
|
||||
|
@ -30,7 +31,8 @@ class EnclaveFailureBanner(enclaveFailed: Boolean, private val context: Context)
|
|||
Action(R.string.ExpiredBuildReminder_update_now) {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)
|
||||
}
|
||||
)
|
||||
),
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -18,7 +19,7 @@ class GroupsV1MigrationSuggestionsBanner(private val suggestionsSize: Int, priva
|
|||
override val enabled: Boolean = suggestionsSize > 0
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
DefaultBanner(
|
||||
title = null,
|
||||
body = pluralStringResource(
|
||||
|
@ -29,7 +30,8 @@ class GroupsV1MigrationSuggestionsBanner(private val suggestionsSize: Int, priva
|
|||
actions = listOf(
|
||||
Action(R.plurals.GroupsV1MigrationSuggestionsReminder_add_members, isPluralizedLabel = true, pluralQuantity = suggestionsSize, onAddMembers),
|
||||
Action(R.string.GroupsV1MigrationSuggestionsReminder_no_thanks, onClick = onNoThanks)
|
||||
)
|
||||
),
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,18 +5,22 @@
|
|||
|
||||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.flowWithLifecycle
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.channels.BufferOverflow
|
||||
import kotlinx.coroutines.channels.awaitClose
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.buffer
|
||||
import kotlinx.coroutines.flow.callbackFlow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatus
|
||||
import org.thoughtcrime.securesms.backup.v2.ui.status.BackupStatusData
|
||||
import org.thoughtcrime.securesms.banner.Banner
|
||||
|
@ -24,68 +28,71 @@ import org.thoughtcrime.securesms.database.DatabaseObserver
|
|||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class MediaRestoreProgressBanner(private val data: MediaRestoreEvent) : Banner() {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(MediaRestoreProgressBanner::class)
|
||||
|
||||
/**
|
||||
* Create a Lifecycle-aware [Flow] of [MediaRestoreProgressBanner] that observes the database for changes in attachments and emits banners when attachments are updated.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun createLifecycleAwareFlow(lifecycleOwner: LifecycleOwner): Flow<MediaRestoreProgressBanner> {
|
||||
if (SignalStore.backup.isRestoreInProgress) {
|
||||
val observer = LifecycleObserver()
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
return observer.flow
|
||||
return if (SignalStore.backup.isRestoreInProgress) {
|
||||
restoreFlow(lifecycleOwner)
|
||||
} else {
|
||||
return flow {
|
||||
flow {
|
||||
emit(MediaRestoreProgressBanner(MediaRestoreEvent(0L, 0L)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a flow that listens for all attachment changes in the db and emits a new banner at most
|
||||
* once every 1 second.
|
||||
*/
|
||||
private fun restoreFlow(lifecycleOwner: LifecycleOwner): Flow<MediaRestoreProgressBanner> {
|
||||
val flow = callbackFlow {
|
||||
val queryObserver = DatabaseObserver.Observer {
|
||||
trySend(Unit)
|
||||
}
|
||||
|
||||
queryObserver.onChanged()
|
||||
AppDependencies.databaseObserver.registerAttachmentUpdatedObserver(queryObserver)
|
||||
|
||||
awaitClose {
|
||||
AppDependencies.databaseObserver.unregisterObserver(queryObserver)
|
||||
}
|
||||
}
|
||||
|
||||
return flow
|
||||
.flowWithLifecycle(lifecycleOwner.lifecycle)
|
||||
.buffer(1, BufferOverflow.DROP_OLDEST)
|
||||
.onEach { delay(1.seconds) }
|
||||
.map { MediaRestoreProgressBanner(loadData()) }
|
||||
.flowOn(Dispatchers.IO)
|
||||
}
|
||||
|
||||
private suspend fun loadData() = withContext(Dispatchers.IO) {
|
||||
// TODO [backups]: define and query data for interrupted/paused restores
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
val completedBytes = totalRestoreSize - remainingAttachmentSize
|
||||
|
||||
if (remainingAttachmentSize == 0L) {
|
||||
SignalStore.backup.totalRestorableAttachmentSize = 0
|
||||
}
|
||||
|
||||
MediaRestoreEvent(completedBytes, totalRestoreSize)
|
||||
}
|
||||
}
|
||||
|
||||
override var enabled: Boolean = data.totalBytes > 0L && data.totalBytes != data.completedBytes
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
BackupStatus(data = BackupStatusData.RestoringMedia(data.completedBytes, data.totalBytes))
|
||||
}
|
||||
|
||||
data class MediaRestoreEvent(val completedBytes: Long, val totalBytes: Long)
|
||||
|
||||
private class LifecycleObserver : DefaultLifecycleObserver {
|
||||
private var attachmentObserver: DatabaseObserver.Observer? = null
|
||||
private val _mutableSharedFlow = MutableSharedFlow<MediaRestoreEvent>(replay = 1)
|
||||
|
||||
val flow = _mutableSharedFlow.map { MediaRestoreProgressBanner(it) }
|
||||
|
||||
override fun onStart(owner: LifecycleOwner) {
|
||||
val queryObserver = DatabaseObserver.Observer {
|
||||
owner.lifecycleScope.launch {
|
||||
_mutableSharedFlow.emit(loadData())
|
||||
}
|
||||
}
|
||||
|
||||
attachmentObserver = queryObserver
|
||||
queryObserver.onChanged()
|
||||
AppDependencies.databaseObserver.registerAttachmentObserver(queryObserver)
|
||||
}
|
||||
|
||||
override fun onStop(owner: LifecycleOwner) {
|
||||
attachmentObserver?.let {
|
||||
AppDependencies.databaseObserver.unregisterObserver(it)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadData() = withContext(Dispatchers.IO) {
|
||||
// TODO [backups]: define and query data for interrupted/paused restores
|
||||
val totalRestoreSize = SignalStore.backup.totalRestorableAttachmentSize
|
||||
val remainingAttachmentSize = SignalDatabase.attachments.getTotalRestorableAttachmentSize()
|
||||
val completedBytes = totalRestoreSize - remainingAttachmentSize
|
||||
MediaRestoreEvent(completedBytes, totalRestoreSize)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
|
@ -34,7 +35,7 @@ class OutdatedBuildBanner(val context: Context, private val daysUntilExpiry: Int
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
val bodyText = when (status) {
|
||||
ExpiryStatus.OUTDATED_ONLY -> if (daysUntilExpiry == 0) {
|
||||
stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today)
|
||||
|
@ -63,7 +64,8 @@ class OutdatedBuildBanner(val context: Context, private val daysUntilExpiry: Int
|
|||
Action(R.string.ExpiredBuildReminder_update_now) {
|
||||
PlayStoreUtil.openPlayStoreOrOurApkDownloadPage(context)
|
||||
}
|
||||
)
|
||||
),
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
|
||||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -17,7 +18,7 @@ import org.thoughtcrime.securesms.banner.ui.compose.DefaultBanner
|
|||
class PendingGroupJoinRequestsBanner(override val enabled: Boolean, private val suggestionsSize: Int, private val onViewClicked: () -> Unit, private val onDismissListener: (() -> Unit)?) : Banner() {
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
DefaultBanner(
|
||||
title = null,
|
||||
body = pluralStringResource(
|
||||
|
@ -25,10 +26,11 @@ class PendingGroupJoinRequestsBanner(override val enabled: Boolean, private val
|
|||
count = suggestionsSize,
|
||||
suggestionsSize
|
||||
),
|
||||
onDismissListener = onDismissListener,
|
||||
actions = listOf(
|
||||
Action(R.string.PendingGroupJoinRequestsReminder_view, onClick = onViewClicked)
|
||||
),
|
||||
onDismissListener = onDismissListener
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -25,11 +26,12 @@ class ServiceOutageBanner(outageInProgress: Boolean) : Banner() {
|
|||
override val enabled = outageInProgress
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
DefaultBanner(
|
||||
title = null,
|
||||
body = stringResource(id = R.string.reminder_header_service_outage_text),
|
||||
importance = Importance.ERROR
|
||||
importance = Importance.ERROR,
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -29,7 +30,7 @@ class UnauthorizedBanner(val context: Context) : Banner() {
|
|||
override val enabled = TextSecurePreferences.isUnauthorizedReceived(context) || !SignalStore.account.isRegistered
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
DefaultBanner(
|
||||
title = null,
|
||||
body = stringResource(id = R.string.UnauthorizedReminder_this_is_likely_because_you_registered_your_phone_number_with_Signal_on_a_different_device),
|
||||
|
@ -39,7 +40,8 @@ class UnauthorizedBanner(val context: Context) : Banner() {
|
|||
val registrationIntent = RegistrationActivity.newIntentForReRegistration(context)
|
||||
context.startActivity(registrationIntent)
|
||||
}
|
||||
)
|
||||
),
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
package org.thoughtcrime.securesms.banner.banners
|
||||
|
||||
import android.content.Context
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
@ -27,7 +28,7 @@ class UsernameOutOfSyncBanner(private val context: Context, private val username
|
|||
}
|
||||
|
||||
@Composable
|
||||
override fun DisplayBanner() {
|
||||
override fun DisplayBanner(contentPadding: PaddingValues) {
|
||||
DefaultBanner(
|
||||
title = null,
|
||||
body = if (usernameSyncState == UsernameSyncState.USERNAME_AND_LINK_CORRUPTED) {
|
||||
|
@ -40,7 +41,8 @@ class UsernameOutOfSyncBanner(private val context: Context, private val username
|
|||
Action(R.string.UsernameOutOfSyncReminder__fix_now) {
|
||||
onActionClick(usernameSyncState == UsernameSyncState.USERNAME_AND_LINK_CORRUPTED)
|
||||
}
|
||||
)
|
||||
),
|
||||
paddingValues = contentPadding
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import androidx.compose.foundation.border
|
|||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
|
@ -32,7 +33,6 @@ import androidx.compose.ui.res.stringResource
|
|||
import androidx.compose.ui.unit.dp
|
||||
import org.signal.core.ui.Previews
|
||||
import org.signal.core.ui.SignalPreview
|
||||
import org.signal.core.ui.theme.SignalTheme
|
||||
import org.signal.core.util.isNotNullOrBlank
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
|
@ -50,115 +50,116 @@ fun DefaultBanner(
|
|||
actions: List<Action> = emptyList(),
|
||||
showProgress: Boolean = false,
|
||||
progressText: String = "",
|
||||
progressPercent: Int = -1
|
||||
progressPercent: Int = -1,
|
||||
paddingValues: PaddingValues
|
||||
) {
|
||||
SignalTheme {
|
||||
Box(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.background(
|
||||
color = when (importance) {
|
||||
Importance.NORMAL -> MaterialTheme.colorScheme.surface
|
||||
Importance.ERROR -> colorResource(id = R.color.reminder_background)
|
||||
}
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = colorResource(id = R.color.signal_colorOutline_38),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp)
|
||||
.background(
|
||||
color = when (importance) {
|
||||
Importance.NORMAL -> MaterialTheme.colorScheme.surface
|
||||
Importance.ERROR -> colorResource(id = R.color.reminder_background)
|
||||
}
|
||||
)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = colorResource(id = R.color.signal_colorOutline_38),
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
)
|
||||
.defaultMinSize(minHeight = 74.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = 74.dp)
|
||||
) {
|
||||
Column {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp, top = 16.dp)
|
||||
) {
|
||||
if (title.isNotNullOrBlank()) {
|
||||
Text(
|
||||
text = title,
|
||||
color = when (importance) {
|
||||
Importance.NORMAL -> MaterialTheme.colorScheme.onSurface
|
||||
Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
Column {
|
||||
Row(modifier = Modifier.fillMaxWidth()) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(start = 16.dp, top = 16.dp)
|
||||
) {
|
||||
if (title.isNotNullOrBlank()) {
|
||||
Text(
|
||||
text = title,
|
||||
color = when (importance) {
|
||||
Importance.NORMAL -> MaterialTheme.colorScheme.onSurface
|
||||
Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = body,
|
||||
color = when (importance) {
|
||||
Importance.NORMAL -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
|
||||
if (showProgress) {
|
||||
if (progressPercent >= 0) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progressPercent / 100f },
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 12.dp)
|
||||
.fillMaxWidth()
|
||||
)
|
||||
} else {
|
||||
LinearProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
|
||||
Text(
|
||||
text = body,
|
||||
text = progressText,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = when (importance) {
|
||||
Importance.NORMAL -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface)
|
||||
},
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
}
|
||||
)
|
||||
|
||||
if (showProgress) {
|
||||
if (progressPercent >= 0) {
|
||||
LinearProgressIndicator(
|
||||
progress = { progressPercent / 100f },
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
} else {
|
||||
LinearProgressIndicator(
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
trackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||
modifier = Modifier.padding(vertical = 12.dp)
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = progressText,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = when (importance) {
|
||||
Importance.NORMAL -> MaterialTheme.colorScheme.onSurfaceVariant
|
||||
Importance.ERROR -> colorResource(id = R.color.signal_light_colorOnSurface)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = Modifier.size(48.dp)) {
|
||||
if (onDismissListener != null) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onHideListener?.invoke()
|
||||
onDismissListener()
|
||||
},
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_x_24),
|
||||
contentDescription = stringResource(id = R.string.InviteActivity_cancel)
|
||||
)
|
||||
}
|
||||
Box(modifier = Modifier.size(48.dp)) {
|
||||
if (onDismissListener != null) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
onHideListener?.invoke()
|
||||
onDismissListener()
|
||||
},
|
||||
modifier = Modifier.size(48.dp)
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(id = R.drawable.symbol_x_24),
|
||||
contentDescription = stringResource(id = R.string.InviteActivity_cancel)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
for (action in actions) {
|
||||
TextButton(onClick = action.onClick) {
|
||||
Text(
|
||||
text = if (!action.isPluralizedLabel) {
|
||||
stringResource(id = action.label)
|
||||
} else {
|
||||
pluralStringResource(id = action.label, count = action.pluralQuantity)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.End,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(end = 8.dp)
|
||||
) {
|
||||
for (action in actions) {
|
||||
TextButton(onClick = action.onClick) {
|
||||
Text(
|
||||
text = if (!action.isPluralizedLabel) {
|
||||
stringResource(id = action.label)
|
||||
} else {
|
||||
pluralStringResource(id = action.label, count = action.pluralQuantity)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -183,7 +184,8 @@ private fun BubblesOptOutPreview() {
|
|||
actions = listOf(
|
||||
Action(R.string.BubbleOptOutTooltip__turn_off) {},
|
||||
Action(R.string.BubbleOptOutTooltip__not_now) {}
|
||||
)
|
||||
),
|
||||
paddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -196,9 +198,10 @@ private fun ForcedUpgradePreview() {
|
|||
title = null,
|
||||
body = stringResource(id = R.string.OutdatedBuildReminder_your_version_of_signal_will_expire_today),
|
||||
importance = Importance.ERROR,
|
||||
actions = listOf(Action(R.string.ExpiredBuildReminder_update_now) {}),
|
||||
onDismissListener = {},
|
||||
onHideListener = { },
|
||||
onDismissListener = {}
|
||||
actions = listOf(Action(R.string.ExpiredBuildReminder_update_now) {}),
|
||||
paddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -215,11 +218,12 @@ private fun FullyLoadedErrorPreview() {
|
|||
title = "Error",
|
||||
body = "Creating more errors.",
|
||||
importance = Importance.ERROR,
|
||||
onDismissListener = {},
|
||||
actions = actions,
|
||||
showProgress = true,
|
||||
progressText = "4 out of 10 errors created.",
|
||||
progressPercent = 40,
|
||||
onDismissListener = {}
|
||||
paddingValues = PaddingValues(horizontal = 12.dp, vertical = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,6 +74,7 @@ import org.thoughtcrime.securesms.components.settings.app.internal.backup.Intern
|
|||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.BackupUploadState
|
||||
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
|
||||
import org.thoughtcrime.securesms.compose.ComposeFragment
|
||||
import org.thoughtcrime.securesms.jobs.LocalBackupJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
|
||||
class InternalBackupPlaygroundFragment : ComposeFragment() {
|
||||
|
@ -81,7 +82,6 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
|||
private val viewModel: InternalBackupPlaygroundViewModel by viewModels()
|
||||
private lateinit var exportFileLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var importFileLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var importDirectoryLauncher: ActivityResultLauncher<Intent>
|
||||
private lateinit var validateFileLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -108,12 +108,6 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
importDirectoryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
viewModel.import(result.data!!.data!!)
|
||||
}
|
||||
}
|
||||
|
||||
validateFileLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
|
@ -141,6 +135,7 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
|||
Screen(
|
||||
state = state,
|
||||
onExportClicked = { viewModel.export() },
|
||||
onExportDirectoryClicked = { LocalBackupJob.enqueueArchive() },
|
||||
onImportMemoryClicked = { viewModel.import() },
|
||||
onImportFileClicked = {
|
||||
val intent = Intent().apply {
|
||||
|
@ -152,8 +147,7 @@ class InternalBackupPlaygroundFragment : ComposeFragment() {
|
|||
importFileLauncher.launch(intent)
|
||||
},
|
||||
onImportDirectoryClicked = {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
importDirectoryLauncher.launch(intent)
|
||||
viewModel.import(SignalStore.settings.signalBackupDirectory!!)
|
||||
},
|
||||
onPlaintextClicked = { viewModel.onPlaintextToggled() },
|
||||
onSaveToDiskClicked = {
|
||||
|
@ -260,6 +254,7 @@ fun Tabs(
|
|||
fun Screen(
|
||||
state: ScreenState,
|
||||
onExportClicked: () -> Unit = {},
|
||||
onExportDirectoryClicked: () -> Unit = {},
|
||||
onImportMemoryClicked: () -> Unit = {},
|
||||
onImportFileClicked: () -> Unit = {},
|
||||
onImportDirectoryClicked: () -> Unit = {},
|
||||
|
@ -302,6 +297,13 @@ fun Screen(
|
|||
Text("Export")
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onExportDirectoryClicked,
|
||||
enabled = !state.backupState.inProgress && state.canReadWriteBackupDirectory
|
||||
) {
|
||||
Text("Export to backup directory")
|
||||
}
|
||||
|
||||
Buttons.LargePrimary(
|
||||
onClick = onTriggerBackupJobClicked,
|
||||
enabled = !state.backupState.inProgress
|
||||
|
@ -323,9 +325,10 @@ fun Screen(
|
|||
Text("Import from file")
|
||||
}
|
||||
Buttons.LargeTonal(
|
||||
onClick = onImportDirectoryClicked
|
||||
onClick = onImportDirectoryClicked,
|
||||
enabled = state.canReadWriteBackupDirectory
|
||||
) {
|
||||
Text("Import from directory")
|
||||
Text("Import from backup directory")
|
||||
}
|
||||
|
||||
Buttons.LargeTonal(
|
||||
|
|
|
@ -9,6 +9,7 @@ import android.net.Uri
|
|||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.State
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Completable
|
||||
|
@ -36,6 +37,7 @@ import org.thoughtcrime.securesms.jobs.AttachmentUploadJob
|
|||
import org.thoughtcrime.securesms.jobs.BackupMessagesJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreJob
|
||||
import org.thoughtcrime.securesms.jobs.BackupRestoreMediaJob
|
||||
import org.thoughtcrime.securesms.jobs.RestoreLocalAttachmentJob
|
||||
import org.thoughtcrime.securesms.jobs.SyncArchivedMediaJob
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.IncomingMessage
|
||||
|
@ -53,7 +55,17 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
|
||||
val disposables = CompositeDisposable()
|
||||
|
||||
private val _state: MutableState<ScreenState> = mutableStateOf(ScreenState(backupState = BackupState.NONE, uploadState = BackupUploadState.NONE, plaintext = false))
|
||||
private val _state: MutableState<ScreenState> = mutableStateOf(
|
||||
ScreenState(
|
||||
backupState = BackupState.NONE,
|
||||
uploadState = BackupUploadState.NONE,
|
||||
plaintext = false,
|
||||
canReadWriteBackupDirectory = SignalStore.settings.signalBackupDirectory?.let {
|
||||
val file = DocumentFile.fromTreeUri(AppDependencies.application, it)
|
||||
file != null && file.canWrite() && file.canRead()
|
||||
} ?: false
|
||||
)
|
||||
)
|
||||
val state: State<ScreenState> = _state
|
||||
|
||||
private val _mediaState: MutableState<MediaState> = mutableStateOf(MediaState())
|
||||
|
@ -129,6 +141,9 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
val snapshotFileSystem = SnapshotFileSystem(AppDependencies.application, snapshotInfo.file)
|
||||
|
||||
LocalArchiver.import(snapshotFileSystem, selfData)
|
||||
|
||||
val mediaNameToFileInfo = archiveFileSystem.filesFileSystem.allFiles()
|
||||
RestoreLocalAttachmentJob.enqueueRestoreLocalAttachmentsJobs(mediaNameToFileInfo)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -382,7 +397,8 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
|
|||
val backupState: BackupState = BackupState.NONE,
|
||||
val uploadState: BackupUploadState = BackupUploadState.NONE,
|
||||
val remoteBackupState: RemoteBackupState = RemoteBackupState.Unknown,
|
||||
val plaintext: Boolean
|
||||
val plaintext: Boolean,
|
||||
val canReadWriteBackupDirectory: Boolean = false
|
||||
)
|
||||
|
||||
enum class BackupState(val inProgress: Boolean = false) {
|
||||
|
|
|
@ -89,7 +89,7 @@ public class VoiceNotePlaybackService extends MediaSessionService {
|
|||
|
||||
setMediaNotificationProvider(new VoiceNoteMediaNotificationProvider(this));
|
||||
setListener(new MediaSessionServiceListener());
|
||||
AppDependencies.getDatabaseObserver().registerAttachmentObserver(attachmentDeletionObserver);
|
||||
AppDependencies.getDatabaseObserver().registerAttachmentDeletedObserver(attachmentDeletionObserver);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -53,6 +53,7 @@ import org.signal.core.util.requireBlob
|
|||
import org.signal.core.util.requireBoolean
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireLongOrNull
|
||||
import org.signal.core.util.requireNonNullBlob
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireObject
|
||||
|
@ -495,7 +496,7 @@ class AttachmentTable(
|
|||
return readableDatabase
|
||||
.select(*PROJECTION)
|
||||
.from(TABLE_NAME)
|
||||
.where("$TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE.toString())
|
||||
.where("$TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE)
|
||||
.limit(batchSize)
|
||||
.orderBy("$ID DESC")
|
||||
.run()
|
||||
|
@ -508,7 +509,7 @@ class AttachmentTable(
|
|||
return readableDatabase
|
||||
.select(*PROJECTION)
|
||||
.from(TABLE_NAME)
|
||||
.where("$REMOTE_KEY IS NOT NULL AND $REMOTE_DIGEST IS NOT NULL AND $TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE.toString())
|
||||
.where("$TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE)
|
||||
.limit(batchSize)
|
||||
.orderBy("$ID DESC")
|
||||
.run()
|
||||
|
@ -517,17 +518,17 @@ class AttachmentTable(
|
|||
attachmentId = AttachmentId(it.requireLong(ID)),
|
||||
mmsId = it.requireLong(MESSAGE_ID),
|
||||
size = it.requireLong(DATA_SIZE),
|
||||
remoteDigest = it.requireBlob(REMOTE_DIGEST)!!,
|
||||
remoteKey = it.requireBlob(REMOTE_KEY)!!
|
||||
remoteDigest = it.requireBlob(REMOTE_DIGEST),
|
||||
remoteKey = it.requireBlob(REMOTE_KEY)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getTotalRestorableAttachmentSize(): Long {
|
||||
fun getRemainingRestorableAttachmentSize(): Long {
|
||||
return readableDatabase
|
||||
.select("SUM($DATA_SIZE)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE.toString())
|
||||
.where("$TRANSFER_STATE = ? OR $TRANSFER_STATE = ?", TRANSFER_NEEDS_RESTORE, TRANSFER_RESTORE_IN_PROGRESS)
|
||||
.run()
|
||||
.readToSingleLong()
|
||||
}
|
||||
|
@ -628,7 +629,7 @@ class AttachmentTable(
|
|||
.where("$MESSAGE_ID = ?", mmsId)
|
||||
.run()
|
||||
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
|
||||
|
||||
deleteCount > 0
|
||||
}
|
||||
|
@ -692,7 +693,7 @@ class AttachmentTable(
|
|||
.where("$MESSAGE_ID = ?", messageId)
|
||||
.run()
|
||||
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
|
||||
|
||||
val threadId = messages.getThreadIdForMessage(messageId)
|
||||
if (threadId > 0) {
|
||||
|
@ -729,7 +730,7 @@ class AttachmentTable(
|
|||
.run()
|
||||
|
||||
deleteDataFileIfPossible(data, contentType, id)
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -849,7 +850,7 @@ class AttachmentTable(
|
|||
|
||||
FileUtils.deleteDirectoryContents(context.getDir(DIRECTORY, Context.MODE_PRIVATE))
|
||||
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
|
||||
}
|
||||
|
||||
fun setTransferState(messageId: Long, attachmentId: AttachmentId, transferState: Int) {
|
||||
|
@ -874,7 +875,6 @@ class AttachmentTable(
|
|||
notifyConversationListeners(threadId)
|
||||
}
|
||||
|
||||
@Throws(MmsException::class)
|
||||
fun setTransferProgressFailed(attachmentId: AttachmentId, mmsId: Long) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
|
@ -885,7 +885,6 @@ class AttachmentTable(
|
|||
notifyConversationListeners(messages.getThreadIdForMessage(mmsId))
|
||||
}
|
||||
|
||||
@Throws(MmsException::class)
|
||||
fun setThumbnailRestoreProgressFailed(attachmentId: AttachmentId, mmsId: Long) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
|
@ -896,7 +895,6 @@ class AttachmentTable(
|
|||
notifyConversationListeners(messages.getThreadIdForMessage(mmsId))
|
||||
}
|
||||
|
||||
@Throws(MmsException::class)
|
||||
fun setTransferProgressPermanentFailure(attachmentId: AttachmentId, mmsId: Long) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
|
@ -907,6 +905,57 @@ class AttachmentTable(
|
|||
notifyConversationListeners(messages.getThreadIdForMessage(mmsId))
|
||||
}
|
||||
|
||||
fun setRestoreInProgressTransferState(restorableAttachments: List<LocalRestorableAttachment>) {
|
||||
setRestoreTransferState(
|
||||
restorableAttachments = restorableAttachments,
|
||||
prefix = "$TRANSFER_STATE = $TRANSFER_NEEDS_RESTORE",
|
||||
state = TRANSFER_RESTORE_IN_PROGRESS
|
||||
)
|
||||
}
|
||||
|
||||
fun setRestoreFailedTransferState(notRestorableAttachments: List<LocalRestorableAttachment>) {
|
||||
setRestoreTransferState(
|
||||
restorableAttachments = notRestorableAttachments,
|
||||
prefix = "$TRANSFER_STATE != $TRANSFER_PROGRESS_PERMANENT_FAILURE",
|
||||
state = TRANSFER_PROGRESS_FAILED
|
||||
)
|
||||
}
|
||||
|
||||
private fun setRestoreTransferState(restorableAttachments: List<LocalRestorableAttachment>, prefix: String, state: Int) {
|
||||
writableDatabase.withinTransaction {
|
||||
val setQueries = SqlUtil.buildCollectionQuery(
|
||||
column = ID,
|
||||
values = restorableAttachments.map { it.attachmentId.id },
|
||||
prefix = "$prefix AND"
|
||||
)
|
||||
|
||||
setQueries.forEach { query ->
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(TRANSFER_STATE to state)
|
||||
.where(query.where, query.whereArgs)
|
||||
.run()
|
||||
}
|
||||
|
||||
val threadQueries = SqlUtil.buildCollectionQuery(
|
||||
column = MessageTable.ID,
|
||||
values = restorableAttachments.map { it.mmsId }
|
||||
)
|
||||
|
||||
val threads = mutableSetOf<Long>()
|
||||
threadQueries.forEach { query ->
|
||||
threads += readableDatabase
|
||||
.select("DISTINCT ${MessageTable.THREAD_ID}")
|
||||
.from(MessageTable.TABLE_NAME)
|
||||
.where(query.where, query.whereArgs)
|
||||
.run()
|
||||
.readToList { it.requireLongOrNull(MessageTable.THREAD_ID) ?: -1 }
|
||||
}
|
||||
|
||||
notifyConversationListeners(threads)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When we find out about a new inbound attachment pointer, we insert a row for it that contains all the info we need to download it via [insertAttachmentWithData].
|
||||
* Later, we download the data for that pointer. Call this method once you have the data to associate it with the attachment. At this point, it is assumed
|
||||
|
@ -982,7 +1031,7 @@ class AttachmentTable(
|
|||
|
||||
notifyConversationListeners(threadId)
|
||||
notifyConversationListListeners()
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentUpdatedObservers()
|
||||
|
||||
if (foundDuplicate) {
|
||||
if (!fileWriteResult.file.delete()) {
|
||||
|
@ -1020,7 +1069,7 @@ class AttachmentTable(
|
|||
}
|
||||
|
||||
notifyConversationListListeners()
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentUpdatedObservers()
|
||||
|
||||
if (!transferFile.delete()) {
|
||||
Log.w(TAG, "Unable to delete transfer file.")
|
||||
|
@ -1904,7 +1953,7 @@ class AttachmentTable(
|
|||
AttachmentId(rowId)
|
||||
}
|
||||
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentUpdatedObservers()
|
||||
return attachmentId
|
||||
}
|
||||
|
||||
|
@ -1961,7 +2010,7 @@ class AttachmentTable(
|
|||
AttachmentId(rowId)
|
||||
}
|
||||
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentUpdatedObservers()
|
||||
return attachmentId
|
||||
}
|
||||
|
||||
|
@ -2101,7 +2150,7 @@ class AttachmentTable(
|
|||
}
|
||||
}
|
||||
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentUpdatedObservers()
|
||||
return attachmentId
|
||||
}
|
||||
|
||||
|
@ -2460,7 +2509,7 @@ class AttachmentTable(
|
|||
val attachmentId: AttachmentId,
|
||||
val mmsId: Long,
|
||||
val size: Long,
|
||||
val remoteDigest: ByteArray,
|
||||
val remoteKey: ByteArray
|
||||
val remoteDigest: ByteArray?,
|
||||
val remoteKey: ByteArray?
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
|
@ -24,7 +22,7 @@ import java.util.concurrent.Executor;
|
|||
|
||||
/**
|
||||
* Allows listening to database changes to varying degrees of specificity.
|
||||
*
|
||||
* <p>
|
||||
* A replacement for the observer system in {@link DatabaseTable}. We should move to this over time.
|
||||
*/
|
||||
public class DatabaseObserver {
|
||||
|
@ -46,11 +44,10 @@ public class DatabaseObserver {
|
|||
private static final String KEY_SCHEDULED_MESSAGES = "ScheduledMessages";
|
||||
private static final String KEY_CONVERSATION_DELETES = "ConversationDeletes";
|
||||
|
||||
private static final String KEY_CALL_UPDATES = "CallUpdates";
|
||||
private static final String KEY_CALL_LINK_UPDATES = "CallLinkUpdates";
|
||||
private static final String KEY_IN_APP_PAYMENTS = "InAppPayments";
|
||||
private static final String KEY_CALL_UPDATES = "CallUpdates";
|
||||
private static final String KEY_CALL_LINK_UPDATES = "CallLinkUpdates";
|
||||
private static final String KEY_IN_APP_PAYMENTS = "InAppPayments";
|
||||
|
||||
private final Application application;
|
||||
private final Executor executor;
|
||||
|
||||
private final Set<Observer> conversationListObservers;
|
||||
|
@ -63,7 +60,8 @@ public class DatabaseObserver {
|
|||
private final Set<Observer> chatColorsObservers;
|
||||
private final Set<Observer> stickerObservers;
|
||||
private final Set<Observer> stickerPackObservers;
|
||||
private final Set<Observer> attachmentObservers;
|
||||
private final Set<Observer> attachmentUpdatedObservers;
|
||||
private final Set<Observer> attachmentDeletedObservers;
|
||||
private final Set<MessageObserver> messageUpdateObservers;
|
||||
private final Map<Long, Set<MessageObserver>> messageInsertObservers;
|
||||
private final Set<Observer> notificationProfileObservers;
|
||||
|
@ -72,8 +70,7 @@ public class DatabaseObserver {
|
|||
private final Map<CallLinkRoomId, Set<Observer>> callLinkObservers;
|
||||
private final Set<InAppPaymentObserver> inAppPaymentObservers;
|
||||
|
||||
public DatabaseObserver(Application application) {
|
||||
this.application = application;
|
||||
public DatabaseObserver() {
|
||||
this.executor = new SerialExecutor(SignalExecutors.BOUNDED);
|
||||
this.conversationListObservers = new HashSet<>();
|
||||
this.conversationObservers = new HashMap<>();
|
||||
|
@ -84,7 +81,8 @@ public class DatabaseObserver {
|
|||
this.chatColorsObservers = new HashSet<>();
|
||||
this.stickerObservers = new HashSet<>();
|
||||
this.stickerPackObservers = new HashSet<>();
|
||||
this.attachmentObservers = new HashSet<>();
|
||||
this.attachmentUpdatedObservers = new HashSet<>();
|
||||
this.attachmentDeletedObservers = new HashSet<>();
|
||||
this.messageUpdateObservers = new HashSet<>();
|
||||
this.messageInsertObservers = new HashMap<>();
|
||||
this.notificationProfileObservers = new HashSet<>();
|
||||
|
@ -149,9 +147,15 @@ public class DatabaseObserver {
|
|||
});
|
||||
}
|
||||
|
||||
public void registerAttachmentObserver(@NonNull Observer listener) {
|
||||
public void registerAttachmentUpdatedObserver(@NonNull Observer listener) {
|
||||
executor.execute(() -> {
|
||||
attachmentObservers.add(listener);
|
||||
attachmentUpdatedObservers.add(listener);
|
||||
});
|
||||
}
|
||||
|
||||
public void registerAttachmentDeletedObserver(@NonNull Observer listener) {
|
||||
executor.execute(() -> {
|
||||
attachmentDeletedObservers.add(listener);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -211,7 +215,8 @@ public class DatabaseObserver {
|
|||
chatColorsObservers.remove(listener);
|
||||
stickerObservers.remove(listener);
|
||||
stickerPackObservers.remove(listener);
|
||||
attachmentObservers.remove(listener);
|
||||
attachmentUpdatedObservers.remove(listener);
|
||||
attachmentDeletedObservers.remove(listener);
|
||||
notificationProfileObservers.remove(listener);
|
||||
unregisterMapped(storyObservers, listener);
|
||||
unregisterMapped(scheduledMessageObservers, listener);
|
||||
|
@ -307,9 +312,16 @@ public class DatabaseObserver {
|
|||
});
|
||||
}
|
||||
|
||||
public void notifyAttachmentObservers() {
|
||||
public void notifyAttachmentUpdatedObservers() {
|
||||
runPostSuccessfulTransaction(KEY_ATTACHMENTS, () -> {
|
||||
notifySet(attachmentObservers);
|
||||
notifySet(attachmentUpdatedObservers);
|
||||
});
|
||||
}
|
||||
|
||||
public void notifyAttachmentDeletedObservers() {
|
||||
runPostSuccessfulTransaction(KEY_ATTACHMENTS, () -> {
|
||||
notifySet(attachmentDeletedObservers);
|
||||
notifySet(attachmentUpdatedObservers);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -71,10 +71,6 @@ public abstract class DatabaseTable {
|
|||
AppDependencies.getDatabaseObserver().notifyStickerObservers();
|
||||
}
|
||||
|
||||
protected void notifyAttachmentListeners() {
|
||||
AppDependencies.getDatabaseObserver().notifyAttachmentObservers();
|
||||
}
|
||||
|
||||
public void reset(SignalDatabase databaseHelper) {
|
||||
this.databaseHelper = databaseHelper;
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import android.database.Cursor
|
|||
import androidx.compose.runtime.Immutable
|
||||
import org.signal.core.util.requireInt
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
|
@ -159,6 +159,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
|||
)
|
||||
)"""
|
||||
)
|
||||
|
||||
private fun applyEqualityOperator(threadId: Long, query: String): String {
|
||||
return query.replace("__EQUALITY__", if (threadId == ALL_THREADS.toLong()) "!=" else "=")
|
||||
}
|
||||
|
@ -207,7 +208,7 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
|||
readableDatabase.rawQuery(UNIQUE_MEDIA_QUERY, null).use { cursor ->
|
||||
while (cursor.moveToNext()) {
|
||||
val size: Int = cursor.requireInt(AttachmentTable.DATA_SIZE)
|
||||
val type: String = cursor.requireNonNullString(AttachmentTable.CONTENT_TYPE)
|
||||
val type: String? = cursor.requireString(AttachmentTable.CONTENT_TYPE)
|
||||
|
||||
when (MediaUtil.getSlideTypeFromContentType(type)) {
|
||||
SlideType.GIF,
|
||||
|
@ -215,17 +216,21 @@ class MediaTable internal constructor(context: Context?, databaseHelper: SignalD
|
|||
SlideType.MMS -> {
|
||||
photoSize += size.toLong()
|
||||
}
|
||||
|
||||
SlideType.VIDEO -> {
|
||||
videoSize += size.toLong()
|
||||
}
|
||||
|
||||
SlideType.AUDIO -> {
|
||||
audioSize += size.toLong()
|
||||
}
|
||||
|
||||
SlideType.LONG_TEXT,
|
||||
SlideType.DOCUMENT -> {
|
||||
documentSize += size.toLong()
|
||||
}
|
||||
else -> {}
|
||||
|
||||
SlideType.VIEW_ONCE -> Unit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2115,7 +2115,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
|||
AppDependencies.databaseObserver.notifyConversationListListeners()
|
||||
|
||||
if (deletedAttachments) {
|
||||
AppDependencies.databaseObserver.notifyAttachmentObservers()
|
||||
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -361,7 +361,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
|||
MultiDeviceDeleteSyncJob.enqueueThreadDeletes(threadTrimsToSync, isFullDelete = false)
|
||||
}
|
||||
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentUpdatedObservers()
|
||||
notifyStickerPackListeners()
|
||||
OptimizeMessageSearchIndexJob.enqueue()
|
||||
}
|
||||
|
@ -395,7 +395,7 @@ class ThreadTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
|||
MultiDeviceDeleteSyncJob.enqueueThreadDeletes(listOf(threadTrimToSync!!), isFullDelete = false)
|
||||
}
|
||||
|
||||
notifyAttachmentListeners()
|
||||
AppDependencies.databaseObserver.notifyAttachmentDeletedObservers()
|
||||
notifyStickerPackListeners()
|
||||
OptimizeMessageSearchIndexJob.enqueue()
|
||||
}
|
||||
|
|
|
@ -71,7 +71,7 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader<GroupedThrea
|
|||
|
||||
PopulatedGroupedThreadMedia mediaGrouping = new PopulatedGroupedThreadMedia(groupingMethod);
|
||||
|
||||
AppDependencies.getDatabaseObserver().registerAttachmentObserver(observer);
|
||||
AppDependencies.getDatabaseObserver().registerAttachmentUpdatedObserver(observer);
|
||||
|
||||
try (Cursor cursor = ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
|
|
|
@ -255,7 +255,7 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
|||
|
||||
@Override
|
||||
public @NonNull DatabaseObserver provideDatabaseObserver() {
|
||||
return new DatabaseObserver(context);
|
||||
return new DatabaseObserver();
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
|
|
|
@ -175,11 +175,7 @@ public final class AttachmentCompressionJob extends BaseJob {
|
|||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
database.setTransferProgressFailed(attachmentId, databaseAttachment.mmsId);
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "Error marking attachment as failed upon failed compression.", e);
|
||||
}
|
||||
database.setTransferProgressFailed(attachmentId, databaseAttachment.mmsId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -425,19 +425,11 @@ class AttachmentDownloadJob private constructor(
|
|||
}
|
||||
|
||||
private fun markFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
try {
|
||||
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||
} catch (e: MmsException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||
}
|
||||
|
||||
private fun markPermanentlyFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
try {
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachmentId, messageId)
|
||||
} catch (e: MmsException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachmentId, messageId)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
|
@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
|||
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec
|
||||
import org.thoughtcrime.securesms.jobs.protos.AttachmentUploadJobData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.service.AttachmentProgressService
|
||||
|
@ -235,11 +234,7 @@ class AttachmentUploadJob private constructor(
|
|||
return
|
||||
}
|
||||
|
||||
try {
|
||||
database.setTransferProgressFailed(attachmentId, databaseAttachment.mmsId)
|
||||
} catch (e: MmsException) {
|
||||
Log.w(TAG, "Error marking attachment as failed upon failed/canceled upload.", e)
|
||||
}
|
||||
database.setTransferProgressFailed(attachmentId, databaseAttachment.mmsId)
|
||||
}
|
||||
|
||||
override fun onShouldRetry(exception: Exception): Boolean {
|
||||
|
|
|
@ -48,7 +48,7 @@ class BackupRestoreMediaJob private constructor(parameters: Parameters) : BaseJo
|
|||
throw NotPushRegisteredException()
|
||||
}
|
||||
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getTotalRestorableAttachmentSize()
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
val jobManager = AppDependencies.jobManager
|
||||
val batchSize = 100
|
||||
val restoreTime = System.currentTimeMillis()
|
||||
|
|
|
@ -215,6 +215,7 @@ public final class JobManagerFactories {
|
|||
put(RequestGroupV2InfoJob.KEY, new RequestGroupV2InfoJob.Factory());
|
||||
put(RestoreAttachmentJob.KEY, new RestoreAttachmentJob.Factory());
|
||||
put(RestoreAttachmentThumbnailJob.KEY, new RestoreAttachmentThumbnailJob.Factory());
|
||||
put(RestoreLocalAttachmentJob.KEY, new RestoreLocalAttachmentJob.Factory());
|
||||
put(RetrieveProfileAvatarJob.KEY, new RetrieveProfileAvatarJob.Factory());
|
||||
put(RetrieveProfileJob.KEY, new RetrieveProfileJob.Factory());
|
||||
put(RetrieveRemoteAnnouncementsJob.KEY, new RetrieveRemoteAnnouncementsJob.Factory());
|
||||
|
|
|
@ -58,7 +58,7 @@ class RestoreAttachmentJob private constructor(
|
|||
|
||||
companion object {
|
||||
const val KEY = "RestoreAttachmentJob"
|
||||
val TAG = Log.tag(AttachmentDownloadJob::class.java)
|
||||
val TAG = Log.tag(RestoreAttachmentJob::class.java)
|
||||
|
||||
private const val KEY_MESSAGE_ID = "message_id"
|
||||
private const val KEY_ATTACHMENT_ID = "part_row_id"
|
||||
|
@ -153,7 +153,7 @@ class RestoreAttachmentJob private constructor(
|
|||
AppDependencies.messageNotifier.updateNotification(context, forConversation(0))
|
||||
}
|
||||
|
||||
if (SignalDatabase.attachments.getTotalRestorableAttachmentSize() == 0L) {
|
||||
if (SignalDatabase.attachments.getRemainingRestorableAttachmentSize() == 0L) {
|
||||
SignalStore.backup.totalRestorableAttachmentSize = 0L
|
||||
}
|
||||
}
|
||||
|
@ -483,11 +483,7 @@ class RestoreAttachmentJob private constructor(
|
|||
}
|
||||
|
||||
private fun markPermanentlyFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
try {
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachmentId, messageId)
|
||||
} catch (e: MmsException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachmentId, messageId)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
|
@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.jobmanager.JobLogger.format
|
|||
import org.thoughtcrime.securesms.jobmanager.JsonJobData
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId.Companion.forConversation
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
@ -232,11 +231,7 @@ class RestoreAttachmentThumbnailJob private constructor(
|
|||
}
|
||||
|
||||
private fun markFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
try {
|
||||
SignalDatabase.attachments.setThumbnailRestoreProgressFailed(attachmentId, messageId)
|
||||
} catch (e: MmsException) {
|
||||
Log.w(TAG, e)
|
||||
}
|
||||
SignalDatabase.attachments.setThumbnailRestoreProgressFailed(attachmentId, messageId)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import android.net.Uri
|
||||
import org.signal.core.util.Base64
|
||||
import org.signal.core.util.androidx.DocumentFileInfo
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.withinTransaction
|
||||
import org.signal.libsignal.protocol.InvalidMacException
|
||||
import org.signal.libsignal.protocol.InvalidMessageException
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.backup.v2.local.ArchiveFileSystem
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable
|
||||
import org.thoughtcrime.securesms.database.AttachmentTable.LocalRestorableAttachment
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.jobs.protos.RestoreLocalAttachmentJobData
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.MmsException
|
||||
import org.whispersystems.signalservice.api.backup.MediaName
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream
|
||||
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.StreamSupplier
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* Restore attachment from local backup storage.
|
||||
*/
|
||||
class RestoreLocalAttachmentJob private constructor(
|
||||
parameters: Parameters,
|
||||
private val attachmentId: AttachmentId,
|
||||
private val messageId: Long,
|
||||
private val restoreUri: Uri,
|
||||
private val size: Long
|
||||
) : Job(parameters) {
|
||||
|
||||
companion object {
|
||||
const val KEY = "RestoreLocalAttachmentJob"
|
||||
val TAG = Log.tag(RestoreLocalAttachmentJob::class.java)
|
||||
|
||||
fun enqueueRestoreLocalAttachmentsJobs(mediaNameToFileInfo: Map<String, DocumentFileInfo>) {
|
||||
var restoreAttachmentJobs: MutableList<Job>
|
||||
|
||||
do {
|
||||
val possibleRestorableAttachments: List<LocalRestorableAttachment> = SignalDatabase.attachments.getLocalRestorableAttachments(500)
|
||||
val restorableAttachments = ArrayList<LocalRestorableAttachment>(possibleRestorableAttachments.size)
|
||||
val notRestorableAttachments = ArrayList<LocalRestorableAttachment>(possibleRestorableAttachments.size)
|
||||
|
||||
restoreAttachmentJobs = ArrayList(possibleRestorableAttachments.size)
|
||||
|
||||
possibleRestorableAttachments
|
||||
.forEachIndexed { index, attachment ->
|
||||
val fileInfo = if (attachment.remoteKey != null && attachment.remoteDigest != null) {
|
||||
val mediaName = MediaName.fromDigest(attachment.remoteDigest).name
|
||||
mediaNameToFileInfo[mediaName]
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
if (fileInfo != null) {
|
||||
restorableAttachments += attachment
|
||||
restoreAttachmentJobs += RestoreLocalAttachmentJob("RestoreLocalAttachmentJob_${index % 2}", attachment, fileInfo)
|
||||
} else {
|
||||
notRestorableAttachments += attachment
|
||||
}
|
||||
}
|
||||
|
||||
SignalDatabase.rawDatabase.withinTransaction {
|
||||
SignalDatabase.attachments.setRestoreInProgressTransferState(restorableAttachments)
|
||||
SignalDatabase.attachments.setRestoreFailedTransferState(notRestorableAttachments)
|
||||
|
||||
SignalStore.backup.totalRestorableAttachmentSize = SignalDatabase.attachments.getRemainingRestorableAttachmentSize()
|
||||
AppDependencies.jobManager.addAll(restoreAttachmentJobs)
|
||||
}
|
||||
} while (restoreAttachmentJobs.isNotEmpty())
|
||||
}
|
||||
}
|
||||
|
||||
private constructor(queue: String, attachment: LocalRestorableAttachment, info: DocumentFileInfo) : this(
|
||||
Parameters.Builder()
|
||||
.setQueue(queue)
|
||||
.setLifespan(TimeUnit.DAYS.toMillis(1))
|
||||
.setMaxAttempts(Parameters.UNLIMITED)
|
||||
.build(),
|
||||
attachmentId = attachment.attachmentId,
|
||||
messageId = attachment.mmsId,
|
||||
restoreUri = info.documentFile.uri,
|
||||
size = info.size
|
||||
)
|
||||
|
||||
override fun serialize(): ByteArray? {
|
||||
return RestoreLocalAttachmentJobData(
|
||||
attachmentId = attachmentId.id,
|
||||
messageId = messageId,
|
||||
fileUri = restoreUri.toString(),
|
||||
fileSize = size
|
||||
).encode()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
override fun run(): Result {
|
||||
Log.i(TAG, "onRun() messageId: $messageId attachmentId: $attachmentId")
|
||||
|
||||
val attachment = SignalDatabase.attachments.getAttachment(attachmentId)
|
||||
|
||||
if (attachment == null) {
|
||||
Log.w(TAG, "attachment no longer exists.")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
if (attachment.remoteDigest == null || attachment.remoteKey == null) {
|
||||
Log.w(TAG, "Attachment no longer has a remote digest or key")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
if (attachment.isPermanentlyFailed) {
|
||||
Log.w(TAG, "Attachment was marked as a permanent failure. Refusing to download.")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
if (attachment.transferState != AttachmentTable.TRANSFER_NEEDS_RESTORE && attachment.transferState != AttachmentTable.TRANSFER_RESTORE_IN_PROGRESS) {
|
||||
Log.w(TAG, "Attachment does not need to be restored.")
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
val combinedKey = Base64.decode(attachment.remoteKey)
|
||||
val streamSupplier = StreamSupplier { ArchiveFileSystem.openInputStream(context, restoreUri) ?: throw IOException("Unable to open stream") }
|
||||
|
||||
try {
|
||||
// TODO [local-backup] actually verify mac and save iv
|
||||
AttachmentCipherInputStream.createForAttachment(streamSupplier, size, attachment.size, combinedKey, null, null, 0, true).use { input ->
|
||||
SignalDatabase.attachments.finalizeAttachmentAfterDownload(attachment.mmsId, attachment.attachmentId, input, null)
|
||||
}
|
||||
} catch (e: InvalidMessageException) {
|
||||
Log.w(TAG, "Experienced an InvalidMessageException while trying to read attachment.", e)
|
||||
if (e.cause is InvalidMacException) {
|
||||
Log.w(TAG, "Detected an invalid mac. Treating as a permanent failure.")
|
||||
markPermanentlyFailed(messageId, attachmentId)
|
||||
}
|
||||
return Result.failure()
|
||||
} catch (e: MmsException) {
|
||||
Log.w(TAG, "Experienced exception while trying to store attachment.", e)
|
||||
return Result.failure()
|
||||
} catch (e: IOException) {
|
||||
Log.w(TAG, "Experienced an exception while trying to read attachment.", e)
|
||||
return Result.retry(defaultBackoff())
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
override fun onFailure() {
|
||||
markFailed(messageId, attachmentId)
|
||||
AppDependencies.databaseObserver.notifyAttachmentUpdatedObservers()
|
||||
}
|
||||
|
||||
private fun markFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
SignalDatabase.attachments.setTransferProgressFailed(attachmentId, messageId)
|
||||
}
|
||||
|
||||
private fun markPermanentlyFailed(messageId: Long, attachmentId: AttachmentId) {
|
||||
SignalDatabase.attachments.setTransferProgressPermanentFailure(attachmentId, messageId)
|
||||
}
|
||||
|
||||
class Factory : Job.Factory<RestoreLocalAttachmentJob?> {
|
||||
override fun create(parameters: Parameters, serializedData: ByteArray?): RestoreLocalAttachmentJob {
|
||||
val data = RestoreLocalAttachmentJobData.ADAPTER.decode(serializedData!!)
|
||||
return RestoreLocalAttachmentJob(
|
||||
parameters = parameters,
|
||||
attachmentId = AttachmentId(data.attachmentId),
|
||||
messageId = data.messageId,
|
||||
restoreUri = Uri.parse(data.fileUri),
|
||||
size = data.fileSize
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -152,7 +152,7 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v
|
|||
val threadId = args.threadId
|
||||
viewModel.fetchAttachments(requireContext(), startingAttachmentId, threadId, sorting)
|
||||
val dbObserver = DatabaseObserver.Observer { viewModel.fetchAttachments(requireContext(), startingAttachmentId, threadId, sorting, true) }
|
||||
AppDependencies.databaseObserver.registerAttachmentObserver(dbObserver)
|
||||
AppDependencies.databaseObserver.registerAttachmentUpdatedObserver(dbObserver)
|
||||
this.dbChangeObserver = dbObserver
|
||||
}
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ public class MediaUtil {
|
|||
public static final String UNKNOWN = "*/*";
|
||||
public static final String OCTET = "application/octet-stream";
|
||||
|
||||
public static SlideType getSlideTypeFromContentType(@Nullable String contentType) {
|
||||
public static @NonNull SlideType getSlideTypeFromContentType(@Nullable String contentType) {
|
||||
if (isGif(contentType)) {
|
||||
return SlideType.GIF;
|
||||
} else if (isImageType(contentType)) {
|
||||
|
|
|
@ -117,4 +117,11 @@ message GroupCallPeekJobData {
|
|||
uint64 groupRecipientId = 1;
|
||||
uint64 senderRecipientId = 2;
|
||||
uint64 serverTimestamp = 3;
|
||||
}
|
||||
}
|
||||
|
||||
message RestoreLocalAttachmentJobData {
|
||||
uint64 attachmentId = 1;
|
||||
uint64 messageId = 2;
|
||||
string fileUri = 3;
|
||||
uint64 fileSize = 4;
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import androidx.documentfile.provider.DocumentFile
|
|||
import androidx.documentfile.provider.isTreeDocumentFile
|
||||
import org.signal.core.util.ThreadUtil
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireLong
|
||||
import org.signal.core.util.requireNonNullString
|
||||
import org.signal.core.util.requireString
|
||||
|
@ -29,7 +30,7 @@ object DocumentFileUtil {
|
|||
private const val FILE_SELECTION = "${DocumentsContract.Document.COLUMN_DISPLAY_NAME} = ?"
|
||||
|
||||
private const val LIST_FILES_SELECTION = "${DocumentsContract.Document.COLUMN_MIME_TYPE} != ?"
|
||||
private val LIST_FILES_SELECTION_ARS = arrayOf(DocumentsContract.Document.MIME_TYPE_DIR)
|
||||
private val LIST_FILES_SELECTION_ARGS = arrayOf(DocumentsContract.Document.MIME_TYPE_DIR)
|
||||
|
||||
private const val MAX_STORAGE_ATTEMPTS: Int = 5
|
||||
private val WAIT_FOR_SCOPED_STORAGE: LongArray = longArrayOf(0, 2.seconds.inWholeMilliseconds, 10.seconds.inWholeMilliseconds, 20.seconds.inWholeMilliseconds, 30.seconds.inWholeMilliseconds)
|
||||
|
@ -83,36 +84,34 @@ object DocumentFileUtil {
|
|||
* If direct queries fail to find the file, will fallback to using [DocumentFile.findFile].
|
||||
*/
|
||||
fun DocumentFile.findFile(context: Context, fileName: String): DocumentFileInfo? {
|
||||
val child = if (isTreeDocumentFile()) {
|
||||
val child: List<DocumentFileInfo> = if (isTreeDocumentFile()) {
|
||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(uri, DocumentsContract.getDocumentId(uri))
|
||||
|
||||
try {
|
||||
context
|
||||
.contentResolver
|
||||
.query(childrenUri, FILE_PROJECTION, FILE_SELECTION, arrayOf(fileName), null)
|
||||
?.use { cursor ->
|
||||
if (cursor.count == 1) {
|
||||
cursor.moveToFirst()
|
||||
val uri = DocumentsContract.buildDocumentUriUsingTree(uri, cursor.requireString(DocumentsContract.Document.COLUMN_DOCUMENT_ID))
|
||||
val displayName = cursor.requireNonNullString(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val length = cursor.requireLong(DocumentsContract.Document.COLUMN_SIZE)
|
||||
?.readToList(predicate = { it.name == fileName }) { cursor ->
|
||||
val uri = DocumentsContract.buildDocumentUriUsingTree(uri, cursor.requireString(DocumentsContract.Document.COLUMN_DOCUMENT_ID))
|
||||
val displayName = cursor.requireNonNullString(DocumentsContract.Document.COLUMN_DISPLAY_NAME)
|
||||
val length = cursor.requireLong(DocumentsContract.Document.COLUMN_SIZE)
|
||||
|
||||
DocumentFileInfo(DocumentFile.fromSingleUri(context, uri)!!, displayName, length)
|
||||
} else {
|
||||
val message = if (cursor.count > 1) "Multiple files" else "No files"
|
||||
Log.w(TAG, "$message returned with same name")
|
||||
null
|
||||
}
|
||||
}
|
||||
DocumentFileInfo(DocumentFile.fromSingleUri(context, uri)!!, displayName, length)
|
||||
} ?: emptyList()
|
||||
} catch (e: Exception) {
|
||||
Log.d(TAG, "Unable to find file directly on ${javaClass.simpleName}, falling back to OS", e)
|
||||
null
|
||||
emptyList()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
emptyList()
|
||||
}
|
||||
|
||||
return child ?: this.findFile(fileName)?.let { DocumentFileInfo(it, it.name!!, it.length()) }
|
||||
return if (child.size == 1) {
|
||||
child[0]
|
||||
} else {
|
||||
Log.w(TAG, "Did not find single file, found (${child.size}), falling back to OS")
|
||||
this.findFile(fileName)?.let { DocumentFileInfo(it, it.name!!, it.length()) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -128,7 +127,7 @@ object DocumentFileUtil {
|
|||
try {
|
||||
val results = context
|
||||
.contentResolver
|
||||
.query(childrenUri, FILE_PROJECTION, LIST_FILES_SELECTION, LIST_FILES_SELECTION_ARS, null)
|
||||
.query(childrenUri, FILE_PROJECTION, LIST_FILES_SELECTION, LIST_FILES_SELECTION_ARGS, null)
|
||||
?.use { cursor ->
|
||||
val results = ArrayList<DocumentFileInfo>(cursor.count)
|
||||
while (cursor.moveToNext()) {
|
||||
|
|
Loading…
Add table
Reference in a new issue