diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt index e567779c2f..6a0b87bf0c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/BackupRepository.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt index 027531c737..515a1e27f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/local/ArchiveFileSystem.kt @@ -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)]!! } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatus.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatus.kt index a40b681d6a..8f51eaec85 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatus.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/ui/status/BackupStatus.kt @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt index bb3ee57197..9c143549a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/Banner.kt @@ -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) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/BannerManager.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/BannerManager.kt index 651e110ccd..8c4399d279 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/BannerManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/BannerManager.kt @@ -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() diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt index 6d71bd0e6d..e5c6aa521e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/BubbleOptOutBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt index 52fa4135c0..300b392fc4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsPermanentErrorBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt index adebcded25..42ce64ce8f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/CdsTemporaryErrorBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt index ff850786c4..860d7c7f6b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/DozeBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/EnclaveFailureBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/EnclaveFailureBanner.kt index e6f5622241..434b5f0f9c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/EnclaveFailureBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/EnclaveFailureBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt index 969ad8d996..55ec574adc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/GroupsV1MigrationSuggestionsBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt index b591c067e9..1acc29709d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/MediaRestoreProgressBanner.kt @@ -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 { - 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 { + 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(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) - } - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt index 5dd179e849..b3cdbfadf3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/OutdatedBuildBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt index e5001e73f4..b4974412b3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/PendingGroupJoinRequestsBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/ServiceOutageBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/ServiceOutageBanner.kt index 59bcba18d4..633561edf5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/ServiceOutageBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/ServiceOutageBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UnauthorizedBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UnauthorizedBanner.kt index 35b5c7e9c5..d848809ae5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UnauthorizedBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UnauthorizedBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt index 3425c6180a..4cc2fb033b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/banners/UsernameOutOfSyncBanner.kt @@ -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 ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/banner/ui/compose/DefaultBanner.kt b/app/src/main/java/org/thoughtcrime/securesms/banner/ui/compose/DefaultBanner.kt index 18266a0394..c4eef12a3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/banner/ui/compose/DefaultBanner.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/banner/ui/compose/DefaultBanner.kt @@ -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 = 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) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt index c3fce6bf51..ac3cac2a9b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt @@ -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 private lateinit var importFileLauncher: ActivityResultLauncher - private lateinit var importDirectoryLauncher: ActivityResultLauncher private lateinit var validateFileLauncher: ActivityResultLauncher 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( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt index d368b81915..73622649db 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundViewModel.kt @@ -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 = mutableStateOf(ScreenState(backupState = BackupState.NONE, uploadState = BackupUploadState.NONE, plaintext = false)) + private val _state: MutableState = 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 = _state private val _mediaState: MutableState = 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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java index 639070ba76..0dd9834f0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackService.java @@ -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 diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt index c85d16c79d..5bb3360161 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentTable.kt @@ -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) { + setRestoreTransferState( + restorableAttachments = restorableAttachments, + prefix = "$TRANSFER_STATE = $TRANSFER_NEEDS_RESTORE", + state = TRANSFER_RESTORE_IN_PROGRESS + ) + } + + fun setRestoreFailedTransferState(notRestorableAttachments: List) { + setRestoreTransferState( + restorableAttachments = notRestorableAttachments, + prefix = "$TRANSFER_STATE != $TRANSFER_PROGRESS_PERMANENT_FAILURE", + state = TRANSFER_PROGRESS_FAILED + ) + } + + private fun setRestoreTransferState(restorableAttachments: List, 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() + 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? ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java index f56a4f6f73..5b643c1566 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseObserver.java @@ -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. - * + *

* 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 conversationListObservers; @@ -63,7 +60,8 @@ public class DatabaseObserver { private final Set chatColorsObservers; private final Set stickerObservers; private final Set stickerPackObservers; - private final Set attachmentObservers; + private final Set attachmentUpdatedObservers; + private final Set attachmentDeletedObservers; private final Set messageUpdateObservers; private final Map> messageInsertObservers; private final Set notificationProfileObservers; @@ -72,8 +70,7 @@ public class DatabaseObserver { private final Map> callLinkObservers; private final Set 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); }); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseTable.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseTable.java index a39f0950d1..2ddc7c1d04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseTable.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseTable.java @@ -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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt index ec0181d6f8..ff8c367c48 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MediaTable.kt @@ -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 } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt index 7efd427c7b..f182c84eaa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageTable.kt @@ -2115,7 +2115,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat AppDependencies.databaseObserver.notifyConversationListListeners() if (deletedAttachments) { - AppDependencies.databaseObserver.notifyAttachmentObservers() + AppDependencies.databaseObserver.notifyAttachmentDeletedObservers() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt index a16e0024ab..a1c1f6aba1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadTable.kt @@ -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() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java index 9d7f4af504..e86a159dd9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/loaders/GroupedThreadMediaLoader.java @@ -71,7 +71,7 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader) { + var restoreAttachmentJobs: MutableList + + do { + val possibleRestorableAttachments: List = SignalDatabase.attachments.getLocalRestorableAttachments(500) + val restorableAttachments = ArrayList(possibleRestorableAttachments.size) + val notRestorableAttachments = ArrayList(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 { + 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 + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt index a6705b55a4..0cfe531798 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt @@ -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 } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java index 5fc605f44e..1a630fe124 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MediaUtil.java @@ -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)) { diff --git a/app/src/main/protowire/JobData.proto b/app/src/main/protowire/JobData.proto index ca35357588..e2e5a42f3d 100644 --- a/app/src/main/protowire/JobData.proto +++ b/app/src/main/protowire/JobData.proto @@ -117,4 +117,11 @@ message GroupCallPeekJobData { uint64 groupRecipientId = 1; uint64 senderRecipientId = 2; uint64 serverTimestamp = 3; -} \ No newline at end of file +} + +message RestoreLocalAttachmentJobData { + uint64 attachmentId = 1; + uint64 messageId = 2; + string fileUri = 3; + uint64 fileSize = 4; +} diff --git a/core-util/src/main/java/org/signal/core/util/androidx/DocumentFileUtil.kt b/core-util/src/main/java/org/signal/core/util/androidx/DocumentFileUtil.kt index e942592474..55ea4c17f7 100644 --- a/core-util/src/main/java/org/signal/core/util/androidx/DocumentFileUtil.kt +++ b/core-util/src/main/java/org/signal/core/util/androidx/DocumentFileUtil.kt @@ -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 = 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(cursor.count) while (cursor.moveToNext()) {