Move BlobProvider and supporting classes to core-util.

This commit is contained in:
Alex Hart
2026-06-24 14:49:49 -04:00
committed by jeffrey-signal
parent f375750cf8
commit 4316136723
106 changed files with 652 additions and 503 deletions
@@ -39,6 +39,7 @@ import org.signal.core.util.MemoryTracker;
import org.signal.core.util.Util;
import org.signal.core.util.concurrent.AnrDetector;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.core.util.logging.AndroidLogger;
import org.signal.core.util.logging.Log;
import org.signal.core.util.logging.Scrubber;
@@ -51,10 +52,10 @@ import org.signal.ringrtc.CallManager;
import org.thoughtcrime.securesms.apkupdate.ApkUpdateRefreshListener;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.thoughtcrime.securesms.backup.v2.BackupRepository;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.conversation.drafts.DraftBlobs;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.database.SQLiteDatabase;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
@@ -99,7 +100,6 @@ import org.thoughtcrime.securesms.messageprocessingalarm.RoutineMessageFetchRece
import org.thoughtcrime.securesms.messages.IncomingMessageObserver;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.registration.util.RegistrationUtil;
@@ -175,7 +175,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
SqlCipherLibraryLoader.load();
SignalDatabase.init(this,
DatabaseSecretProvider.getOrCreateDatabaseSecret(this),
AttachmentSecretProvider.getInstance(this).getOrCreateAttachmentSecret());
AttachmentSecretProvider.getInstance(this, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret());
Logger.setTarget(SqlCipherLogTarget.INSTANCE);
})
.addBlocking("signal-store", () -> SignalStore.init(this))
@@ -578,7 +578,7 @@ public class ApplicationContext extends Application implements AppForegroundObse
@WorkerThread
private void initializeBlobProvider() {
BlobProvider.getInstance().initialize(this);
AppDependencies.getBlobs().initialize(this, DraftBlobs.INSTANCE::deleteOrphanedDraftFiles);
}
@WorkerThread
@@ -11,10 +11,11 @@ import androidx.annotation.Nullable;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.contentproviders.BlobProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.notifications.v2.InChatNotificationSoundSuppressor;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.IOException;
@@ -88,9 +89,9 @@ public class AudioRecorder {
ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe();
BlobProvider.BlobBuilder blobBuilder = BlobProvider.getInstance()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType(MediaUtil.AUDIO_AAC);
BlobProvider.BlobBuilder blobBuilder = AppDependencies.getBlobs()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType(MediaUtil.AUDIO_AAC);
recordingUri = blobBuilder.buildUriForDraftAttachment();
recordingUriFuture = blobBuilder.createForDraftAttachmentAsync(context);
@@ -11,9 +11,9 @@ import androidx.appcompat.content.res.AppCompatResources
import com.airbnb.lottie.SimpleColorFilter
import org.signal.core.models.media.Media
import org.signal.core.util.concurrent.SignalExecutors
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.MediaUtil
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
@@ -80,7 +80,7 @@ object AvatarRenderer {
private fun renderPhoto(context: Context, avatar: Avatar.Photo, onAvatarRendered: (Media) -> Unit) {
SignalExecutors.BOUNDED.execute {
val blob = BlobProvider.getInstance()
val blob = AppDependencies.blobs
.forData(AvatarPickerStorage.read(context, PartAuthority.getAvatarPickerFilename(avatar.uri)), avatar.size)
.createForSingleSessionOnDisk(context)
@@ -124,7 +124,7 @@ object AvatarRenderer {
val bytes = outStream.toByteArray()
val inStream = ByteArrayInputStream(bytes)
val uri = BlobProvider.getInstance().forData(inStream, bytes.size.toLong()).createForSingleSessionOnDisk(context)
val uri = AppDependencies.blobs.forData(inStream, bytes.size.toLong()).createForSingleSessionOnDisk(context)
onAvatarRendered(createMedia(uri, bytes.size.toLong()))
}
@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.avatar.AvatarBundler
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), ImageEditorFragment.Controller {
@@ -39,15 +39,15 @@ class PhotoEditorFragment : Fragment(R.layout.avatar_photo_editor_fragment), Ima
SignalExecutors.BOUNDED.execute {
val editedImageUri = imageEditorFragment.renderToSingleUseBlob()
val size = BlobProvider.getFileSize(editedImageUri) ?: 0
val inputStream = BlobProvider.getInstance().getStream(applicationContext, editedImageUri)
val size = AppDependencies.blobs.getFileSize(editedImageUri) ?: 0
val inputStream = AppDependencies.blobs.getStream(applicationContext, editedImageUri)
val onDiskUri = AvatarPickerStorage.save(applicationContext, inputStream)
val photo = AvatarBundler.extractPhoto(args.photoAvatar)
val database = SignalDatabase.avatarPicker
val newPhoto = photo.copy(uri = onDiskUri, size = size)
database.update(newPhoto)
BlobProvider.getInstance().delete(requireContext(), photo.uri)
AppDependencies.blobs.delete(requireContext(), photo.uri)
ThreadUtil.runOnMain {
setFragmentResult(REQUEST_KEY_EDIT, AvatarBundler.bundlePhoto(newPhoto))
@@ -16,9 +16,9 @@ import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.conversation.colors.AvatarColor
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.groups.GroupId
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.NameUtil
import org.whispersystems.signalservice.api.util.StreamDetails
@@ -36,7 +36,7 @@ class AvatarPickerRepository(context: Context) {
try {
val bytes = StreamUtil.readFully(details.stream)
Avatar.Photo(
BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(),
AppDependencies.blobs.forData(bytes).createForSingleSessionInMemory(),
details.length,
Avatar.DatabaseId.DoNotPersist
)
@@ -56,7 +56,7 @@ class AvatarPickerRepository(context: Context) {
try {
val bytes = AvatarHelper.getAvatarBytes(applicationContext, recipient.id)
Avatar.Photo(
BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory(),
AppDependencies.blobs.forData(bytes).createForSingleSessionInMemory(),
AvatarHelper.getAvatarLength(applicationContext, recipient.id),
Avatar.DatabaseId.DoNotPersist
)
@@ -6,7 +6,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.KeyStoreHelper;
import org.signal.core.util.crypto.KeyStoreHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
/**
@@ -23,9 +23,9 @@ import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.backup.proto.KeyValue;
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.signal.core.util.crypto.AttachmentSecret;
import org.signal.core.util.crypto.ClassicDecryptingPartInputStream;
import org.signal.core.util.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable;
import org.thoughtcrime.securesms.database.EmojiSearchTable;
@@ -24,8 +24,8 @@ import org.thoughtcrime.securesms.backup.proto.KeyValue;
import org.thoughtcrime.securesms.backup.proto.SharedPreference;
import org.thoughtcrime.securesms.backup.proto.SqlStatement;
import org.thoughtcrime.securesms.backup.proto.Sticker;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.signal.core.util.crypto.AttachmentSecret;
import org.signal.core.util.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.EmojiSearchTable;
import org.thoughtcrime.securesms.database.KeyValueDatabase;
@@ -47,6 +47,7 @@ import org.signal.core.util.bytes
import org.signal.core.util.concurrent.LimitedWorker
import org.signal.core.util.concurrent.SignalDispatchers
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.crypto.AttachmentSecretProvider
import org.signal.core.util.decodeOrNull
import org.signal.core.util.forceForeignKeyConstraintsEnabled
import org.signal.core.util.fullWalCheckpoint
@@ -93,7 +94,7 @@ import org.thoughtcrime.securesms.backup.v2.ui.BackupAlert
import org.thoughtcrime.securesms.backup.v2.ui.subscription.MessageBackupsType
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.subscription.RecurringInAppPaymentRepository
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
import org.thoughtcrime.securesms.database.AttachmentTable
import org.thoughtcrime.securesms.database.BackupMediaSnapshotTable.ArchiveMediaItem
@@ -140,7 +141,6 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogRepository
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.service.BackupMediaRestoreService
@@ -717,7 +717,7 @@ object BackupRepository {
SignalDatabase(
context = context,
databaseSecret = DatabaseSecretProvider.getOrCreateDatabaseSecret(context),
attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore).getOrCreateAttachmentSecret(),
name = "$baseName.db"
)
}
@@ -2235,7 +2235,7 @@ object BackupRepository {
}
Log.i(TAG, "[remoteRestore] Downloading backup")
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
val tempBackupFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
when (val result = downloadBackupFile(tempBackupFile, progressListener)) {
is NetworkResult.Success -> Log.i(TAG, "[remoteRestore] Download successful")
else -> {
@@ -2355,7 +2355,7 @@ object BackupRepository {
}
Log.i(TAG, "[restoreLinkAndSyncBackup] Downloading backup")
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
val tempBackupFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
when (val result = AppDependencies.signalServiceMessageReceiver.retrieveLinkAndSyncBackup(response.cdn, response.key, tempBackupFile, progressListener)) {
is NetworkResult.Success -> Log.i(TAG, "[restoreLinkAndSyncBackup] Download successful")
else -> {
@@ -7,8 +7,8 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.util.JsonUtils
import org.signal.network.NetworkResult
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.providers.BlobProvider
class ExportAccountDataRepository {
@@ -44,7 +44,7 @@ class ExportAccountDataRepository {
tree["text"].asText()
}
val uri = BlobProvider.getInstance()
val uri = AppDependencies.blobs
.forData(dataStr.encodeToByteArray())
.withMimeType(mimeType)
.withFileName(fileName)
@@ -57,7 +57,6 @@ import org.thoughtcrime.securesms.jobs.LocalBackupJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.protos.LocalBackupCreationProgress
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import java.io.FileOutputStream
import java.io.IOException
@@ -136,7 +135,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
fun validateBackup() {
_state.value = _state.value.copy(statusMessage = "Exporting to a temporary file...")
val tempFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
val tempFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
disposables += Single
.fromCallable {
@@ -207,7 +206,7 @@ class InternalBackupPlaygroundViewModel : ViewModel() {
SignalExecutors.BOUNDED_IO.execute {
Log.d(TAG, "Downloading file...")
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
val tempBackupFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
when (val result = BackupRepository.downloadBackupFile(tempBackupFile)) {
is NetworkResult.Success -> Log.i(TAG, "Download successful")
@@ -24,8 +24,8 @@ import kotlinx.coroutines.withContext
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.database.model.InAppPaymentReceiptRecord
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.payments.FiatMoneyUtil
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.DateUtils
import java.io.ByteArrayOutputStream
import java.util.Locale
@@ -75,7 +75,7 @@ object ReceiptImageRenderer {
val bitmap = view.drawToBitmap()
bitmap.compress(Bitmap.CompressFormat.PNG, 0, outputStream)
BlobProvider.getInstance()
AppDependencies.blobs
.forData(outputStream.toByteArray())
.withMimeType("image/png")
.withFileName("Signal-Donation-Receipt.png")
@@ -75,7 +75,7 @@ import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeDa
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.QrCodeState
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.UsernameQrCodeColorScheme
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.UsernameLinkSettingsState.ActiveTab
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.CommunicationActions
import java.io.ByteArrayOutputStream
import java.util.UUID
@@ -405,7 +405,7 @@ private fun shareQrBadge(activity: Activity, badge: Bitmap?) {
badge.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream)
byteArrayOutputStream.flush()
val bytes = byteArrayOutputStream.toByteArray()
val shareUri = BlobProvider.getInstance()
val shareUri = AppDependencies.blobs
.forData(bytes)
.withMimeType("image/png")
.withFileName("SignalUsernameQr.png")
@@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.mms.IncomingMessage
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver
import org.thoughtcrime.securesms.recipients.RecipientId
@@ -88,7 +87,7 @@ class InternalConversationSettingsFragment : ComposeFragment(), InternalConversa
)
val stream = BitmapUtil.toCompressedJpeg(bitmap)
val bytes = stream.readBytes()
val uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionOnDisk(requireContext())
val uri = AppDependencies.blobs.forData(bytes).createForSingleSessionOnDisk(requireContext())
return UriAttachment(
uri = uri,
contentType = MediaUtil.IMAGE_JPEG,
@@ -18,8 +18,8 @@ import org.thoughtcrime.securesms.contactshare.Contact.Email;
import org.thoughtcrime.securesms.contactshare.Contact.Name;
import org.thoughtcrime.securesms.contactshare.Contact.Phone;
import org.thoughtcrime.securesms.contactshare.Contact.PostalAddress;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.SignalE164Util;
@@ -98,8 +98,8 @@ public class SharedContactRepository {
Log.w(TAG, "Failed to parse the vcard.", e);
}
if (BlobProvider.AUTHORITY.equals(uri.getAuthority())) {
BlobProvider.getInstance().delete(context, uri);
if (AppDependencies.getBlobs().getAuthority().equals(uri.getAuthority())) {
AppDependencies.getBlobs().delete(context, uri);
}
return contact;
@@ -0,0 +1,46 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.conversation.drafts
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.drafts
import org.thoughtcrime.securesms.dependencies.AppDependencies
import java.io.File
object DraftBlobs {
private val TAG = Log.tag(DraftBlobs::class)
fun deleteOrphanedDraftFiles(directory: File) {
val files = directory.listFiles()
if (files == null || files.size == 0) {
Log.d(TAG, "No attachment drafts exist. Skipping.")
return
}
val draftDatabase = drafts
val voiceNoteDrafts = draftDatabase.getAllVoiceNoteDrafts()
val draftFileNames = voiceNoteDrafts
.asSequence()
.map { VoiceNoteDraft.fromDraft(it) }
.map(VoiceNoteDraft::uri)
.mapNotNull { AppDependencies.blobs.buildFileName(it) }
.toList()
for (file in files) {
if (!draftFileNames.contains(file.getName())) {
if (file.delete()) {
Log.d(TAG, "Deleted orphaned attachment draft: " + file.getName())
} else {
Log.d(TAG, "Failed to delete orphaned attachment draft: " + file.getName())
}
}
}
}
}
@@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.mms.QuoteId
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideFactory
import org.thoughtcrime.securesms.mms.StickerSlide
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor
@@ -161,7 +160,7 @@ class DraftRepository(
fun deleteVoiceNoteDraftData(draft: DraftTable.Draft?) {
if (draft != null) {
SignalExecutors.BOUNDED.execute {
BlobProvider.getInstance().delete(context, Uri.parse(draft.value).buildUpon().clearQuery().build())
AppDependencies.blobs.delete(context, Uri.parse(draft.value).buildUpon().clearQuery().build())
}
}
}
@@ -326,7 +326,6 @@ import org.thoughtcrime.securesms.polls.PollOption
import org.thoughtcrime.securesms.polls.PollRecord
import org.thoughtcrime.securesms.profiles.manage.EditProfileActivity
import org.thoughtcrime.securesms.profiles.spoofing.ReviewCardDialogFragment
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.ratelimit.RecaptchaProofBottomSheetFragment
import org.thoughtcrime.securesms.ratelimit.RecaptchaRequiredEvent
import org.thoughtcrime.securesms.reactions.ReactionsBottomSheetDialogFragment
@@ -1013,7 +1012,7 @@ class ConversationFragment :
override fun onGifSelectSuccess(blobUri: Uri, width: Int, height: Int) {
setMedia(
uri = blobUri,
mediaType = SlideFactory.MediaType.from(BlobProvider.getMimeType(blobUri))!!,
mediaType = SlideFactory.MediaType.from(AppDependencies.blobs.getMimeType(blobUri))!!,
width = width,
height = height,
videoGif = true
@@ -86,7 +86,6 @@ import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.polls.Poll
import org.thoughtcrime.securesms.profiles.spoofing.ReviewRecipient
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.recipients.RecipientUtil
@@ -681,7 +680,7 @@ class ConversationRepository(
val thumbnailUri = thumbnailSlide.uri ?: return@fromCallable null
val inputStream = PartAuthority.getAttachmentStream(applicationContext, thumbnailUri)
val tempUri = BlobProvider.getInstance().forData(inputStream, thumbnailSlide.fileSize)
val tempUri = AppDependencies.blobs.forData(inputStream, thumbnailSlide.fileSize)
.withMimeType(thumbnailSlide.contentType)
.createForSingleSessionOnDisk(applicationContext)
@@ -826,9 +825,9 @@ class ConversationRepository(
SignalExecutors.BOUNDED_IO.execute {
slides
.mapNotNull(Slide::getUri)
.filter(BlobProvider::isAuthority)
.filter { AppDependencies.blobs.isAuthority(it) }
.forEach {
BlobProvider.getInstance().delete(applicationContext, it)
AppDependencies.blobs.delete(applicationContext, it)
}
}
}
@@ -0,0 +1,28 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.crypto
import android.content.Context
import org.signal.core.util.crypto.AttachmentSecretStore
import org.thoughtcrime.securesms.util.TextSecurePreferences
object AppAttachmentSecretStore : AttachmentSecretStore {
override fun getAttachmentUnencryptedSecret(context: Context): String? {
return TextSecurePreferences.getAttachmentUnencryptedSecret(context)
}
override fun getAttachmentEncryptedSecret(context: Context): String? {
return TextSecurePreferences.getAttachmentEncryptedSecret(context)
}
override fun setAttachmentEncryptedSecret(context: Context, secret: String) {
TextSecurePreferences.setAttachmentEncryptedSecret(context, secret)
}
override fun setAttachmentUnencryptedSecret(context: Context, secret: String?) {
TextSecurePreferences.setAttachmentUnencryptedSecret(context, secret)
}
}
@@ -1,116 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.util.Base64;
import androidx.annotation.NonNull;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.core.util.JsonUtils;
import java.io.IOException;
/**
* Encapsulates the key material used to encrypt attachments on disk.
*
* There are two logical pieces of material, a deprecated set of keys used to encrypt
* legacy attachments, and a key that is used to encrypt attachments going forward.
*/
public class AttachmentSecret {
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] classicCipherKey;
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] classicMacKey;
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] modernKey;
public AttachmentSecret(byte[] classicCipherKey, byte[] classicMacKey, byte[] modernKey)
{
this.classicCipherKey = classicCipherKey;
this.classicMacKey = classicMacKey;
this.modernKey = modernKey;
}
@SuppressWarnings("unused")
public AttachmentSecret() {
}
@JsonIgnore
byte[] getClassicCipherKey() {
return classicCipherKey;
}
@JsonIgnore
byte[] getClassicMacKey() {
return classicMacKey;
}
@JsonIgnore
public byte[] getModernKey() {
return modernKey;
}
@JsonIgnore
void setClassicCipherKey(byte[] classicCipherKey) {
this.classicCipherKey = classicCipherKey;
}
@JsonIgnore
void setClassicMacKey(byte[] classicMacKey) {
this.classicMacKey = classicMacKey;
}
public String serialize() {
try {
return JsonUtils.toJson(this);
} catch (IOException e) {
throw new AssertionError(e);
}
}
static AttachmentSecret fromString(@NonNull String value) {
try {
return JsonUtils.fromJson(value, AttachmentSecret.class);
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static class ByteArraySerializer extends JsonSerializer<byte[]> {
@Override
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING));
}
}
private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING);
}
}
}
@@ -1,90 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.content.Context;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.security.SecureRandom;
/**
* A provider that is responsible for creating or retrieving the AttachmentSecret model.
*
* On modern Android, the serialized secrets are themselves encrypted using a key that lives
* in the system KeyStore, for whatever that is worth.
*/
public class AttachmentSecretProvider {
private static AttachmentSecretProvider provider;
public static synchronized AttachmentSecretProvider getInstance(@NonNull Context context) {
if (provider == null) provider = new AttachmentSecretProvider(context.getApplicationContext());
return provider;
}
private final Context context;
private AttachmentSecret attachmentSecret;
private AttachmentSecretProvider(@NonNull Context context) {
this.context = context.getApplicationContext();
}
public synchronized AttachmentSecret getOrCreateAttachmentSecret() {
if (attachmentSecret != null) return attachmentSecret;
String unencryptedSecret = TextSecurePreferences.getAttachmentUnencryptedSecret(context);
String encryptedSecret = TextSecurePreferences.getAttachmentEncryptedSecret(context);
if (unencryptedSecret != null) attachmentSecret = getUnencryptedAttachmentSecret(context, unencryptedSecret);
else if (encryptedSecret != null) attachmentSecret = getEncryptedAttachmentSecret(encryptedSecret);
else attachmentSecret = createAndStoreAttachmentSecret(context);
return attachmentSecret;
}
public synchronized AttachmentSecret setClassicKey(@NonNull Context context, @NonNull byte[] classicCipherKey, @NonNull byte[] classicMacKey) {
AttachmentSecret currentSecret = getOrCreateAttachmentSecret();
currentSecret.setClassicCipherKey(classicCipherKey);
currentSecret.setClassicMacKey(classicMacKey);
storeAttachmentSecret(context, attachmentSecret);
return attachmentSecret;
}
private AttachmentSecret getUnencryptedAttachmentSecret(@NonNull Context context, @NonNull String unencryptedSecret)
{
AttachmentSecret attachmentSecret = AttachmentSecret.fromString(unencryptedSecret);
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
TextSecurePreferences.setAttachmentUnencryptedSecret(context, null);
return attachmentSecret;
}
private AttachmentSecret getEncryptedAttachmentSecret(@NonNull String serializedEncryptedSecret) {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.SealedData.fromString(serializedEncryptedSecret);
return AttachmentSecret.fromString(new String(KeyStoreHelper.unseal(encryptedSecret)));
}
private AttachmentSecret createAndStoreAttachmentSecret(@NonNull Context context) {
SecureRandom random = new SecureRandom();
byte[] secret = new byte[32];
random.nextBytes(secret);
AttachmentSecret attachmentSecret = new AttachmentSecret(null, null, secret);
storeAttachmentSecret(context, attachmentSecret);
return attachmentSecret;
}
private void storeAttachmentSecret(@NonNull Context context, @NonNull AttachmentSecret attachmentSecret) {
KeyStoreHelper.SealedData encryptedSecret = KeyStoreHelper.seal(attachmentSecret.serialize().getBytes());
TextSecurePreferences.setAttachmentEncryptedSecret(context, encryptedSecret.serialize());
}
}
@@ -1,170 +0,0 @@
/**
* Copyright (C) 2011 Whisper Systems
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms.crypto;
import androidx.annotation.NonNull;
import org.signal.core.util.logging.Log;
import org.signal.core.util.stream.LimitedInputStream;
import org.signal.core.util.Util;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ClassicDecryptingPartInputStream {
private static final String TAG = Log.tag(ClassicDecryptingPartInputStream.class);
private static final int IV_LENGTH = 16;
private static final int MAC_LENGTH = 20;
public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file)
throws IOException
{
try {
if (file.length() <= IV_LENGTH + MAC_LENGTH) {
throw new IOException("File too short");
}
verifyMac(attachmentSecret, file);
FileInputStream fileStream = new FileInputStream(file);
byte[] ivBytes = new byte[IV_LENGTH];
readFully(fileStream, ivBytes);
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivBytes);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(attachmentSecret.getClassicCipherKey(), "AES"), iv);
return new CipherInputStreamWrapper(new LimitedInputStream(fileStream, file.length() - MAC_LENGTH - IV_LENGTH), cipher);
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
private static void verifyMac(AttachmentSecret attachmentSecret, File file) throws IOException {
Mac mac = initializeMac(new SecretKeySpec(attachmentSecret.getClassicMacKey(), "HmacSHA1"));
FileInputStream macStream = new FileInputStream(file);
InputStream dataStream = new LimitedInputStream(new FileInputStream(file), file.length() - MAC_LENGTH);
byte[] theirMac = new byte[MAC_LENGTH];
if (macStream.skip(file.length() - MAC_LENGTH) != file.length() - MAC_LENGTH) {
throw new IOException("Unable to seek");
}
readFully(macStream, theirMac);
byte[] buffer = new byte[4096];
int read;
while ((read = dataStream.read(buffer)) != -1) {
mac.update(buffer, 0, read);
}
byte[] ourMac = mac.doFinal();
if (!MessageDigest.isEqual(ourMac, theirMac)) {
throw new IOException("Bad MAC");
}
macStream.close();
dataStream.close();
}
private static Mac initializeMac(SecretKeySpec key) {
try {
Mac hmac = Mac.getInstance("HmacSHA1");
hmac.init(key);
return hmac;
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new AssertionError(e);
}
}
private static void readFully(InputStream in, byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = in.read(buffer, offset, buffer.length-offset);
if (read + offset < buffer.length) offset += read;
else return;
}
}
// Note (4/3/17) -- Older versions of Android have a busted OpenSSL provider that
// throws a RuntimeException on a BadPaddingException, so we have to catch
// that here in case someone calls close() before reaching the end of the
// stream (since close() implicitly calls doFinal())
//
// See Signal-Android Issue #6477
// Android: https://android-review.googlesource.com/#/c/65321/
private static class CipherInputStreamWrapper extends CipherInputStream {
CipherInputStreamWrapper(InputStream is, Cipher c) {
super(is, c);
}
@Override
public void close() throws IOException {
try {
super.close();
} catch (Throwable t) {
Log.w(TAG, t);
}
}
@Override
public long skip(long skipAmount)
throws IOException
{
long remaining = skipAmount;
if (skipAmount <= 0) {
return 0;
}
byte[] skipBuffer = new byte[4092];
while (remaining > 0) {
int read = super.read(skipBuffer, 0, Util.toIntExact(Math.min(skipBuffer.length, remaining)));
if (read < 0) {
break;
}
remaining -= read;
}
return skipAmount - remaining;
}
}
}
@@ -5,6 +5,7 @@ import android.content.Context;
import androidx.annotation.NonNull;
import org.signal.core.util.crypto.KeyStoreHelper;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import java.io.IOException;
@@ -1,241 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.util.Base64;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.signal.core.util.logging.Log;
import org.signal.core.util.JsonUtils;
import java.io.IOException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableEntryException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
public final class KeyStoreHelper {
private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
private static final String KEY_ALIAS = "SignalSecret";
private static final Executor executor = Executors.newSingleThreadExecutor();
public static SealedData seal(@NonNull byte[] input) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<SealedData> result = new AtomicReference<>();
executor.execute(() -> {
try {
SecretKey secretKey = getOrCreateKeyStoreEntry();
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] iv = cipher.getIV();
byte[] data = cipher.doFinal(input);
result.set(new SealedData(iv, data));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
} finally {
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return result.get();
}
public static byte[] unseal(@NonNull SealedData sealedData) {
CountDownLatch latch = new CountDownLatch(1);
AtomicReference<byte[]> result = new AtomicReference<>();
executor.execute(() -> {
try {
SecretKey secretKey = getKeyStoreEntry();
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, secretKey, new GCMParameterSpec(128, sealedData.iv));
result.set(cipher.doFinal(sealedData.data));
} catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException e) {
throw new AssertionError(e);
} finally {
latch.countDown();
}
});
try {
latch.await();
} catch (InterruptedException e) {
throw new AssertionError(e);
}
return result.get();
}
@RequiresApi(23)
private static SecretKey getOrCreateKeyStoreEntry() {
if (hasKeyStoreEntry()) return getKeyStoreEntry();
else return createKeyStoreEntry();
}
@RequiresApi(23)
private static SecretKey createKeyStoreEntry() {
try {
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(KEY_ALIAS, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_GCM)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
.build();
keyGenerator.init(keyGenParameterSpec);
return keyGenerator.generateKey();
} catch (NoSuchAlgorithmException | NoSuchProviderException | InvalidAlgorithmParameterException e) {
throw new AssertionError(e);
}
}
@RequiresApi(23)
private static SecretKey getKeyStoreEntry() {
KeyStore keyStore = getKeyStore();
try {
// Attempt 1
return getSecretKey(keyStore);
} catch (UnrecoverableKeyException e) {
try {
// Attempt 2
return getSecretKey(keyStore);
} catch (UnrecoverableKeyException e2) {
throw new AssertionError(e2);
}
}
}
private static SecretKey getSecretKey(KeyStore keyStore) throws UnrecoverableKeyException {
try {
KeyStore.SecretKeyEntry entry = (KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
return entry.getSecretKey();
} catch (UnrecoverableKeyException e) {
throw e;
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableEntryException e) {
throw new AssertionError(e);
}
}
private static KeyStore getKeyStore() {
try {
KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
return keyStore;
} catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
@RequiresApi(23)
private static boolean hasKeyStoreEntry() {
try {
KeyStore ks = KeyStore.getInstance(ANDROID_KEY_STORE);
ks.load(null);
return ks.containsAlias(KEY_ALIAS) && ks.entryInstanceOf(KEY_ALIAS, KeyStore.SecretKeyEntry.class);
} catch (KeyStoreException | IOException | NoSuchAlgorithmException | CertificateException e) {
throw new AssertionError(e);
}
}
public static class SealedData {
@SuppressWarnings("unused")
private static final String TAG = Log.tag(SealedData.class);
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] iv;
@JsonProperty
@JsonSerialize(using = ByteArraySerializer.class)
@JsonDeserialize(using = ByteArrayDeserializer.class)
private byte[] data;
SealedData(@NonNull byte[] iv, @NonNull byte[] data) {
this.iv = iv;
this.data = data;
}
@SuppressWarnings("unused")
public SealedData() {}
public String serialize() {
try {
return JsonUtils.toJson(this);
} catch (IOException e) {
throw new AssertionError(e);
}
}
public static SealedData fromString(@NonNull String value) {
try {
return JsonUtils.fromJson(value, SealedData.class);
} catch (IOException e) {
throw new AssertionError(e);
}
}
private static class ByteArraySerializer extends JsonSerializer<byte[]> {
@Override
public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(Base64.encodeToString(value, Base64.NO_WRAP | Base64.NO_PADDING));
}
}
private static class ByteArrayDeserializer extends JsonDeserializer<byte[]> {
@Override
public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
return Base64.decode(p.getValueAsString(), Base64.NO_WRAP | Base64.NO_PADDING);
}
}
}
}
@@ -1,86 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import androidx.annotation.NonNull;
import org.signal.core.util.Conversions;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class ModernDecryptingPartInputStream {
public static final String PREMATURE_END_ERROR_MESSAGE = "Prematurely reached end of stream!";
public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull byte[] random, @NonNull File file, long offset)
throws IOException
{
return createFor(attachmentSecret, random, new FileInputStream(file), offset);
}
public static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file, long offset)
throws IOException
{
FileInputStream inputStream = new FileInputStream(file);
byte[] random = new byte[32];
readFully(inputStream, random);
return createFor(attachmentSecret, random, inputStream, offset);
}
private static InputStream createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull byte[] random, @NonNull InputStream inputStream, long offset) throws IOException {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "HmacSHA256"));
byte[] iv = new byte[16];
int remainder = (int) (offset % 16);
Conversions.longTo4ByteArray(iv, 12, offset / 16);
byte[] key = mac.doFinal(random);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
long skipped = inputStream.skip(offset - remainder);
if (skipped != offset - remainder) {
throw new IOException("Skip failed: " + skipped + " vs " + (offset - remainder));
}
CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher);
byte[] remainderBuffer = new byte[remainder];
readFully(cipherInputStream, remainderBuffer);
return cipherInputStream;
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
private static void readFully(InputStream in, byte[] buffer) throws IOException {
int offset = 0;
for (;;) {
int read = in.read(buffer, offset, buffer.length-offset);
if (read == -1) throw new IOException(PREMATURE_END_ERROR_MESSAGE);
else if (read + offset < buffer.length) offset += read;
else return;
}
}
}
@@ -1,61 +0,0 @@
package org.thoughtcrime.securesms.crypto;
import kotlin.Pair;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.Mac;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
/**
* Constructs an OutputStream that encrypts data written to it with the AttachmentSecret provided.
*
* The on-disk format is very simple, and intentionally no longer includes authentication.
*/
public class ModernEncryptingPartOutputStream {
public static Pair<byte[], OutputStream> createFor(@NonNull AttachmentSecret attachmentSecret, @NonNull File file, boolean inline)
throws IOException
{
byte[] random = new byte[32];
new SecureRandom().nextBytes(random);
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(attachmentSecret.getModernKey(), "HmacSHA256"));
FileOutputStream fileOutputStream = new FileOutputStream(file);
byte[] iv = new byte[16];
byte[] key = mac.doFinal(random);
Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(key, "AES"), new IvParameterSpec(iv));
if (inline) {
fileOutputStream.write(random);
}
return new Pair<>(random, new CipherOutputStream(fileOutputStream, cipher));
} catch (NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | NoSuchPaddingException e) {
throw new AssertionError(e);
}
}
public static long getPlaintextLength(long cipherTextLength) {
return cipherTextLength - 32;
}
}
@@ -40,6 +40,10 @@ import org.signal.core.util.UuidUtil
import org.signal.core.util.bitmaps.BitmapDecodingException
import org.signal.core.util.copyTo
import org.signal.core.util.count
import org.signal.core.util.crypto.AttachmentSecret
import org.signal.core.util.crypto.ClassicDecryptingPartInputStream
import org.signal.core.util.crypto.ModernDecryptingPartInputStream
import org.signal.core.util.crypto.ModernEncryptingPartOutputStream
import org.signal.core.util.delete
import org.signal.core.util.deleteAll
import org.signal.core.util.drain
@@ -79,10 +83,6 @@ import org.thoughtcrime.securesms.attachments.WallpaperAttachment
import org.thoughtcrime.securesms.audio.AudioHash
import org.thoughtcrime.securesms.backup.v2.ArchivedMediaObject
import org.thoughtcrime.securesms.backup.v2.exporters.ChatItemArchiveExporter
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_FILE
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.DATA_HASH_END
import org.thoughtcrime.securesms.database.AttachmentTable.Companion.PREUPLOAD_MESSAGE_ID
@@ -6,9 +6,9 @@ import androidx.annotation.VisibleForTesting
import androidx.sqlite.db.SupportSQLiteDatabase
import net.zetetic.database.sqlcipher.SQLiteOpenHelper
import org.signal.core.util.SqlUtil
import org.signal.core.util.crypto.AttachmentSecret
import org.signal.core.util.logging.Log
import org.signal.core.util.withinTransaction
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.DatabaseSecret
import org.thoughtcrime.securesms.database.helpers.SignalDatabaseMigrations
import org.thoughtcrime.securesms.database.model.AvatarPickerDatabase
@@ -5,6 +5,9 @@ import android.database.Cursor
import androidx.core.content.contentValuesOf
import org.greenrobot.eventbus.EventBus
import org.signal.core.util.StreamUtil
import org.signal.core.util.crypto.AttachmentSecret
import org.signal.core.util.crypto.ModernDecryptingPartInputStream
import org.signal.core.util.crypto.ModernEncryptingPartOutputStream
import org.signal.core.util.delete
import org.signal.core.util.exists
import org.signal.core.util.forEach
@@ -25,9 +28,6 @@ import org.signal.core.util.toInt
import org.signal.core.util.update
import org.signal.core.util.withinTransaction
import org.signal.glide.decryptableuri.DecryptableUri
import org.thoughtcrime.securesms.crypto.AttachmentSecret
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
import org.thoughtcrime.securesms.database.model.IncomingSticker
import org.thoughtcrime.securesms.database.model.StickerPackId
import org.thoughtcrime.securesms.database.model.StickerPackRecord
@@ -11,6 +11,7 @@ import org.signal.core.util.CoreUtilDependencies
import org.signal.core.util.billing.BillingApi
import org.signal.core.util.concurrent.DeadlockDetector
import org.signal.core.util.concurrent.LatestValueObservable
import org.signal.core.util.contentproviders.BlobProvider
import org.signal.core.util.orNull
import org.signal.core.util.resettableLazy
import org.signal.glide.SignalGlideDependencies
@@ -261,6 +262,11 @@ object AppDependencies {
provider.provideBillingApi()
}
@JvmStatic
val blobs: BlobProvider by lazy {
provider.provideBlobs()
}
private val _webSocketObserver: BehaviorSubject<WebSocketConnectionState> = BehaviorSubject.create()
/**
@@ -511,5 +517,6 @@ object AppDependencies {
fun provideDonationsApi(authWebSocket: SignalWebSocket.AuthenticatedWebSocket, unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): DonationsApi
fun provideSvrBApi(libSignalNetwork: Network): SvrBApi
fun provideKeyTransparencyApi(unauthWebSocket: SignalWebSocket.UnauthenticatedWebSocket): KeyTransparencyApi
fun provideBlobs(): BlobProvider
}
}
@@ -21,6 +21,7 @@ import org.signal.core.util.UptimeSleepTimer;
import org.signal.core.util.billing.BillingApi;
import org.signal.core.util.concurrent.DeadlockDetector;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.contentproviders.BlobProvider;
import org.signal.libsignal.net.Network;
import org.signal.libsignal.protocol.SignalProtocolAddress;
import org.signal.libsignal.zkgroup.GenericServerPublicParams;
@@ -46,6 +47,7 @@ import org.signal.network.service.MessageService;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.components.TypingStatusRepository;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
import org.thoughtcrime.securesms.crypto.storage.SignalBaseIdentityKeyStore;
import org.thoughtcrime.securesms.crypto.storage.SignalIdentityKeyStore;
@@ -635,6 +637,10 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
return new KeyTransparencyApi(unauthWebSocket);
}
@Override public @NotNull BlobProvider provideBlobs() {
return new BlobProvider(context, AppAttachmentSecretStore.INSTANCE);
}
@VisibleForTesting
static class DynamicCredentialsProvider implements CredentialsProvider {
@@ -9,6 +9,7 @@ import net.zetetic.database.sqlcipher.SQLiteDatabase;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.NewDeviceRestoreStatus;
import org.signal.devicetransfer.ServerTask;
@@ -16,12 +17,11 @@ import org.thoughtcrime.securesms.AppInitialization;
import org.thoughtcrime.securesms.backup.BackupEvent;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.backup.FullBackupImporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.RemoteConfig;
import java.io.IOException;
import java.io.InputStream;
@@ -52,7 +52,7 @@ public final class NewDeviceServerTask implements ServerTask {
BackupPassphrase.set(context, passphrase);
FullBackupImporter.importFile(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret(),
database,
inputStream,
passphrase,
@@ -7,15 +7,15 @@ import androidx.annotation.NonNull;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.core.util.logging.Log;
import org.signal.devicetransfer.ClientTask;
import org.thoughtcrime.securesms.backup.BackupEvent;
import org.thoughtcrime.securesms.backup.FullBackupExporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.DeviceTransferBlockingInterceptor;
import org.thoughtcrime.securesms.util.RemoteConfig;
import java.io.IOException;
import java.io.OutputStream;
@@ -43,7 +43,7 @@ final class OldDeviceClientTask implements ClientTask {
String passphrase = SignalStore.account().getAccountEntropyPool().getValue();
FullBackupExporter.transfer(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret(),
SignalDatabase.getBackupDatabase(),
outputStream,
passphrase);
@@ -12,10 +12,11 @@ import okio.HashingSink
import okio.blackholeSink
import okio.buffer
import okio.source
import org.signal.core.util.crypto.AttachmentSecretProvider
import org.signal.core.util.crypto.ModernDecryptingPartInputStream
import org.signal.core.util.crypto.ModernEncryptingPartOutputStream
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
import org.thoughtcrime.securesms.mms.PartAuthority
import java.io.File
import java.io.IOException
@@ -60,12 +61,12 @@ private fun Context.getJumboFile(versionUuid: UUID): File = File(File(getEmojiDi
private fun getFilesUri(name: String, format: String): Uri = PartAuthority.getEmojiUri(name)
private fun getOutputStream(context: Context, outputFile: File): OutputStream {
val attachmentSecret = AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret
val attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore).orCreateAttachmentSecret
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second
}
private fun getInputStream(context: Context, inputFile: File): InputStream {
val attachmentSecret = AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret
val attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore).orCreateAttachmentSecret
return ModernDecryptingPartInputStream.createFor(attachmentSecret, inputFile, 0)
}
@@ -11,7 +11,6 @@ import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.net.ContentProxySelector;
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -66,10 +65,10 @@ final class GiphyMp4Repository {
try (Response response = client.newCall(request).execute()) {
if (response.code() >= 200 && response.code() < 300) {
return BlobProvider.getInstance()
.forData(response.body().byteStream(), response.body().contentLength())
.withMimeType(mime)
.createForSingleSessionOnDisk(AppDependencies.getApplication());
return AppDependencies.getBlobs()
.forData(response.body().byteStream(), response.body().contentLength())
.withMimeType(mime)
.createForSingleSessionOnDisk(AppDependencies.getApplication());
} else {
throw new IOException("Unexpected response code: " + response.code());
}
@@ -12,28 +12,27 @@ import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.models.media.Media;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Fragment;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4SaveResult;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ViewModel;
import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView;
import org.signal.core.models.media.Media;
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
import org.thoughtcrime.securesms.mms.SlideFactory;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
import java.util.Collections;
import java.util.Objects;
import java.util.Optional;
public class GiphyActivity extends PassphraseRequiredActivity implements KeyboardPageSearchView.Callbacks {
@@ -120,7 +119,7 @@ public class GiphyActivity extends PassphraseRequiredActivity implements Keyboar
}
private void handleGiphyMp4SuccessfulResult(@NonNull GiphyMp4SaveResult.Success success) {
SlideFactory.MediaType mediaType = Objects.requireNonNull(SlideFactory.MediaType.from(BlobProvider.getMimeType(success.getBlobUri())));
SlideFactory.MediaType mediaType = Objects.requireNonNull(SlideFactory.MediaType.from(AppDependencies.getBlobs().getMimeType(success.getBlobUri())));
String mimeType = MediaUtil.getMimeType(this, success.getBlobUri());
if (mimeType == null) {
mimeType = mediaType.toFallbackMimeType();
@@ -17,7 +17,7 @@ import org.signal.core.util.logging.Log;
import org.signal.glide.common.io.GlideStreamConfig;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.signal.core.util.contentproviders.BlobProvider;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -18,23 +18,26 @@ import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
import org.signal.apng.ApngDecoder;
import org.signal.blurhash.BlurHash;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
import org.signal.core.util.crypto.AttachmentSecret;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.glide.blurhash.BlurHashModelLoader;
import org.signal.glide.blurhash.BlurHashResourceDecoder;
import org.signal.glide.common.io.InputStreamFactory;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.signal.glide.decryptableuri.DecryptableUriStreamLoader;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
import org.thoughtcrime.securesms.badges.load.BadgeLoader;
import org.thoughtcrime.securesms.badges.load.GiftBadgeModel;
import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoLoader;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.glide.cache.ApngDrawableTranscoder;
import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder;
import org.thoughtcrime.securesms.glide.cache.ApngInputStreamFactoryResourceDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.ByteBufferApngDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngResourceEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
@@ -47,15 +50,13 @@ import org.thoughtcrime.securesms.glide.cache.StreamBitmapDecoder;
import org.thoughtcrime.securesms.glide.cache.StreamFactoryApngDecoder;
import org.thoughtcrime.securesms.glide.cache.StreamFactoryGifDecoder;
import org.thoughtcrime.securesms.glide.cache.WebpSanDecoder;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.signal.glide.decryptableuri.DecryptableUriStreamLoader;
import org.thoughtcrime.securesms.mms.RegisterGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader;
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
import org.thoughtcrime.securesms.util.ConversationShortcutPhoto;
import org.thoughtcrime.securesms.util.RemoteConfig;
import java.io.File;
import java.io.InputStream;
@@ -69,7 +70,7 @@ public class SignalGlideComponents implements RegisterGlideComponents {
@Override
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret();
byte[] secret = attachmentSecret.getModernKey();
registry.prepend(File.class, File.class, UnitModelLoader.Factory.getInstance());
@@ -10,16 +10,22 @@ import androidx.annotation.WorkerThread;
import androidx.media3.common.MimeTypes;
import org.greenrobot.eventbus.EventBus;
import org.signal.core.models.database.AttachmentId;
import org.signal.core.models.media.TransformProperties;
import org.signal.core.util.MemoryFileDescriptor.MemoryFileException;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.signal.core.util.crypto.AttachmentSecret;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.core.util.crypto.ModernDecryptingPartInputStream;
import org.signal.core.util.crypto.ModernEncryptingPartOutputStream;
import org.signal.core.util.logging.Log;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.signal.mediasend.MediaConstraints;
import org.signal.mediasend.SentMediaQuality;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.signal.core.models.media.TransformProperties;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.events.PartProgressEvent;
@@ -27,18 +33,13 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobmanager.persistence.JobSpec;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.signal.mediasend.MediaConstraints;
import org.thoughtcrime.securesms.mms.MediaStream;
import org.thoughtcrime.securesms.mms.MmsException;
import org.signal.mediasend.SentMediaQuality;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.thoughtcrime.securesms.service.AttachmentProgressService;
import org.thoughtcrime.securesms.transport.UndeliverableMessageException;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.thoughtcrime.securesms.util.ImageCompressionUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.signal.core.util.MemoryFileDescriptor.MemoryFileException;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.video.StreamingTranscoder;
import org.thoughtcrime.securesms.video.TranscoderOptions;
@@ -263,7 +264,7 @@ public final class AttachmentCompressionJob extends BaseJob {
if (transcoder.isTranscodeRequired()) {
Log.i(TAG, "Compressing with streaming muxer");
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret();
File file = AttachmentTable.newDataFile(context);
file.deleteOnExit();
@@ -48,7 +48,6 @@ import org.thoughtcrime.securesms.logsubmit.SubmitDebugLogActivity
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.notifications.NotificationChannels
import org.thoughtcrime.securesms.notifications.NotificationIds
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.storage.StorageSyncHelper
import org.thoughtcrime.securesms.util.MediaUtil
@@ -463,9 +462,9 @@ class BackupMessagesJob private constructor(
}
}
BlobProvider.getInstance().clearTemporaryBackupsDirectory(AppDependencies.application)
AppDependencies.blobs.clearTemporaryBackupsDirectory(AppDependencies.application)
val tempBackupFile = BlobProvider.getInstance().forTemporaryBackup(AppDependencies.application)
val tempBackupFile = AppDependencies.blobs.forTemporaryBackup(AppDependencies.application)
val outputStream = FileOutputStream(tempBackupFile)
val backupKey = SignalStore.backup.messageBackupKey
@@ -7,11 +7,11 @@ import org.thoughtcrime.securesms.avatar.Avatar
import org.thoughtcrime.securesms.avatar.AvatarRenderer
import org.thoughtcrime.securesms.avatar.Avatars
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.profiles.ProfileName
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.transport.RetryLaterException
import java.util.concurrent.CountDownLatch
@@ -92,7 +92,7 @@ class CreateReleaseChannelJob private constructor(parameters: Parameters) : Base
Avatars.ColorPair(ContextCompat.getColor(context, R.color.notification_background_ultramarine), ContextCompat.getColor(context, R.color.core_white), "")
),
onAvatarRendered = { media ->
AvatarHelper.setAvatar(context, id, BlobProvider.getInstance().getStream(context, media.uri))
AvatarHelper.setAvatar(context, id, AppDependencies.blobs.getStream(context, media.uri))
SignalDatabase.recipients.setProfileAvatar(id, "local")
latch.countDown()
},
@@ -3,10 +3,10 @@ package org.thoughtcrime.securesms.jobs
import okhttp3.ResponseBody
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.Job
import org.thoughtcrime.securesms.jobmanager.JsonJobData
import org.thoughtcrime.securesms.jobmanager.impl.AutoDownloadEmojiConstraint
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.s3.S3
import org.thoughtcrime.securesms.transport.RetryLaterException
import java.io.IOException
@@ -44,7 +44,7 @@ class FetchRemoteMegaphoneImageJob(parameters: Parameters, private val uuid: Str
S3.getObject(imageUrl).use { response ->
val body: ResponseBody? = response.body
if (body != null) {
val uri = BlobProvider.getInstance()
val uri = AppDependencies.blobs
.forData(body.byteStream(), body.contentLength())
.createForMultipleSessionsOnDisk(context)
@@ -9,7 +9,11 @@ import androidx.annotation.Nullable;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.ui.permissions.Permissions;
import org.signal.core.ui.util.StorageUtil;
import org.signal.core.util.NoExternalStorageException;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.backup.BackupEvent;
@@ -17,18 +21,15 @@ import org.thoughtcrime.securesms.backup.BackupFileIOError;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.backup.BackupVerifier;
import org.thoughtcrime.securesms.backup.FullBackupExporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.signal.core.util.NoExternalStorageException;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.service.GenericForegroundService;
import org.thoughtcrime.securesms.service.NotificationController;
import org.thoughtcrime.securesms.util.BackupUtil;
import org.signal.core.ui.util.StorageUtil;
import java.io.File;
import java.io.FileInputStream;
@@ -143,7 +144,7 @@ public final class LocalBackupJob extends BaseJob {
try {
Stopwatch stopwatch = new Stopwatch("backup-export");
BackupEvent finishedEvent = FullBackupExporter.export(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret(),
SignalDatabase.getBackupDatabase(),
tempFile,
backupPassword,
@@ -12,16 +12,17 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.androidx.DocumentFileUtil;
import org.signal.core.util.androidx.DocumentFileUtil.OperationResult;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.backup.BackupEvent;
import org.thoughtcrime.securesms.backup.BackupFileIOError;
import org.thoughtcrime.securesms.backup.BackupPassphrase;
import org.thoughtcrime.securesms.backup.BackupVerifier;
import org.signal.core.util.androidx.DocumentFileUtil;
import org.signal.core.util.androidx.DocumentFileUtil.OperationResult;
import org.thoughtcrime.securesms.backup.FullBackupExporter;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
@@ -130,7 +131,7 @@ public final class LocalBackupJobApi29 extends BaseJob {
try {
Stopwatch stopwatch = new Stopwatch("backup-export");
BackupEvent finishedEvent = FullBackupExporter.export(context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret(),
SignalDatabase.getBackupDatabase(),
temporaryFile,
backupPassword,
@@ -10,7 +10,6 @@ import org.thoughtcrime.securesms.jobmanager.JsonJobData
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.net.NotPushRegisteredException
import org.thoughtcrime.securesms.profiles.AvatarHelper
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.whispersystems.signalservice.api.crypto.AttachmentCipherInputStream.IntegrityCheck
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
@@ -58,7 +57,7 @@ class MultiDeviceContactSyncJob(parameters: Parameters, private val attachmentPo
val contactAttachment: SignalServiceAttachmentPointer = AttachmentPointerUtil.createSignalAttachmentPointer(attachmentPointer)
try {
val contactsFile: File = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(context)
val contactsFile: File = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(context)
AppDependencies.signalServiceMessageReceiver
.retrieveAttachment(contactAttachment, contactsFile, MAX_ATTACHMENT_SIZE, IntegrityCheck.forEncryptedDigest(contactAttachment.digest.get()))
.use(this::processContactFile)
@@ -10,15 +10,15 @@ import android.provider.ContactsContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.ui.permissions.Permissions;
import org.signal.core.util.AppForegroundObserver;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.IdentityKey;
import org.signal.libsignal.zkgroup.profiles.ProfileKey;
import org.thoughtcrime.securesms.conversation.colors.ChatColorsMapper;
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil;
import org.signal.network.exceptions.PushNetworkException;
import org.signal.network.service.CdnService;
import org.thoughtcrime.securesms.database.RecipientTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.database.model.IdentityRecord;
import org.signal.network.service.CdnService;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobmanager.Job;
import org.thoughtcrime.securesms.jobmanager.JsonJobData;
@@ -26,17 +26,13 @@ import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
import org.thoughtcrime.securesms.jobmanager.impl.SealedSenderConstraint;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.net.NotPushRegisteredException;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.signal.core.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
import org.whispersystems.signalservice.api.crypto.AttachmentCipherStreamUtil;
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentStream;
import org.whispersystems.signalservice.api.messages.multidevice.ContactsMessage;
@@ -46,9 +42,9 @@ import org.whispersystems.signalservice.api.messages.multidevice.DeviceContactsO
import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
import org.whispersystems.signalservice.api.messages.multidevice.VerifiedMessage;
import org.whispersystems.signalservice.api.push.SignalServiceAddress;
import org.signal.network.exceptions.PushNetworkException;
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
import org.whispersystems.signalservice.api.util.InvalidNumberException;
import org.whispersystems.signalservice.internal.crypto.PaddingInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
@@ -177,10 +173,10 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
out.close();
updateUri = writeDetails.getUri();
long length = BlobProvider.getInstance().calculateFileSize(context, updateUri);
long length = AppDependencies.getBlobs().calculateFileSize(context, updateUri);
sendUpdate(AppDependencies.getSignalServiceMessageSender(),
BlobProvider.getInstance().getStream(context, updateUri),
AppDependencies.getBlobs().getStream(context, updateUri),
length,
false);
@@ -188,7 +184,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
Log.w(TAG, e);
} finally {
if (updateUri != null) {
BlobProvider.getInstance().delete(context, updateUri);
AppDependencies.getBlobs().delete(context, updateUri);
}
}
}
@@ -253,17 +249,17 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
updateUri = writeDetails.getUri();
long length = BlobProvider.getInstance().calculateFileSize(context, updateUri);
long length = AppDependencies.getBlobs().calculateFileSize(context, updateUri);
sendUpdate(AppDependencies.getSignalServiceMessageSender(),
BlobProvider.getInstance().getStream(context, updateUri),
AppDependencies.getBlobs().getStream(context, updateUri),
length,
true);
} catch(InterruptedException e) {
Log.w(TAG, e);
} finally {
if (updateUri != null) {
BlobProvider.getInstance().delete(context, updateUri);
AppDependencies.getBlobs().delete(context, updateUri);
}
}
}
@@ -377,7 +373,7 @@ public class MultiDeviceContactUpdateJob extends BaseJob {
private @NonNull WriteDetails createTempFile() throws IOException {
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
InputStream inputStream = new ParcelFileDescriptor.AutoCloseInputStream(pipe[0]);
Future<Uri> futureUri = BlobProvider.getInstance()
Future<Uri> futureUri = AppDependencies.getBlobs()
.forData(inputStream, 0)
.withFileName("multidevice-contact-update")
.createForSingleSessionOnDiskAsync(context);
@@ -25,7 +25,6 @@ import org.thoughtcrime.securesms.jobmanager.JsonJobData
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.notifications.v2.ConversationId
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel
import org.thoughtcrime.securesms.s3.S3
@@ -361,7 +360,7 @@ class RetrieveRemoteAnnouncementsJob private constructor(private val force: Bool
for ((uuid, megaphone) in megaphonesToDelete) {
SignalDatabase.remoteMegaphones.clear(uuid)
if (megaphone.imageUri != null) {
BlobProvider.getInstance().delete(context, megaphone.imageUri)
AppDependencies.blobs.delete(context, megaphone.imageUri)
}
}
}
@@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.linkdevice.LinkDeviceRepository.createAndUploadArchive
import org.thoughtcrime.securesms.net.SignalNetwork
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
import org.whispersystems.signalservice.api.link.LinkedDeviceVerificationCodeResponse
import org.whispersystems.signalservice.api.link.TransferArchiveError
@@ -281,7 +280,7 @@ object LinkDeviceRepository {
fun createAndUploadArchive(ephemeralMessageBackupKey: MessageBackupKey, deviceId: Int, deviceRegistrationId: Int, cancellationSignal: () -> Boolean): LinkUploadArchiveResult {
Log.d(TAG, "[createAndUploadArchive] Beginning process.")
val stopwatch = Stopwatch("link-archive")
val tempBackupFile = BlobProvider.getInstance().forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
val tempBackupFile = AppDependencies.blobs.forNonAutoEncryptingSingleSessionOnDisk(AppDependencies.application)
val outputStream = FileOutputStream(tempBackupFile)
try {
@@ -12,8 +12,10 @@ import androidx.core.util.Consumer;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.signal.core.util.ByteUnit;
import org.signal.core.util.Hex;
import org.signal.core.util.Result;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.signal.libsignal.protocol.InvalidMessageException;
@@ -42,15 +44,12 @@ import org.thoughtcrime.securesms.net.LinkPreviewRedirectValidationInterceptor;
import org.thoughtcrime.securesms.net.RequestController;
import org.thoughtcrime.securesms.net.UserAgentInterceptor;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.service.webrtc.links.CallLinkCredentials;
import org.thoughtcrime.securesms.service.webrtc.links.ReadCallLinkResult;
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
import org.thoughtcrime.securesms.stickers.StickerUrl;
import org.thoughtcrime.securesms.util.AvatarUtil;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.signal.core.util.ByteUnit;
import org.thoughtcrime.securesms.util.ImageCompressionUtil;
import org.thoughtcrime.securesms.util.LinkUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -68,11 +67,10 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import kotlin.Pair;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.disposables.Disposable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.Pair;
import okhttp3.CacheControl;
import okhttp3.Call;
import okhttp3.OkHttpClient;
@@ -467,7 +465,7 @@ public class LinkPreviewRepository {
int height,
@NonNull String contentType) {
Uri uri = BlobProvider.getInstance().forData(bytes).createForSingleSessionInMemory();
Uri uri = AppDependencies.getBlobs().forData(bytes).createForSingleSessionInMemory();
return new UriAttachment(uri,
contentType,
@@ -12,6 +12,7 @@ import androidx.annotation.WorkerThread;
import org.json.JSONException;
import org.json.JSONObject;
import org.signal.core.util.Stopwatch;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
@@ -21,10 +22,8 @@ import org.signal.debuglogsviewer.DebugLogsViewer;
import org.thoughtcrime.securesms.database.LogDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.net.StandardUserAgentInterceptor;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.signal.core.util.Stopwatch;
import java.io.IOException;
import java.io.OutputStream;
@@ -39,8 +38,8 @@ import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.GZIPOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -190,10 +189,10 @@ public class SubmitDebugLogRepository {
try {
ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
Future<Uri> futureUri = BlobProvider.getInstance()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType("application/gzip")
.createForSingleSessionOnDiskAsync(context);
Future<Uri> futureUri = AppDependencies.getBlobs()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType("application/gzip")
.createForSingleSessionOnDiskAsync(context);
OutputStream gzipOutput = new GZIPOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
@@ -224,12 +223,12 @@ public class SubmitDebugLogRepository {
}
@Override public long contentLength() {
return BlobProvider.getInstance().calculateFileSize(context, gzipUri);
return AppDependencies.getBlobs().calculateFileSize(context, gzipUri);
}
@Override
public void writeTo(@NonNull BufferedSink sink) throws IOException {
Source source = Okio.source(BlobProvider.getInstance().getStream(context, gzipUri));
Source source = Okio.source(AppDependencies.getBlobs().getStream(context, gzipUri));
sink.writeAll(source);
}
});
@@ -237,7 +236,7 @@ public class SubmitDebugLogRepository {
stopwatch.split("upload");
stopwatch.stop(TAG);
BlobProvider.getInstance().delete(context, gzipUri);
AppDependencies.getBlobs().delete(context, gzipUri);
return Optional.of(logUrl);
} catch (IOException | RuntimeException | ExecutionException | InterruptedException e) {
@@ -264,10 +263,10 @@ public class SubmitDebugLogRepository {
Stopwatch stopwatch = new Stopwatch("log-upload");
ParcelFileDescriptor[] fds = ParcelFileDescriptor.createPipe();
Future<Uri> futureUri = BlobProvider.getInstance()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType("application/gzip")
.createForSingleSessionOnDiskAsync(context);
Future<Uri> futureUri = AppDependencies.getBlobs()
.forData(new ParcelFileDescriptor.AutoCloseInputStream(fds[0]), 0)
.withMimeType("application/gzip")
.createForSingleSessionOnDiskAsync(context);
OutputStream gzipOutput = new GZIPOutputStream(new ParcelFileDescriptor.AutoCloseOutputStream(fds[1]));
@@ -297,12 +296,12 @@ public class SubmitDebugLogRepository {
}
@Override public long contentLength() {
return BlobProvider.getInstance().calculateFileSize(context, gzipUri);
return AppDependencies.getBlobs().calculateFileSize(context, gzipUri);
}
@Override
public void writeTo(@NonNull BufferedSink sink) throws IOException {
Source source = Okio.source(BlobProvider.getInstance().getStream(context, gzipUri));
Source source = Okio.source(AppDependencies.getBlobs().getStream(context, gzipUri));
sink.writeAll(source);
}
});
@@ -310,7 +309,7 @@ public class SubmitDebugLogRepository {
stopwatch.split("upload");
stopwatch.stop(TAG);
BlobProvider.getInstance().delete(context, gzipUri);
AppDependencies.getBlobs().delete(context, gzipUri);
return Optional.of(logUrl);
} catch (IOException | RuntimeException | ExecutionException | InterruptedException e) {
@@ -31,12 +31,12 @@ import com.google.android.gms.maps.SupportMapFragment;
import com.google.android.gms.maps.model.LatLng;
import com.google.android.gms.maps.model.MapStyleOptions;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.location.SignalMapView;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -196,7 +196,7 @@ public final class PlacePickerActivity extends AppCompatActivity {
public void onSuccess(Bitmap result) {
dismissibleDialog.dismiss();
byte[] blob = BitmapUtil.toByteArray(result);
Uri uri = BlobProvider.getInstance()
Uri uri = AppDependencies.getBlobs()
.forData(blob)
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionInMemory();
@@ -4,9 +4,10 @@ import android.content.Context
import android.net.Uri
import androidx.annotation.RequiresApi
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.attachments
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.mms.PartUriParser
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.video.MediaDataSourceProvider
import org.thoughtcrime.securesms.video.interfaces.MediaInput
import org.thoughtcrime.securesms.video.interfaces.MediaInputFactory
import org.thoughtcrime.securesms.video.videoconverter.mediadatasource.MediaDataSourceMediaInput
@@ -19,8 +20,8 @@ import java.io.IOException
object DecryptableUriMediaInput : MediaInputFactory {
@Throws(IOException::class)
override fun createForUri(context: Context, uri: Uri): MediaInput {
if (BlobProvider.isAuthority(uri)) {
return MediaDataSourceMediaInput(BlobProvider.getInstance().getMediaDataSource(context, uri))
if (AppDependencies.blobs.isAuthority(uri)) {
return MediaDataSourceMediaInput(MediaDataSourceProvider.getMediaDataSource(context, uri))
}
return if (PartAuthority.isLocalUri(uri)) {
createForAttachmentUri(uri)
@@ -20,10 +20,11 @@ import org.signal.mediasend.MediaConstraints;
import org.signal.mediasend.CameraFragment;
import org.signal.mediasend.capture.CameraXFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.mediasend.v2.gallery.MediaGalleryFragment;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.signal.core.util.contentproviders.BlobProvider;
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
import org.thoughtcrime.securesms.util.MediaUtil;
@@ -79,10 +80,10 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
@Override
public void onImageCaptured(@NonNull byte[] data, int width, int height) {
Uri blobUri = BlobProvider.getInstance()
.forData(data)
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionInMemory();
Uri blobUri = AppDependencies.getBlobs()
.forData(data)
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionInMemory();
onMediaSelected(new Media(blobUri,
MediaUtil.IMAGE_JPEG,
@@ -13,8 +13,9 @@ import org.signal.core.util.StreamUtil;
import org.signal.core.util.logging.Log;
import org.signal.core.models.media.Media;
import org.signal.imageeditor.core.model.EditorModel;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.fonts.FontTypefaceProvider;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.signal.core.util.contentproviders.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import java.io.ByteArrayOutputStream;
@@ -45,10 +46,10 @@ public final class ImageEditorModelRenderMediaTransform implements MediaTransfor
try {
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
Uri uri = BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionOnDisk(context);
Uri uri = AppDependencies.getBlobs()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionOnDisk(context);
return new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, false, false, media.getBucketId(), media.getCaption(), null, null);
} catch (IOException e) {
@@ -39,7 +39,6 @@ import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.mms.SlideDeck
import org.thoughtcrime.securesms.mms.VideoSlide
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
@@ -237,8 +236,8 @@ class MediaSelectionRepository(context: Context) {
fun deleteBlobs(media: List<Media>) {
media
.map(Media::uri)
.filter(BlobProvider::isAuthority)
.forEach { BlobProvider.getInstance().delete(context, it) }
.filter { AppDependencies.blobs.isAuthority(it) }
.forEach { AppDependencies.blobs.delete(context, it) }
}
fun cleanUp(selectedMedia: List<Media>) {
@@ -21,6 +21,7 @@ import io.reactivex.rxjava3.subjects.PublishSubject
import io.reactivex.rxjava3.subjects.Subject
import org.signal.core.models.media.Media
import org.signal.core.util.Util
import org.signal.core.util.contentproviders.BlobProvider
import org.signal.core.util.getParcelableArrayListCompat
import org.signal.core.util.getParcelableCompat
import org.signal.core.util.logging.Log
@@ -31,9 +32,9 @@ import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.conversation.MessageSendType
import org.thoughtcrime.securesms.conversation.MessageStyler
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult
import org.thoughtcrime.securesms.mms.PushMediaConstraints
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
import org.thoughtcrime.securesms.stories.Stories
@@ -472,7 +473,7 @@ class MediaSelectionViewModel(
editorStates.forEach { it.writeToParcel(parcel, 0) }
val serializedEditorState: ByteArray = parcel.marshall()
parcel.recycle()
val blobUri = BlobProvider.getInstance().forData(serializedEditorState).createForSingleUseInMemory()
val blobUri = AppDependencies.blobs.forData(serializedEditorState).createForSingleUseInMemory()
outState.putParcelable(STATE_EDITORS, blobUri)
}
}
@@ -496,7 +497,7 @@ class MediaSelectionViewModel(
val cameraFirstCapture: Media? = savedInstanceState.getParcelableCompat(STATE_CAMERA_FIRST_CAPTURE, Media::class.java)
val editorCount: Int = savedInstanceState.getInt(STATE_EDITOR_COUNT, 0)
val blobUri: Uri? = savedInstanceState.getParcelableCompat(STATE_EDITORS, Uri::class.java)
val blobProvider: BlobProvider = BlobProvider.getInstance()
val blobProvider: BlobProvider = AppDependencies.blobs
val editorStates: List<Bundle> = if (editorCount > 0 && blobUri != null && blobProvider.hasStream(context, blobUri)) {
val accumulator: MutableList<Bundle> = mutableListOf()
val blob: ByteArray = ByteStreams.toByteArray(blobProvider.getStream(context, blobUri))
@@ -10,8 +10,9 @@ import androidx.annotation.WorkerThread
import org.signal.core.models.media.Media
import org.signal.core.util.CursorUtil
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.contentproviders.BlobProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mediasend.MediaRepository
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.video.videoconverter.utils.VideoConstants
import java.io.FileDescriptor
@@ -72,7 +73,7 @@ class MediaCaptureRepository(context: Context) {
return try {
val data: T = dataSupplier()
val length: Long = getLength(data)
val uri: Uri = createBlobBuilder(BlobProvider.getInstance(), data, length)
val uri: Uri = createBlobBuilder(AppDependencies.blobs, data, length)
.withMimeType(mimeType)
.createForSingleSessionOnDisk(context)
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.contacts.paged.ContactSearchKey
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.StoryType
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.fonts.TextFont
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.keyvalue.StorySend
@@ -18,7 +19,6 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview
import org.thoughtcrime.securesms.mediasend.v2.UntrustedRecords
import org.thoughtcrime.securesms.mediasend.v2.text.TextStoryPostCreationState
import org.thoughtcrime.securesms.mms.OutgoingMessage
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.stories.Stories
import java.io.ByteArrayOutputStream
@@ -32,7 +32,7 @@ class TextStoryPostSendRepository {
val outputStream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
bitmap.recycle()
BlobProvider.getInstance().forData(outputStream.toByteArray()).createForSingleSessionInMemory()
AppDependencies.blobs.forData(outputStream.toByteArray()).createForSingleSessionInMemory()
}.subscribeOn(Schedulers.computation())
}
@@ -40,7 +40,6 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionRepository
import org.thoughtcrime.securesms.mediasend.v2.MediaValidator
import org.thoughtcrime.securesms.mms.PartAuthority
import org.thoughtcrime.securesms.mms.PushMediaConstraints
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment
@@ -96,8 +95,8 @@ object MediaSendV3Repository : MediaSendRepository {
override suspend fun deleteBlobs(media: List<Media>) {
media
.map(Media::uri)
.filter(BlobProvider::isAuthority)
.forEach { BlobProvider.getInstance().delete(appContext, it) }
.filter { AppDependencies.blobs.isAuthority(it) }
.forEach { AppDependencies.blobs.delete(appContext, it) }
}
override suspend fun send(request: SendRequest): SendResult = withContext(Dispatchers.IO) {
@@ -19,7 +19,6 @@ import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord
import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord.ActionId
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.util.LocaleRemoteConfig
import org.thoughtcrime.securesms.util.RemoteConfig
@@ -50,7 +49,7 @@ object RemoteMegaphoneRepository {
SignalExecutors.BOUNDED_IO.execute {
db.markFinished(remote.uuid)
if (remote.imageUri != null) {
BlobProvider.getInstance().delete(context, remote.imageUri)
AppDependencies.blobs.delete(context, remote.imageUri)
}
}
}
@@ -11,7 +11,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
import java.io.File;
/**
* We moved files stored by {@link org.thoughtcrime.securesms.providers.BlobProvider} from the cache
* We moved files stored by {@link org.signal.core.util.contentproviders.BlobProvider} from the cache
* into internal storage, so we gotta move any existing multi-session files.
*/
public class BlobStorageLocationMigrationJob extends MigrationJob {
@@ -37,13 +37,17 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.RequestManager;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.ui.permissions.Permissions;
import org.signal.core.ui.view.Stub;
import org.signal.core.util.ExpiringProfileCredentialUtil;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.signal.core.util.concurrent.ListenableFuture;
import org.signal.core.util.concurrent.ListenableFuture.Listener;
import org.signal.core.util.concurrent.SettableFuture;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.signal.core.ui.view.Stub;
import org.signal.core.util.permissions.PermissionCompat;
import org.signal.mediasend.MediaConstraints;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AudioView;
@@ -55,6 +59,7 @@ import org.thoughtcrime.securesms.components.location.SignalPlace;
import org.thoughtcrime.securesms.conversation.MessageSendType;
import org.thoughtcrime.securesms.database.MediaTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
@@ -67,19 +72,14 @@ import org.thoughtcrime.securesms.payments.create.CreatePaymentFragmentArgs;
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
import org.thoughtcrime.securesms.payments.preferences.RecipientHasNotEnabledPaymentsDialog;
import org.thoughtcrime.securesms.payments.preferences.model.PayeeParcelable;
import org.signal.core.util.permissions.PermissionCompat;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.ProfileUtil;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.signal.core.util.ExpiringProfileCredentialUtil;
import java.io.IOException;
import java.util.Collections;
@@ -174,14 +174,14 @@ public class AttachmentManager {
}
private void cleanup(final @Nullable Uri uri) {
if (uri != null && BlobProvider.isAuthority(uri)) {
if (uri != null && AppDependencies.getBlobs().isAuthority(uri)) {
Log.d(TAG, "cleaning up " + uri);
BlobProvider.getInstance().delete(context, uri);
AppDependencies.getBlobs().delete(context, uri);
}
}
private void markGarbage(@Nullable Uri uri) {
if (uri != null && BlobProvider.isAuthority(uri)) {
if (uri != null && AppDependencies.getBlobs().isAuthority(uri)) {
Log.d(TAG, "Marking garbage that needs cleaning: " + uri);
garbage.add(uri);
}
@@ -215,10 +215,10 @@ public class AttachmentManager {
@Override
public void onSuccess(@NonNull Bitmap result) {
byte[] blob = BitmapUtil.toByteArray(result);
Uri uri = BlobProvider.getInstance()
.forData(blob)
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionInMemory();
Uri uri = AppDependencies.getBlobs()
.forData(blob)
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionInMemory();
LocationSlide locationSlide = new LocationSlide(context, uri, blob.length, place);
ThreadUtil.runOnMain(() -> {
@@ -241,7 +241,7 @@ public class AttachmentManager {
attachmentViewStub.get().setVisibility(View.VISIBLE);
removableMediaView.display(mapView, false);
LocationSlide locationSlide = new LocationSlide(context, thumbnailUri, BlobProvider.getFileSize(thumbnailUri), place);
LocationSlide locationSlide = new LocationSlide(context, thumbnailUri, AppDependencies.getBlobs().getFileSize(thumbnailUri), place);
setSlide(locationSlide);
attachmentListener.onAttachmentChanged();
}
@@ -12,14 +12,14 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.provider.DocumentsContractCompat;
import org.signal.core.models.database.AttachmentId;
import org.signal.core.models.media.TransformProperties;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.avatar.AvatarPickerStorage;
import org.signal.core.models.media.TransformProperties;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.emoji.EmojiFiles;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.providers.PartProvider;
import java.io.FileNotFoundException;
@@ -58,7 +58,7 @@ public class PartAuthority {
uriMatcher.addURI(AUTHORITY, "wallpaper/*", WALLPAPER_ROW);
uriMatcher.addURI(AUTHORITY, "emoji/*", EMOJI_ROW);
uriMatcher.addURI(AUTHORITY, "avatar_picker/*", AVATAR_PICKER_ROW);
uriMatcher.addURI(BlobProvider.AUTHORITY, BlobProvider.PATH, BLOB_ROW);
uriMatcher.addURI(AppDependencies.getBlobs().getAuthority(), AppDependencies.getBlobs().PATH, BLOB_ROW);
}
public static InputStream getAttachmentThumbnailStream(@NonNull Context context, @NonNull Uri uri)
@@ -75,7 +75,7 @@ public class PartAuthority {
switch (match) {
case PART_ROW: return SignalDatabase.attachments().getAttachmentStream(new PartUriParser(uri).getPartId(), 0);
case STICKER_ROW: return SignalDatabase.stickers().getStickerStream(ContentUris.parseId(uri));
case BLOB_ROW: return BlobProvider.getInstance().getStream(context, uri);
case BLOB_ROW: return AppDependencies.getBlobs().getStream(context, uri);
case EMOJI_ROW: return EmojiFiles.openForReading(context, getEmojiFilename(uri));
case AVATAR_PICKER_ROW: return AvatarPickerStorage.read(context, getAvatarPickerFilename(uri));
case THUMBNAIL_ROW: return SignalDatabase.attachments().getAttachmentThumbnailStream(new PartUriParser(uri).getPartId(), 0);
@@ -96,7 +96,7 @@ public class PartAuthority {
if (attachment != null) return attachment.fileName;
else return null;
case BLOB_ROW:
return BlobProvider.getFileName(uri);
return AppDependencies.getBlobs().getFileName(uri);
default:
return null;
}
@@ -112,7 +112,7 @@ public class PartAuthority {
if (attachment != null) return attachment.size;
else return null;
case BLOB_ROW:
return BlobProvider.getFileSize(uri);
return AppDependencies.getBlobs().getFileSize(uri);
default:
return null;
}
@@ -128,7 +128,7 @@ public class PartAuthority {
if (attachment != null) return attachment.contentType;
else return null;
case BLOB_ROW:
return BlobProvider.getMimeType(uri);
return AppDependencies.getBlobs().getMimeType(uri);
default:
return null;
}
@@ -11,7 +11,6 @@ import org.signal.glide.decryptableuri.DecryptableUri
import org.thoughtcrime.securesms.database.model.MessageId
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mms.Slide
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.util.ImageCompressionUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.kb
@@ -127,8 +126,7 @@ object NotificationThumbnails {
}
if (result != null) {
val thumbnailUri = BlobProvider
.getInstance()
val thumbnailUri = AppDependencies.blobs
.forData(result.data)
.withMimeType(result.mimeType)
.withFileName(result.hashCode().toString())
@@ -137,7 +135,7 @@ object NotificationThumbnails {
synchronized(thumbnailCache) {
if (thumbnailCache.size >= MAX_CACHE_SIZE) {
thumbnailCache.remove(thumbnailCache.keys.first())?.uri?.let {
BlobProvider.getInstance().delete(context, it)
AppDependencies.blobs.delete(context, it)
}
}
thumbnailCache[messageId] = CachedThumbnail(thumbnailUri, result.mimeType)
@@ -6,17 +6,18 @@ import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.ByteUnit;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.crypto.AttachmentSecret;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.core.util.crypto.ModernDecryptingPartInputStream;
import org.signal.core.util.crypto.ModernEncryptingPartOutputStream;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import org.thoughtcrime.securesms.database.model.ProfileAvatarFileDetails;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.signal.core.util.ByteUnit;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.whispersystems.signalservice.api.util.StreamDetails;
@@ -117,7 +118,7 @@ public class AvatarHelper {
* IOException. It is recommended to call {@link #hasAvatar(Context, RecipientId)} first.
*/
public static @NonNull InputStream getAvatar(@NonNull Context context, @NonNull RecipientId recipientId) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret();
File avatarFile = getAvatarFile(context, recipientId);
return ModernDecryptingPartInputStream.createFor(attachmentSecret, avatarFile, 0);
@@ -166,7 +167,7 @@ public class AvatarHelper {
* recipient. Only intended to be used for backup. Otherwise, use {@link #setAvatar(Context, RecipientId, InputStream)}.
*/
public static @NonNull OutputStream getOutputStream(@NonNull Context context, @NonNull RecipientId recipientId, boolean isSyncAvatar) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret();
File targetFile = getAvatarFile(context, recipientId, isSyncAvatar);
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, targetFile, true).getSecond();
}
@@ -215,7 +216,7 @@ public class AvatarHelper {
private static void setAvatarInternal(@NonNull Context context, @NonNull RecipientId recipientId, @NonNull InputStream inputStream, boolean isSyncAvatar)
throws IOException
{
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret();
File targetFile = getAvatarFile(context, recipientId, isSyncAvatar);
File tempFile = new File(targetFile.getParent(), targetFile.getName() + ".tmp");
@@ -22,23 +22,22 @@ import com.airbnb.lottie.SimpleColorFilter;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import org.signal.core.ui.compose.SignalIcons;
import org.signal.core.models.media.Media;
import org.signal.core.ui.logging.LoggingFragment;
import org.signal.core.util.EditTextUtil;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
import org.signal.core.ui.logging.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.avatar.Avatars;
import org.thoughtcrime.securesms.avatar.picker.AvatarPickerFragment;
import org.thoughtcrime.securesms.databinding.CreateProfileFragmentBinding;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.keyvalue.PhoneNumberPrivacyValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.signal.core.models.media.Media;
import org.thoughtcrime.securesms.profiles.edit.pnp.WhoCanFindMeByPhoneNumberFragment;
import org.thoughtcrime.securesms.profiles.manage.EditProfileNameFragment;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.CommunicationActions;
import org.thoughtcrime.securesms.util.RemoteConfig;
import org.thoughtcrime.securesms.util.ViewUtil;
@@ -47,7 +46,6 @@ import org.thoughtcrime.securesms.util.text.AfterTextChanged;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
import static org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity.EXCLUDE_SYSTEM;
import static org.thoughtcrime.securesms.profiles.edit.CreateProfileActivity.GROUP_ID;
@@ -119,7 +117,7 @@ public class CreateProfileFragment extends LoggingFragment {
private void handleMediaFromResult(@NonNull Media media) {
SimpleTask.run(() -> {
try {
InputStream stream = BlobProvider.getInstance().getStream(requireContext(), media.getUri());
InputStream stream = AppDependencies.getBlobs().getStream(requireContext(), media.getUri());
return StreamUtil.readFully(stream);
} catch (IOException ioException) {
@@ -10,6 +10,7 @@ import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.models.media.Media;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
@@ -17,10 +18,8 @@ import org.thoughtcrime.securesms.badges.models.Badge;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.jobs.RefreshOwnProfileJob;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.signal.core.models.media.Media;
import org.thoughtcrime.securesms.profiles.AvatarHelper;
import org.thoughtcrime.securesms.profiles.ProfileName;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
@@ -135,7 +134,7 @@ class EditProfileViewModel extends ViewModel {
} else {
SignalExecutors.BOUNDED.execute(() -> {
try {
InputStream stream = BlobProvider.getInstance().getStream(context, media.getUri());
InputStream stream = AppDependencies.getBlobs().getStream(context, media.getUri());
byte[] data = StreamUtil.readFully(stream);
internalAvatarState.postValue(InternalAvatarState.loading(data));
@@ -15,11 +15,13 @@ import android.graphics.Bitmap
import android.net.Uri
import android.os.ParcelFileDescriptor
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.contentproviders.BaseContentProvider
import org.signal.core.util.crypto.AttachmentSecretProvider
import org.signal.core.util.logging.AndroidLogger
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.ApplicationContext
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
import org.thoughtcrime.securesms.crypto.DatabaseSecretProvider
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.SqlCipherLibraryLoader
@@ -73,7 +75,7 @@ class AvatarProvider : BaseContentProvider() {
SignalDatabase.init(
application,
DatabaseSecretProvider.getOrCreateDatabaseSecret(application),
AttachmentSecretProvider.getInstance(application).getOrCreateAttachmentSecret()
AttachmentSecretProvider.getInstance(application, AppAttachmentSecretStore).getOrCreateAttachmentSecret()
)
SignalStore.init(application)
@@ -1,62 +0,0 @@
package org.thoughtcrime.securesms.providers;
import android.content.ContentProvider;
import android.content.Context;
import android.content.pm.ProviderInfo;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.provider.OpenableColumns;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
public abstract class BaseContentProvider extends ContentProvider {
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
/**
* Sanity checks the security like FileProvider does.
*/
@Override
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
super.attachInfo(context, info);
if (info.exported) {
throw new SecurityException("Provider must not be exported");
}
if (!info.grantUriPermissions) {
throw new SecurityException("Provider must grant uri permissions");
}
}
protected static Cursor createCursor(@Nullable String[] projection, @NonNull String fileName, long fileSize) {
if (projection == null || projection.length == 0) {
projection = COLUMNS;
}
ArrayList<String> cols = new ArrayList<>(projection.length);
ArrayList<Object> values = new ArrayList<>(projection.length);
for (String col : projection) {
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
cols.add(OpenableColumns.DISPLAY_NAME);
values.add(fileName);
} else if (OpenableColumns.SIZE.equals(col)) {
cols.add(OpenableColumns.SIZE);
values.add(fileSize);
}
}
MatrixCursor cursor = new MatrixCursor(cols.toArray(new String[0]), 1);
cursor.addRow(values.toArray(new Object[0]));
return cursor;
}
protected static String createFileNameForMimeType(String mimeType) {
return mimeType.replace('/', '.');
}
}
@@ -1,3 +1,8 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.providers;
import android.content.ContentValues;
@@ -9,11 +14,13 @@ import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.signal.core.util.CoreUtilDependencies;
import org.signal.core.util.MemoryFileUtil;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.Util;
import org.signal.core.util.contentproviders.BaseContentProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.util.MemoryFileUtil;
import org.signal.core.util.Util;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -35,8 +42,8 @@ public final class BlobContentProvider extends BaseContentProvider {
Log.i(TAG, "openFile() called: " + uri);
try {
try (InputStream stream = BlobProvider.getInstance().getStream(AppDependencies.getApplication(), uri)) {
Long fileSize = BlobProvider.getFileSize(uri);
try (InputStream stream = AppDependencies.getBlobs().getStream(CoreUtilDependencies.getApplication(), uri)) {
Long fileSize = AppDependencies.getBlobs().getFileSize(uri);
if (fileSize == null) {
Log.w(TAG, "No file size available");
throw new FileNotFoundException();
@@ -64,9 +71,9 @@ public final class BlobContentProvider extends BaseContentProvider {
public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
Log.i(TAG, "query() called: " + uri);
String mimeType = BlobProvider.getMimeType(uri);
String fileName = BlobProvider.getFileName(uri);
Long fileSize = BlobProvider.getFileSize(uri);
String mimeType = AppDependencies.getBlobs().getMimeType(uri);
String fileName = AppDependencies.getBlobs().getFileName(uri);
Long fileSize = AppDependencies.getBlobs().getFileSize(uri);
if (fileSize == null) {
Log.w(TAG, "No file size");
@@ -79,16 +86,16 @@ public final class BlobContentProvider extends BaseContentProvider {
}
if (fileName == null) {
fileName = createFileNameForMimeType(mimeType);
fileName = BaseContentProvider.createFileNameForMimeType(mimeType);
}
return createCursor(projection, fileName, fileSize);
return BaseContentProvider.createCursor(projection, fileName, fileSize);
}
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return BlobProvider.getMimeType(uri);
return AppDependencies.getBlobs().getMimeType(uri);
}
@Nullable
@@ -1,704 +0,0 @@
package org.thoughtcrime.securesms.providers;
import android.app.Application;
import android.content.Context;
import android.content.UriMatcher;
import android.media.MediaDataSource;
import android.net.Uri;
import androidx.annotation.AnyThread;
import androidx.annotation.IntRange;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.thoughtcrime.securesms.components.voice.VoiceNoteDraft;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.database.DraftTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.util.IOFunction;
import org.signal.core.util.Util;
import org.thoughtcrime.securesms.video.ByteArrayMediaDataSource;
import org.thoughtcrime.securesms.video.EncryptedMediaDataSource;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
/**
* Allows for the creation and retrieval of blobs.
*/
public class BlobProvider {
private static final String TAG = Log.tag(BlobProvider.class);
private static final String DRAFT_ATTACHMENTS_DIRECTORY = "draft_blobs";
private static final String MULTI_SESSION_DIRECTORY = "multi_session_blobs";
private static final String SINGLE_SESSION_DIRECTORY = "single_session_blobs";
private static final String TEMP_BACKUPS_DIRECTORY = "temp_backups";
public static final String AUTHORITY = BuildConfig.APPLICATION_ID + ".blob";
public static final Uri CONTENT_URI = Uri.parse("content://" + AUTHORITY + "/blob");
public static final String PATH = "blob/*/*/*/*/*";
private static final int STORAGE_TYPE_PATH_SEGMENT = 1;
private static final int MIMETYPE_PATH_SEGMENT = 2;
private static final int FILENAME_PATH_SEGMENT = 3;
private static final int FILESIZE_PATH_SEGMENT = 4;
private static final int ID_PATH_SEGMENT = 5;
private static final int MATCH = 1;
private static final UriMatcher URI_MATCHER = new UriMatcher(UriMatcher.NO_MATCH) {{
addURI(AUTHORITY, PATH, MATCH);
}};
private static final BlobProvider INSTANCE = new BlobProvider();
private final Map<Uri, byte[]> memoryBlobs = new HashMap<>();
private volatile boolean initialized = false;
public static BlobProvider getInstance() {
return INSTANCE;
}
/**
* Begin building a blob for the provided data. Allows for the creation of in-memory blobs.
*/
public MemoryBlobBuilder forData(@NonNull byte[] data) {
return new MemoryBlobBuilder(data);
}
/**
* Begin building a blob for the provided input stream.
*/
public BlobBuilder forData(@NonNull InputStream data, long fileSize) {
return new BlobBuilder(data, fileSize);
}
public static boolean isSingleUseMemoryBlob(Uri uri) throws IOException {
return StorageType.decode(uri.getPathSegments().get(STORAGE_TYPE_PATH_SEGMENT)) == StorageType.SINGLE_USE_MEMORY;
}
public synchronized boolean hasStream(@NonNull Context context, @NonNull Uri uri) {
waitUntilInitialized();
try {
if (isAuthority(uri)) {
StorageType storageType = StorageType.decode(uri.getPathSegments().get(STORAGE_TYPE_PATH_SEGMENT));
if (storageType.isMemory()) {
byte[] data = memoryBlobs.get(uri);
return data != null;
} else {
String id = uri.getPathSegments().get(ID_PATH_SEGMENT);
String directory = getDirectory(storageType);
File file = new File(getOrCreateDirectory(context, directory), buildFileName(id));
return file.exists();
}
}
} catch (IOException e) {
// Intentionally left blank
}
return false;
}
/**
* Retrieve a stream for the content with the specified URI.
*
* @throws IOException If the stream fails to open or the spec of the URI doesn't match.
*/
public synchronized @NonNull InputStream getStream(@NonNull Context context, @NonNull Uri uri) throws IOException {
waitUntilInitialized();
return getStream(context, uri, 0L);
}
/**
* Retrieve a stream for the content with the specified URI starting from the specified position.
*
* @throws IOException If the stream fails to open or the spec of the URI doesn't match.
*/
public synchronized @NonNull InputStream getStream(@NonNull Context context, @NonNull Uri uri, long position) throws IOException {
waitUntilInitialized();
return getBlobRepresentation(context,
uri,
bytes -> {
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
if (byteArrayInputStream.skip(position) != position) {
throw new IOException("Failed to skip to position " + position + " for: " + uri);
}
return byteArrayInputStream;
},
file -> ModernDecryptingPartInputStream.createFor(getAttachmentSecret(context),
file,
position));
}
public synchronized @NonNull MediaDataSource getMediaDataSource(@NonNull Context context, @NonNull Uri uri) throws IOException {
waitUntilInitialized();
return getBlobRepresentation(context,
uri,
ByteArrayMediaDataSource::new,
file -> EncryptedMediaDataSource.createForDiskBlob(getAttachmentSecret(context), file));
}
private synchronized @NonNull <T> T getBlobRepresentation(@NonNull Context context,
@NonNull Uri uri,
@NonNull IOFunction<byte[], T> getByteRepresentation,
@NonNull IOFunction<File, T> getFileRepresentation)
throws IOException
{
if (isAuthority(uri)) {
StorageType storageType = StorageType.decode(uri.getPathSegments().get(STORAGE_TYPE_PATH_SEGMENT));
if (storageType.isMemory()) {
byte[] data = memoryBlobs.get(uri);
if (data != null) {
if (storageType == StorageType.SINGLE_USE_MEMORY) {
memoryBlobs.remove(uri);
}
return getByteRepresentation.apply(data);
} else {
throw new IOException("Failed to find in-memory blob for: " + uri);
}
} else {
String id = uri.getPathSegments().get(ID_PATH_SEGMENT);
String directory = getDirectory(storageType);
File file = new File(getOrCreateDirectory(context, directory), buildFileName(id));
return getFileRepresentation.apply(file);
}
} else {
throw new IOException("Provided URI does not match this spec. Uri: " + uri);
}
}
private synchronized AttachmentSecret getAttachmentSecret(@NonNull Context context) {
return AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
}
/**
* Delete the content with the specified URI.
*/
public synchronized void delete(@NonNull Context context, @NonNull Uri uri) {
waitUntilInitialized();
if (!isAuthority(uri)) {
Log.d(TAG, "Can't delete. Not the authority for uri: " + uri);
return;
}
Log.d(TAG, "Deleting " + getId(uri));
try {
StorageType storageType = StorageType.decode(uri.getPathSegments().get(STORAGE_TYPE_PATH_SEGMENT));
if (storageType.isMemory()) {
memoryBlobs.remove(uri);
} else {
String id = uri.getPathSegments().get(ID_PATH_SEGMENT);
String directory = getDirectory(storageType);
File file = new File(getOrCreateDirectory(context, directory), buildFileName(id));
if (file.delete()) {
Log.d(TAG, "Successfully deleted " + getId(uri));
} else {
throw new IOException("File wasn't deleted.");
}
}
} catch (IOException e) {
Log.w(TAG, "Failed to delete uri: " + getId(uri), e);
}
}
/**
* Allows the class to be initialized. Part of this initialization is deleting any leftover
* single-session blobs from the previous session. However, this class defers that work to a
* background thread, so callers don't have to worry about it.
*/
@AnyThread
public synchronized void initialize(@NonNull Context context) {
SignalExecutors.BOUNDED.execute(() -> {
synchronized (this) {
File directory = getOrCreateDirectory(context, SINGLE_SESSION_DIRECTORY);
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.delete()) {
Log.d(TAG, "Deleted single-session file: " + file.getName());
} else {
Log.w(TAG, "Failed to delete single-session file! " + file.getName());
}
}
} else {
Log.w(TAG, "Null directory listing!");
}
deleteOrphanedDraftFiles(context);
Log.i(TAG, "Initialized.");
initialized = true;
notifyAll();
}
});
}
@WorkerThread
public synchronized void clearTemporaryBackupsDirectory(@NonNull Context context) {
File directory = getOrCreateDirectory(context, TEMP_BACKUPS_DIRECTORY);
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.delete()) {
Log.d(TAG, "Deleted temporary backup file: " + file.getName());
} else {
Log.w(TAG, "Failed to delete temporary backup file: " + file.getName());
}
}
} else {
Log.w(TAG, "Null directory listing!");
}
}
@VisibleForTesting
public synchronized byte[] getMemoryBlob(@NonNull Uri uri) {
return memoryBlobs.get(uri);
}
private static void deleteOrphanedDraftFiles(@NonNull Context context) {
File directory = getOrCreateDirectory(context, DRAFT_ATTACHMENTS_DIRECTORY);
File[] files = directory.listFiles();
if (files == null || files.length == 0) {
Log.d(TAG, "No attachment drafts exist. Skipping.");
return;
}
DraftTable draftDatabase = SignalDatabase.drafts();
DraftTable.Drafts voiceNoteDrafts = draftDatabase.getAllVoiceNoteDrafts();
@SuppressWarnings("ConstantConditions")
List<String> draftFileNames = voiceNoteDrafts.stream()
.map(VoiceNoteDraft::fromDraft)
.map(VoiceNoteDraft::getUri)
.map(BlobProvider::getId)
.filter(Objects::nonNull)
.map(BlobProvider::buildFileName)
.collect(Collectors.toList());
for (final File file : files) {
if (!draftFileNames.contains(file.getName())) {
if (file.delete()) {
Log.d(TAG, "Deleted orphaned attachment draft: " + file.getName());
} else {
Log.d(TAG, "Failed to delete orphaned attachment draft: " + file.getName());
}
}
}
}
public static @Nullable String getMimeType(@NonNull Uri uri) {
if (isAuthority(uri)) {
return uri.getPathSegments().get(MIMETYPE_PATH_SEGMENT);
}
return null;
}
public static @Nullable String getFileName(@NonNull Uri uri) {
if (isAuthority(uri)) {
return uri.getPathSegments().get(FILENAME_PATH_SEGMENT);
}
return null;
}
public static @Nullable Long getFileSize(@NonNull Uri uri) {
if (isAuthority(uri)) {
try {
return Long.parseLong(uri.getPathSegments().get(FILESIZE_PATH_SEGMENT));
} catch (NumberFormatException e) {
return null;
}
}
return null;
}
private static @Nullable String getId(@NonNull Uri uri) {
if (isAuthority(uri)) {
return Uri.encode(uri.getPathSegments().get(ID_PATH_SEGMENT));
}
return null;
}
@WorkerThread
public long calculateFileSize(@NonNull Context context, @NonNull Uri uri) {
if (!isAuthority(uri)) {
return 0;
}
try (InputStream stream = getStream(context, uri)) {
return StreamUtil.getStreamLength(stream);
} catch (IOException e) {
Log.w(TAG, e);
return 0;
}
}
public static boolean isAuthority(@NonNull Uri uri) {
return URI_MATCHER.match(uri) == MATCH;
}
@WorkerThread
private synchronized @NonNull Uri writeBlobSpecToDisk(@NonNull Context context, @NonNull BlobSpec blobSpec)
throws IOException
{
waitUntilInitialized();
Future<Uri> uriFuture = writeBlobSpecToDiskAsync(context, blobSpec);
try {
return uriFuture.get();
} catch (ExecutionException | InterruptedException e) {
Log.e(TAG, "Error writing blob spec to disk", e);
if (e.getCause() instanceof IOException) {
throw (IOException) e.getCause();
} else {
throw new IOException(e);
}
}
}
@WorkerThread
private synchronized @NonNull Future<Uri> writeBlobSpecToDiskAsync(@NonNull Context context, @NonNull BlobSpec blobSpec)
throws IOException
{
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
String directory = getDirectory(blobSpec.getStorageType());
File outputFile = new File(getOrCreateDirectory(context, directory), buildFileName(blobSpec.id));
OutputStream outputStream = ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).getSecond();
final Uri uri = buildUri(blobSpec);
return SignalExecutors.BOUNDED.submit(() -> {
try {
StreamUtil.copy(blobSpec.getData(), outputStream);
return uri;
} catch (IOException e) {
delete(context, uri);
Log.w(TAG, "Error during write!", e);
throw e;
}
});
}
private synchronized @NonNull Uri writeBlobSpecToMemory(@NonNull BlobSpec blobSpec, @NonNull byte[] data) {
Uri uri = buildUri(blobSpec);
memoryBlobs.put(uri, data);
return uri;
}
private static @NonNull String buildFileName(@NonNull String id) {
return Uri.encode(id) + ".blob";
}
private static @NonNull String getDirectory(@NonNull StorageType storageType) {
switch (storageType) {
case SINGLE_USE_MEMORY:
case SINGLE_SESSION_MEMORY:
throw new IllegalArgumentException("In-Memory Blobs do not have directories.");
case SINGLE_SESSION_DISK:
return SINGLE_SESSION_DIRECTORY;
case MULTI_SESSION_DISK:
return MULTI_SESSION_DIRECTORY;
case ATTACHMENT_DRAFT:
return DRAFT_ATTACHMENTS_DIRECTORY;
case TEMP_BACKUPS:
return TEMP_BACKUPS_DIRECTORY;
}
return storageType == StorageType.MULTI_SESSION_DISK ? MULTI_SESSION_DIRECTORY : SINGLE_SESSION_DIRECTORY;
}
private static @NonNull Uri buildUri(@NonNull BlobSpec blobSpec) {
return CONTENT_URI.buildUpon()
.appendPath(blobSpec.getStorageType().encode())
.appendPath(blobSpec.getMimeType())
.appendPath(blobSpec.getFileName())
.appendEncodedPath(String.valueOf(blobSpec.getFileSize()))
.appendPath(blobSpec.getId())
.build();
}
private static File getOrCreateDirectory(@NonNull Context context, @NonNull String directory) {
return context.getDir(directory, Context.MODE_PRIVATE);
}
/**
* Returns a {@link File} within the appropriate directory to be cleaned up as part of
* normal operations. Unlike other blobs, this is just a file reference and no
* automatic encryption occurs when reading or writing and must be done by the caller.
*
* @return file located in the appropriate directory to be delete on app session restarts
*/
public File forNonAutoEncryptingSingleSessionOnDisk(@NonNull Context context) {
String directory = getDirectory(StorageType.SINGLE_SESSION_DISK);
String id = UUID.randomUUID().toString();
return new File(getOrCreateDirectory(context, directory), buildFileName(id));
}
/**
* Returns a {@link File} within the appropriate directory to persist between multiple
* process lifetimes. Unlike other blobs, this is just a file reference and no
* automatic encryption occurs when reading or writing and must be done by the caller.
*
* @return file located in the appropriate directory. The directory is periodically cleared.
*/
public File forTemporaryBackup(@NonNull Context context) {
String directory = getDirectory(StorageType.TEMP_BACKUPS);
String id = UUID.randomUUID().toString();
return new File(getOrCreateDirectory(context, directory), buildFileName(id));
}
public class BlobBuilder {
private InputStream data;
private String id;
private String mimeType;
private String fileName;
private long fileSize;
private BlobBuilder(@NonNull InputStream data, long fileSize) {
this.id = UUID.randomUUID().toString();
this.data = data;
this.fileSize = fileSize;
}
public BlobBuilder withMimeType(@NonNull String mimeType) {
this.mimeType = mimeType;
return this;
}
public BlobBuilder withFileName(@Nullable String fileName) {
this.fileName = fileName;
return this;
}
protected BlobSpec buildBlobSpec(@NonNull StorageType storageType) {
return new BlobSpec(data, id, storageType, mimeType, fileName, fileSize);
}
/**
* Create a blob that will exist for a single app session. An app session is defined as the
* period from one {@link Application#onCreate()} to the next.
*/
@WorkerThread
public Uri createForSingleSessionOnDisk(@NonNull Context context) throws IOException {
return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.SINGLE_SESSION_DISK));
}
/**
* Create an async blob that will exist for a single app session. An app session is defined as the
* period from one {@link Application#onCreate()} to the next. The file will be created on disk
* synchronously, but the data will copied asynchronously. This is helpful when the copy is
* long-running, such as in the case of recording a voice note.
*/
@WorkerThread
public Future<Uri> createForSingleSessionOnDiskAsync(@NonNull Context context) throws IOException {
return writeBlobSpecToDiskAsync(context, buildBlobSpec(StorageType.SINGLE_SESSION_DISK));
}
/**
* Create a blob that will exist for multiple app sessions. It is the caller's responsibility to
* eventually call {@link BlobProvider#delete(Context, Uri)} when the blob is no longer in use.
*/
@WorkerThread
public Uri createForMultipleSessionsOnDisk(@NonNull Context context) throws IOException {
return writeBlobSpecToDisk(context, buildBlobSpec(StorageType.MULTI_SESSION_DISK));
}
/**
* Create a blob that will exist for multiple app sessions. The file will be created on disk
* synchronously, but the data will copied asynchronously. This is helpful when the copy is
* long-running, such as in the case of recording a voice note.
* <p>
* It is the caller's responsibility to eventually call {@link BlobProvider#delete(Context, Uri)}
* when the blob is no longer in use.
*/
@WorkerThread
public Future<Uri> createForDraftAttachmentAsync(@NonNull Context context)
throws IOException
{
return writeBlobSpecToDiskAsync(context, buildBlobSpec(StorageType.ATTACHMENT_DRAFT));
}
/**
* Builds the URI for a draft attachment without waiting for the data to be written.
* This is useful for getting a URI reference while data is still being written asynchronously.
* The URI can be used to check file size and save periodic snapshots.
* <p>
* It is the caller's responsibility to eventually call {@link BlobProvider#delete(Context, Uri)}
* when the blob is no longer in use.
*/
@WorkerThread
public Uri buildUriForDraftAttachment() {
return buildUri(buildBlobSpec(StorageType.ATTACHMENT_DRAFT));
}
}
private synchronized void waitUntilInitialized() {
if (!initialized) {
Log.i(TAG, "Waiting for initialization...");
synchronized (this) {
while (!initialized) {
Util.wait(this, 0);
}
Log.i(TAG, "Initialization complete.");
}
}
}
public class MemoryBlobBuilder extends BlobBuilder {
private byte[] data;
private MemoryBlobBuilder(@NonNull byte[] data) {
super(new ByteArrayInputStream(data), data.length);
this.data = data;
}
@Override
public MemoryBlobBuilder withMimeType(@NonNull String mimeType) {
super.withMimeType(mimeType);
return this;
}
@Override
public MemoryBlobBuilder withFileName(@NonNull String fileName) {
super.withFileName(fileName);
return this;
}
/**
* Create a blob that is stored in memory and can only be read a single time. After a single
* read, it will be removed from storage. Useful for when a Uri is needed to read transient data.
*/
public Uri createForSingleUseInMemory() {
return writeBlobSpecToMemory(buildBlobSpec(StorageType.SINGLE_USE_MEMORY), data);
}
/**
* Create a blob that is stored in memory. Will persist for a single app session. You should
* always try to call {@link BlobProvider#delete(Context, Uri)} after you're done with the blob
* to free up memory.
*/
public Uri createForSingleSessionInMemory() {
return writeBlobSpecToMemory(buildBlobSpec(StorageType.SINGLE_SESSION_MEMORY), data);
}
}
private static class BlobSpec {
private final InputStream data;
private final String id;
private final StorageType storageType;
private final String mimeType;
private final String fileName;
private final long fileSize;
private BlobSpec(@NonNull InputStream data,
@NonNull String id,
@NonNull StorageType storageType,
@NonNull String mimeType,
@Nullable String fileName,
@IntRange(from = 0) long fileSize)
{
this.data = data;
this.id = id;
this.storageType = storageType;
this.mimeType = mimeType;
this.fileName = fileName;
this.fileSize = fileSize;
}
private @NonNull InputStream getData() {
return data;
}
private @NonNull String getId() {
return id;
}
private @NonNull StorageType getStorageType() {
return storageType;
}
private @NonNull String getMimeType() {
return mimeType;
}
private @Nullable String getFileName() {
return fileName;
}
private long getFileSize() {
return fileSize;
}
}
private enum StorageType {
SINGLE_USE_MEMORY("single-use-memory", true),
SINGLE_SESSION_MEMORY("single-session-memory", true),
SINGLE_SESSION_DISK("single-session-disk", false),
MULTI_SESSION_DISK("multi-session-disk", false),
ATTACHMENT_DRAFT("attachment-draft", false),
TEMP_BACKUPS("temporary-backups", false);
private final String encoded;
private final boolean inMemory;
StorageType(String encoded, boolean inMemory) {
this.encoded = encoded;
this.inMemory = inMemory;
}
private String encode() {
return encoded;
}
private boolean isMemory() {
return inMemory;
}
private static StorageType decode(@NonNull String encoded) throws IOException {
for (StorageType storageType : StorageType.values()) {
if (storageType.encoded.equals(encoded)) {
return storageType;
}
}
throw new IOException("Failed to decode lifespan.");
}
}
}
@@ -38,6 +38,7 @@ import androidx.annotation.RequiresApi;
import org.signal.core.util.StreamUtil;
import org.signal.core.util.ThreadUtil;
import org.signal.core.util.concurrent.SignalExecutors;
import org.signal.core.util.contentproviders.BaseContentProvider;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.BuildConfig;
import org.signal.core.models.database.AttachmentId;
@@ -46,7 +47,7 @@ import org.thoughtcrime.securesms.database.AttachmentTable;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.mms.PartUriParser;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.MemoryFileUtil;
import org.signal.core.util.MemoryFileUtil;
import org.signal.core.util.Util;
import java.io.FileNotFoundException;
@@ -18,14 +18,14 @@ import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import org.signal.core.ui.BottomSheetUtil;
import org.signal.core.ui.util.ThemeUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.qr.QrView;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.groups.GroupId;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.qr.QrCodeUtil;
import org.signal.core.ui.BottomSheetUtil;
import org.signal.core.ui.util.ThemeUtil;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
@@ -131,11 +131,11 @@ public class GroupLinkShareQrDialogFragment extends DialogFragment {
byte[] bytes = byteArrayOutputStream.toByteArray();
return BlobProvider.getInstance()
.forData(bytes)
.withMimeType("image/png")
.withFileName("SignalGroupQr.png")
.createForSingleSessionInMemory();
return AppDependencies.getBlobs()
.forData(bytes)
.withMimeType("image/png")
.withFileName("SignalGroupQr.png")
.createForSingleSessionInMemory();
} finally {
qrBitmap.recycle();
}
@@ -24,6 +24,7 @@ import org.signal.archive.LocalBackupRestoreProgress
import org.signal.core.models.AccountEntropyPool
import org.signal.core.models.MasterKey
import org.signal.core.util.StreamUtil
import org.signal.core.util.crypto.AttachmentSecretProvider
import org.signal.core.util.logging.Log
import org.signal.registration.PreExistingRegistrationData
import org.signal.registration.StorageController
@@ -38,7 +39,7 @@ import org.thoughtcrime.securesms.backup.v2.RemoteRestoreResult
import org.thoughtcrime.securesms.backup.v2.RestoreV2Event
import org.thoughtcrime.securesms.backup.v2.local.LocalArchiver
import org.thoughtcrime.securesms.backup.v2.local.SnapshotFileSystem
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
import org.thoughtcrime.securesms.crypto.ProfileKeyUtil
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.databaseprotos.LocalRegistrationMetadata
@@ -278,7 +279,7 @@ class AppRegistrationStorageController(private val context: Context) : StorageCo
val database = SignalDatabase.backupDatabase
FullBackupImporter.importFile(
context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore).getOrCreateAttachmentSecret(),
database,
uri,
passphrase,
@@ -9,11 +9,12 @@ import android.content.Context
import android.net.Uri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.signal.core.util.crypto.AttachmentSecretProvider
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.AppInitialization
import org.thoughtcrime.securesms.backup.BackupPassphrase
import org.thoughtcrime.securesms.backup.FullBackupImporter
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.jobmanager.impl.DataRestoreConstraint
@@ -59,7 +60,7 @@ object RestoreRepository {
FullBackupImporter.importFile(
context,
AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret(),
AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore).getOrCreateAttachmentSecret(),
database,
backupFileUri,
passphrase,
@@ -20,13 +20,12 @@ import com.bumptech.glide.Glide;
import org.signal.core.util.logging.Log;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.signal.video.VideoPlayer;
import org.thoughtcrime.securesms.PassphraseRequiredActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.signal.video.VideoPlayer;
import java.util.concurrent.TimeUnit;
@@ -88,7 +87,7 @@ public class ViewOnceMessageActivity extends PassphraseRequiredActivity implemen
super.onStop();
cancelDurationUpdate();
video.cleanup();
BlobProvider.getInstance().delete(this, uri);
AppDependencies.getBlobs().delete(this, uri);
finish();
}
@@ -31,7 +31,10 @@ import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import org.signal.core.ui.permissions.Permissions;
import org.signal.core.util.FontUtil;
import org.signal.core.util.ParcelUtil;
import org.signal.core.util.ThrottledDebouncer;
import org.signal.core.util.concurrent.LifecycleDisposable;
import org.signal.core.util.concurrent.SimpleTask;
import org.signal.core.util.logging.Log;
@@ -46,6 +49,8 @@ import org.signal.imageeditor.core.renderers.BezierDrawingRenderer;
import org.signal.imageeditor.core.renderers.FaceBlurRenderer;
import org.signal.imageeditor.core.renderers.MultiLineTextRenderer;
import org.signal.imageeditor.core.renderers.UriGlideRenderer;
import org.signal.mediasend.MediaConstraints;
import org.signal.mediasend.SentMediaQuality;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.attachments.AttachmentSaver;
@@ -54,20 +59,14 @@ import org.thoughtcrime.securesms.fonts.FontTypefaceProvider;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment;
import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations;
import org.signal.mediasend.MediaConstraints;
import org.thoughtcrime.securesms.mms.PushMediaConstraints;
import org.signal.mediasend.SentMediaQuality;
import org.signal.core.ui.permissions.Permissions;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.scribbles.stickers.AnalogClockStickerRenderer;
import org.thoughtcrime.securesms.scribbles.stickers.DigitalClockStickerRenderer;
import org.thoughtcrime.securesms.scribbles.stickers.FeatureSticker;
import org.thoughtcrime.securesms.scribbles.stickers.TappableRenderer;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.signal.core.util.ParcelUtil;
import org.thoughtcrime.securesms.util.SaveAttachmentUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.signal.core.util.ThrottledDebouncer;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
@@ -76,10 +75,9 @@ import java.util.Collections;
import java.util.List;
import java.util.Objects;
import kotlin.Pair;
import io.reactivex.rxjava3.core.Single;
import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.Pair;
import static android.app.Activity.RESULT_OK;
@@ -813,10 +811,10 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
image.recycle();
return BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleUseInMemory();
return AppDependencies.getBlobs()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleUseInMemory();
}
@WorkerThread
@@ -827,10 +825,10 @@ public final class ImageEditorFragment extends Fragment implements ImageEditorHu
image.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
image.recycle();
return BlobProvider.getInstance()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionInMemory();
return AppDependencies.getBlobs()
.forData(outputStream.toByteArray())
.withMimeType(MediaUtil.IMAGE_JPEG)
.createForSingleSessionInMemory();
}
private void onDrawingChanged(boolean stillTouching, boolean isUserEdit) {
@@ -9,7 +9,7 @@ import io.reactivex.rxjava3.core.Single
import io.reactivex.rxjava3.schedulers.Schedulers
import org.signal.core.models.media.Media
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.MediaUtil
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.UriUtil
@@ -49,7 +49,7 @@ class ShareRepository(context: Context) {
} ?: return ResolvedShareData.Failure
val blobUri: Uri = try {
BlobProvider.getInstance()
AppDependencies.blobs
.forData(stream, size)
.withMimeType(mimeType)
.withFileName(name)
@@ -94,7 +94,7 @@ class ShareRepository(context: Context) {
val dimens: Pair<Int, Int> = MediaUtil.getDimensions(appContext, mimeType, uri)
val duration = 0L
val blobUri = try {
BlobProvider.getInstance()
AppDependencies.blobs
.forData(stream, size)
.withMimeType(mimeType)
.createForSingleSessionOnDisk(appContext)
@@ -28,7 +28,7 @@ import org.thoughtcrime.securesms.database.SignalDatabase
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
import org.thoughtcrime.securesms.database.model.databaseprotos.StoryTextPost
import org.thoughtcrime.securesms.providers.BlobProvider
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.sharing.v2.ShareActivity
import org.thoughtcrime.securesms.stories.StoryTextPostModel
import org.thoughtcrime.securesms.stories.landing.StoriesLandingItem
@@ -88,7 +88,7 @@ object StoryContextMenu {
bitmap.recycle()
SaveAttachmentUtil.SaveAttachment(
uri = BlobProvider.getInstance().forData(jpeg.readBytes()).createForSingleUseInMemory(),
uri = AppDependencies.blobs.forData(jpeg.readBytes()).createForSingleUseInMemory(),
contentType = MediaUtil.IMAGE_JPEG,
date = messageRecord.dateSent,
fileName = null
@@ -2,9 +2,10 @@ package org.thoughtcrime.securesms.util
import android.content.Context
import androidx.annotation.WorkerThread
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream
import org.signal.core.util.crypto.AttachmentSecretProvider
import org.signal.core.util.crypto.ModernDecryptingPartInputStream
import org.signal.core.util.crypto.ModernEncryptingPartOutputStream
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
import java.io.File
import java.io.InputStream
import java.io.OutputStream
@@ -15,13 +16,13 @@ import java.io.OutputStream
object EncryptedStreamUtils {
@WorkerThread
fun getOutputStream(context: Context, outputFile: File): OutputStream {
val attachmentSecret = AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret
val attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore).orCreateAttachmentSecret
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).second
}
@WorkerThread
fun getInputStream(context: Context, inputFile: File): InputStream {
val attachmentSecret = AttachmentSecretProvider.getInstance(context).orCreateAttachmentSecret
val attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore).orCreateAttachmentSecret
return ModernDecryptingPartInputStream.createFor(attachmentSecret, inputFile, 0)
}
}
@@ -1,10 +0,0 @@
package org.thoughtcrime.securesms.util;
import java.io.IOException;
/**
* A function which takes 1 input and returns 1 output, and is capable of throwing an IO Exception.
*/
public interface IOFunction<I, O> {
O apply(I input) throws IOException;
}
@@ -14,7 +14,6 @@ import android.media.ThumbnailUtils;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;
import kotlin.Pair;
import android.webkit.MimeTypeMap;
import androidx.annotation.NonNull;
@@ -26,16 +25,17 @@ import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import org.signal.core.models.database.AttachmentId;
import org.signal.core.models.media.Media;
import org.signal.core.util.ContentTypeUtil;
import org.signal.core.util.bitmaps.BitmapDecodingException;
import org.signal.core.util.bitmaps.BitmapUtil;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.signal.core.models.database.AttachmentId;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.signal.core.models.media.Media;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.signal.glide.decryptableuri.DecryptableUri;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.database.SignalDatabase;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.DocumentSlide;
import org.thoughtcrime.securesms.mms.GifSlide;
import org.thoughtcrime.securesms.mms.ImageSlide;
@@ -46,7 +46,7 @@ import org.thoughtcrime.securesms.mms.StickerSlide;
import org.thoughtcrime.securesms.mms.TextSlide;
import org.thoughtcrime.securesms.mms.VideoSlide;
import org.thoughtcrime.securesms.mms.ViewOnceSlide;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.video.MediaDataSourceProvider;
import java.io.FileNotFoundException;
import java.io.IOException;
@@ -55,6 +55,8 @@ import java.net.URLConnection;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
import kotlin.Pair;
public class MediaUtil {
private static final String TAG = Log.tag(MediaUtil.class);
@@ -293,8 +295,8 @@ public class MediaUtil {
MediaMetadataRetriever retriever = new MediaMetadataRetriever();
try {
if (BlobProvider.isAuthority(uri) && MediaUtil.isVideo(BlobProvider.getMimeType(uri))) {
MediaDataSource source = BlobProvider.getInstance().getMediaDataSource(context, uri);
if (AppDependencies.getBlobs().isAuthority(uri) && MediaUtil.isVideo(AppDependencies.getBlobs().getMimeType(uri))) {
MediaDataSource source = MediaDataSourceProvider.getMediaDataSource(context, uri);
retriever.setDataSource(source);
} else if (PartAuthority.isAttachmentUri(uri)) {
MediaDataSource source = SignalDatabase.attachments().mediaDataSourceFor(PartAuthority.requireAttachmentId(uri), false);
@@ -471,7 +473,7 @@ public class MediaUtil {
return false;
}
if (BlobProvider.isAuthority(uri) && MediaUtil.isVideo(BlobProvider.getMimeType(uri))) {
if (AppDependencies.getBlobs().isAuthority(uri) && MediaUtil.isVideo(AppDependencies.getBlobs().getMimeType(uri))) {
return true;
}
@@ -516,11 +518,11 @@ public class MediaUtil {
MediaUtil.isVideo(URLConnection.guessContentTypeFromName(uri.toString()))) {
return ThumbnailUtils.createVideoThumbnail(uri.toString().replace("file://", ""),
MediaStore.Video.Thumbnails.MINI_KIND);
} else if (BlobProvider.isAuthority(uri) &&
MediaUtil.isVideo(BlobProvider.getMimeType(uri)))
} else if (AppDependencies.getBlobs().isAuthority(uri) &&
MediaUtil.isVideo(AppDependencies.getBlobs().getMimeType(uri)))
{
try {
MediaDataSource source = BlobProvider.getInstance().getMediaDataSource(context, uri);
MediaDataSource source = MediaDataSourceProvider.getMediaDataSource(context, uri);
return extractFrame(source, timeUs);
} catch (IOException e) {
Log.w(TAG, "Failed to extract frame for URI: " + uri, e);
@@ -1,93 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.content.Context;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.MemoryFile;
import android.os.ParcelFileDescriptor;
import android.os.ProxyFileDescriptorCallback;
import android.os.storage.StorageManager;
import android.system.ErrnoException;
import android.system.OsConstants;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
import java.io.IOException;
import java.io.InputStream;
import java.util.Objects;
@RequiresApi(api = 26)
final class MemoryFileDescriptorProxy {
private static final String TAG = Log.tag(MemoryFileDescriptorProxy.class);
public static ParcelFileDescriptor create(@NonNull Context context,
@NonNull MemoryFile file)
throws IOException
{
StorageManager storageManager = Objects.requireNonNull(context.getSystemService(StorageManager.class));
HandlerThread thread = new HandlerThread("MemoryFile");
thread.start();
Log.i(TAG, "Thread started");
Handler handler = new Handler(thread.getLooper());
ProxyCallback proxyCallback = new ProxyCallback(file, () -> {
if (thread.quitSafely()) {
Log.i(TAG, "Thread quitSafely true");
} else {
Log.w(TAG, "Thread quitSafely false");
}
});
ParcelFileDescriptor parcelFileDescriptor = storageManager.openProxyFileDescriptor(ParcelFileDescriptor.MODE_READ_ONLY,
proxyCallback,
handler);
Log.i(TAG, "Created");
return parcelFileDescriptor;
}
private static final class ProxyCallback extends ProxyFileDescriptorCallback {
private final MemoryFile memoryFile;
private final Runnable onClose;
ProxyCallback(@NonNull MemoryFile memoryFile, Runnable onClose) {
this.memoryFile = memoryFile;
this.onClose = onClose;
}
@Override
public long onGetSize() {
return memoryFile.length();
}
@Override
public int onRead(long offset, int size, byte[] data) throws ErrnoException {
try {
InputStream inputStream = memoryFile.getInputStream();
if(inputStream.skip(offset) != offset){
if (offset > memoryFile.length()) {
throw new ErrnoException("onRead", OsConstants.EIO);
}
throw new AssertionError();
}
return inputStream.read(data, 0, size);
} catch (IOException e) {
throw new ErrnoException("onRead", OsConstants.EBADF);
}
}
@Override
public void onRelease() {
Log.i(TAG, "onRelease()");
memoryFile.close();
onClose.run();
}
}
}
@@ -1,51 +0,0 @@
package org.thoughtcrime.securesms.util;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.MemoryFile;
import android.os.ParcelFileDescriptor;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import java.io.FileDescriptor;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public final class MemoryFileUtil {
private MemoryFileUtil() {}
public static ParcelFileDescriptor getParcelFileDescriptor(@NonNull MemoryFile file)
throws IOException
{
if (Build.VERSION.SDK_INT >= 26) {
return MemoryFileDescriptorProxy.create(AppDependencies.getApplication(), file);
} else {
return getParcelFileDescriptorLegacy(file);
}
}
@SuppressWarnings("JavaReflectionMemberAccess")
@SuppressLint("PrivateApi")
public static ParcelFileDescriptor getParcelFileDescriptorLegacy(@NonNull MemoryFile file)
throws IOException
{
try {
Method method = MemoryFile.class.getDeclaredMethod("getFileDescriptor");
FileDescriptor fileDescriptor = (FileDescriptor) method.invoke(file);
Field field = fileDescriptor.getClass().getDeclaredField("descriptor");
field.setAccessible(true);
int fd = field.getInt(fileDescriptor);
return ParcelFileDescriptor.adoptFd(fd);
} catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException | NoSuchFieldException e) {
throw new IOException(e);
}
}
}
@@ -3,8 +3,8 @@ package org.thoughtcrime.securesms.util
import android.content.Context
import org.signal.core.util.kibiBytes
import org.signal.core.util.splitByByteLength
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.mms.TextSlide
import org.thoughtcrime.securesms.providers.BlobProvider
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -31,7 +31,7 @@ object MessageUtil {
val textData = rawText.toByteArray()
val timestamp = SimpleDateFormat("yyyy-MM-dd-HHmmss", Locale.US).format(Date())
val filename = String.format("signal-%s.txt", timestamp)
val textUri = BlobProvider.getInstance()
val textUri = AppDependencies.blobs
.forData(textData)
.withMimeType(MediaUtil.LONG_TEXT)
.withFileName(filename)
@@ -6,10 +6,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import org.signal.core.util.StreamUtil;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.ModernEncryptingPartOutputStream;
import org.signal.core.util.crypto.AttachmentSecret;
import org.signal.core.util.crypto.AttachmentSecretProvider;
import org.signal.core.util.crypto.ModernDecryptingPartInputStream;
import org.signal.core.util.crypto.ModernEncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore;
import java.io.File;
import java.io.IOException;
@@ -93,12 +94,12 @@ public final class FileStorage {
}
private static @NonNull OutputStream getOutputStream(@NonNull Context context, File outputFile) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret();
return ModernEncryptingPartOutputStream.createFor(attachmentSecret, outputFile, true).getSecond();
}
private static @NonNull InputStream getInputStream(@NonNull Context context, File inputFile) throws IOException {
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context).getOrCreateAttachmentSecret();
AttachmentSecret attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore.INSTANCE).getOrCreateAttachmentSecret();
return ModernDecryptingPartInputStream.createFor(attachmentSecret, inputFile, 0);
}
}
@@ -4,8 +4,8 @@ import android.media.MediaDataSource;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ClassicDecryptingPartInputStream;
import org.signal.core.util.crypto.AttachmentSecret;
import org.signal.core.util.crypto.ClassicDecryptingPartInputStream;
import org.signal.core.util.Util;
import java.io.File;
@@ -5,7 +5,7 @@ import android.media.MediaDataSource;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.signal.core.util.crypto.AttachmentSecret;
import java.io.File;
@@ -0,0 +1,33 @@
/*
* Copyright 2026 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.video
import android.content.Context
import android.media.MediaDataSource
import android.net.Uri
import com.squareup.wire.internal.JvmStatic
import org.signal.core.util.crypto.AttachmentSecretProvider
import org.thoughtcrime.securesms.crypto.AppAttachmentSecretStore
import org.thoughtcrime.securesms.dependencies.AppDependencies
import java.io.IOException
/**
* Utilizes blob provider to load a blob as a media data source.
*/
object MediaDataSourceProvider {
@JvmStatic
@Suppress("MoveLambdaOutsideParentheses")
@Throws(IOException::class)
fun getMediaDataSource(context: Context, uri: Uri): MediaDataSource {
val attachmentSecret = AttachmentSecretProvider.getInstance(context, AppAttachmentSecretStore).getOrCreateAttachmentSecret()
return AppDependencies.blobs.getBlobRepresentation(
context,
uri,
::ByteArrayMediaDataSource,
{ EncryptedMediaDataSource.createForDiskBlob(attachmentSecret, it!!) }
)
}
}
@@ -5,8 +5,8 @@ import android.media.MediaDataSource;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.ModernDecryptingPartInputStream;
import org.signal.core.util.crypto.AttachmentSecret;
import org.signal.core.util.crypto.ModernDecryptingPartInputStream;
import org.thoughtcrime.securesms.video.videoconverter.mediadatasource.InputStreamMediaDataSource;
import java.io.File;
@@ -8,13 +8,11 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.TransferListener;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import java.io.EOFException;
import java.io.IOException;
@@ -44,15 +42,15 @@ class BlobDataSource implements DataSource {
@Override
public long open(DataSpec dataSpec) throws IOException {
this.dataSpec = dataSpec;
this.inputStream = BlobProvider.getInstance().getStream(context, dataSpec.uri, dataSpec.position);
this.inputStream = AppDependencies.getBlobs().getStream(context, dataSpec.uri, dataSpec.position);
if (listener != null) {
listener.onTransferStart(this, dataSpec, false);
}
long size = unwrapLong(BlobProvider.getFileSize(dataSpec.uri));
long size = unwrapLong(AppDependencies.getBlobs().getFileSize(dataSpec.uri));
if (size == 0) {
size = BlobProvider.getInstance().calculateFileSize(context, dataSpec.uri);
size = AppDependencies.getBlobs().calculateFileSize(context, dataSpec.uri);
}
if (size - dataSpec.position <= 0) throw new EOFException("No more data");
@@ -7,17 +7,14 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.OptIn;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.DataSource;
import androidx.media3.datasource.DataSpec;
import androidx.media3.datasource.DefaultDataSource;
import androidx.media3.datasource.DefaultDataSourceFactory;
import androidx.media3.datasource.TransferListener;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.mms.PartAuthority;
import org.thoughtcrime.securesms.providers.BlobProvider;
import java.io.IOException;
import java.util.Collections;
@@ -26,7 +23,7 @@ import java.util.Map;
import okhttp3.OkHttpClient;
/**
/**
* Go-to {@link DataSource} that handles all of our various types of video sources.
* Will defer to other {@link DataSource}s depending on the URI.
*/
@@ -57,7 +54,7 @@ import okhttp3.OkHttpClient;
@Override
public long open(DataSpec dataSpec) throws IOException {
if (BlobProvider.isAuthority(dataSpec.uri)) {
if (AppDependencies.getBlobs().isAuthority(dataSpec.uri)) {
dataSource = blobDataSource;
} else if (PartAuthority.isLocalUri(dataSpec.uri)) {
dataSource = partDataSource;