mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-15 07:28:30 +00:00
Reshape entry point for V3 media screens.
This commit is contained in:
committed by
Greyson Parrelli
parent
6d944c0f8c
commit
5c415139fd
@@ -475,7 +475,7 @@
|
|||||||
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
|
android:windowSoftInputMode="stateAlwaysHidden|adjustNothing" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".mediasend.v3.MediaSendV3Activity"
|
android:name="org.signal.mediasend.MediaSendActivity"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize|uiMode"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
|
|||||||
@@ -157,7 +157,6 @@ import org.thoughtcrime.securesms.main.rememberMainNavigationDetailLocation
|
|||||||
import org.thoughtcrime.securesms.main.storiesNavGraphBuilder
|
import org.thoughtcrime.securesms.main.storiesNavGraphBuilder
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil
|
||||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity
|
||||||
import org.thoughtcrime.securesms.mediasend.v3.MediaSendV3ActivityContract
|
|
||||||
import org.thoughtcrime.securesms.megaphone.Megaphone
|
import org.thoughtcrime.securesms.megaphone.Megaphone
|
||||||
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
|
import org.thoughtcrime.securesms.megaphone.MegaphoneActionController
|
||||||
import org.thoughtcrime.securesms.megaphone.Megaphones
|
import org.thoughtcrime.securesms.megaphone.Megaphones
|
||||||
@@ -280,7 +279,7 @@ class MainActivity : PassphraseRequiredActivity(), VoiceNoteMediaControllerOwner
|
|||||||
super.onCreate(savedInstanceState, ready)
|
super.onCreate(savedInstanceState, ready)
|
||||||
navigator = MainNavigator(this, mainNavigationViewModel)
|
navigator = MainNavigator(this, mainNavigationViewModel)
|
||||||
|
|
||||||
mediaActivityLauncher = registerForActivityResult(MediaSendV3ActivityContract()) { }
|
mediaActivityLauncher = registerForActivityResult(MediaSendActivityContract()) { }
|
||||||
|
|
||||||
AppForegroundObserver.addListener(object : AppForegroundObserver.Listener {
|
AppForegroundObserver.addListener(object : AppForegroundObserver.Listener {
|
||||||
override fun onForeground() {
|
override fun onForeground() {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.app.Application
|
import android.app.Application
|
||||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import org.signal.core.ui.CoreUiDependencies
|
||||||
import org.signal.core.util.billing.BillingApi
|
import org.signal.core.util.billing.BillingApi
|
||||||
import org.signal.core.util.concurrent.DeadlockDetector
|
import org.signal.core.util.concurrent.DeadlockDetector
|
||||||
import org.signal.core.util.concurrent.LatestValueObservable
|
import org.signal.core.util.concurrent.LatestValueObservable
|
||||||
@@ -13,6 +14,7 @@ import org.signal.glide.SignalGlideDependencies
|
|||||||
import org.signal.libsignal.net.Network
|
import org.signal.libsignal.net.Network
|
||||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
||||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
||||||
|
import org.signal.mediasend.MediaSendDependencies
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender
|
import org.thoughtcrime.securesms.components.TypingStatusSender
|
||||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
||||||
@@ -97,7 +99,9 @@ object AppDependencies {
|
|||||||
_application = application
|
_application = application
|
||||||
AppDependencies.provider = provider
|
AppDependencies.provider = provider
|
||||||
|
|
||||||
|
CoreUiDependencies.init(CoreUiDependenciesProvider)
|
||||||
SignalGlideDependencies.init(application, SignalGlideDependenciesProvider)
|
SignalGlideDependencies.init(application, SignalGlideDependenciesProvider)
|
||||||
|
MediaSendDependencies.init(application, MediaSendDependenciesProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.dependencies
|
||||||
|
|
||||||
|
import org.signal.core.ui.CoreUiDependencies
|
||||||
|
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||||
|
|
||||||
|
object CoreUiDependenciesProvider : CoreUiDependencies.Provider {
|
||||||
|
override fun provideIsIncognitoKeyboardEnabled(): Boolean {
|
||||||
|
return TextSecurePreferences.isIncognitoKeyboardEnabled(AppDependencies.application)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.dependencies
|
||||||
|
|
||||||
|
import org.signal.mediasend.MediaSendDependencies
|
||||||
|
import org.signal.mediasend.MediaSendRepository
|
||||||
|
import org.signal.mediasend.preupload.PreUploadRepository
|
||||||
|
import org.thoughtcrime.securesms.mediasend.v3.MediaSendV3PreUploadRepository
|
||||||
|
import org.thoughtcrime.securesms.mediasend.v3.MediaSendV3Repository
|
||||||
|
|
||||||
|
object MediaSendDependenciesProvider : MediaSendDependencies.Provider {
|
||||||
|
override fun provideMediaSendRepository(): MediaSendRepository = MediaSendV3Repository
|
||||||
|
|
||||||
|
override fun providePreUploadRepository(): PreUploadRepository = MediaSendV3PreUploadRepository
|
||||||
|
}
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2026 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.mediasend.v3
|
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
|
||||||
import org.signal.core.models.media.Media
|
|
||||||
import org.signal.mediasend.MediaSendActivity
|
|
||||||
import org.thoughtcrime.securesms.mediasend.CameraFragment
|
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
|
||||||
import java.io.FileDescriptor
|
|
||||||
import java.util.Optional
|
|
||||||
|
|
||||||
/**
|
|
||||||
* App-layer implementation of the feature module media send activity.
|
|
||||||
*/
|
|
||||||
class MediaSendV3Activity : MediaSendActivity(), CameraFragment.Controller {
|
|
||||||
|
|
||||||
override val preUploadCallback = MediaSendV3PreUploadCallback()
|
|
||||||
override val repository by lazy { MediaSendV3Repository(applicationContext) }
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun CameraSlot() {
|
|
||||||
MediaSendV3CameraSlot()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun TextStoryEditorSlot() {
|
|
||||||
MediaSendV3PlaceholderScreen(text = "Text Story Editor")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun VideoEditorSlot() {
|
|
||||||
MediaSendV3PlaceholderScreen(text = "Video Editor")
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun SendSlot() {
|
|
||||||
MediaSendV3PlaceholderScreen(text = "Send Review")
|
|
||||||
}
|
|
||||||
|
|
||||||
// region Camera Callbacks
|
|
||||||
|
|
||||||
override fun onCameraError() {
|
|
||||||
error("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onImageCaptured(data: ByteArray, width: Int, height: Int) {
|
|
||||||
error("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onVideoCaptured(fd: FileDescriptor) {
|
|
||||||
error("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onVideoCaptureError() {
|
|
||||||
error("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onGalleryClicked() {
|
|
||||||
error("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCameraCountButtonClicked() {
|
|
||||||
error("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQrCodeFound(data: String) {
|
|
||||||
error("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMostRecentMediaItem(): Flowable<Optional<Media?>> {
|
|
||||||
error("Not yet implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMediaConstraints(): MediaConstraints {
|
|
||||||
return MediaConstraints.getPushMediaConstraints()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getMaxVideoDuration(): Int {
|
|
||||||
error("Not yet implemented")
|
|
||||||
}
|
|
||||||
// endregion
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright 2026 Signal Messenger, LLC
|
|
||||||
* SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.mediasend.v3
|
|
||||||
|
|
||||||
import org.signal.mediasend.MediaSendActivityContract
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Activity result contract bound to [MediaSendV3Activity].
|
|
||||||
*/
|
|
||||||
class MediaSendV3ActivityContract : MediaSendActivityContract(MediaSendV3Activity::class.java)
|
|
||||||
@@ -9,7 +9,7 @@ import android.content.Context
|
|||||||
import androidx.annotation.WorkerThread
|
import androidx.annotation.WorkerThread
|
||||||
import org.signal.core.models.media.Media
|
import org.signal.core.models.media.Media
|
||||||
import org.signal.mediasend.MediaRecipientId
|
import org.signal.mediasend.MediaRecipientId
|
||||||
import org.signal.mediasend.preupload.PreUploadManager
|
import org.signal.mediasend.preupload.PreUploadRepository
|
||||||
import org.signal.mediasend.preupload.PreUploadResult
|
import org.signal.mediasend.preupload.PreUploadResult
|
||||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||||
@@ -19,7 +19,7 @@ import org.thoughtcrime.securesms.recipients.Recipient
|
|||||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||||
import org.thoughtcrime.securesms.sms.MessageSender
|
import org.thoughtcrime.securesms.sms.MessageSender
|
||||||
|
|
||||||
class MediaSendV3PreUploadCallback : PreUploadManager.Callback {
|
object MediaSendV3PreUploadRepository : PreUploadRepository {
|
||||||
|
|
||||||
@WorkerThread
|
@WorkerThread
|
||||||
override fun preUpload(context: Context, media: Media, recipientId: MediaRecipientId?): PreUploadResult? {
|
override fun preUpload(context: Context, media: Media, recipientId: MediaRecipientId?): PreUploadResult? {
|
||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
package org.thoughtcrime.securesms.mediasend.v3
|
package org.thoughtcrime.securesms.mediasend.v3
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
@@ -48,11 +47,9 @@ import kotlin.time.Duration.Companion.seconds
|
|||||||
/**
|
/**
|
||||||
* App-layer implementation of [MediaSendRepository] that bridges to legacy v2 infrastructure.
|
* App-layer implementation of [MediaSendRepository] that bridges to legacy v2 infrastructure.
|
||||||
*/
|
*/
|
||||||
class MediaSendV3Repository(
|
object MediaSendV3Repository : MediaSendRepository {
|
||||||
context: Context
|
|
||||||
) : MediaSendRepository {
|
|
||||||
|
|
||||||
private val appContext = context.applicationContext
|
private val appContext = AppDependencies.application
|
||||||
private val legacyRepository = MediaSelectionRepository(appContext)
|
private val legacyRepository = MediaSelectionRepository(appContext)
|
||||||
private val mediaRepository = MediaRepository()
|
private val mediaRepository = MediaRepository()
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ android {
|
|||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.5.4"
|
kotlinCompilerExtensionVersion = "1.5.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testFixtures {
|
||||||
|
enable = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
@@ -38,4 +42,7 @@ dependencies {
|
|||||||
api(libs.google.zxing.core)
|
api(libs.google.zxing.core)
|
||||||
api(libs.material.material)
|
api(libs.material.material)
|
||||||
api(libs.accompanist.permissions)
|
api(libs.accompanist.permissions)
|
||||||
|
|
||||||
|
// JUnit is used by test fixtures
|
||||||
|
testFixturesImplementation(testLibs.junit.junit)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.ui
|
||||||
|
|
||||||
|
object CoreUiDependencies {
|
||||||
|
|
||||||
|
private lateinit var _provider: Provider
|
||||||
|
|
||||||
|
fun init(provider: Provider) {
|
||||||
|
if (this::_provider.isInitialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_provider = provider
|
||||||
|
}
|
||||||
|
|
||||||
|
val isIncognitoKeyboardEnabled: Boolean
|
||||||
|
get() = _provider.provideIsIncognitoKeyboardEnabled()
|
||||||
|
|
||||||
|
interface Provider {
|
||||||
|
fun provideIsIncognitoKeyboardEnabled(): Boolean
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.text.font.FontFamily
|
|||||||
import androidx.compose.ui.text.font.FontStyle
|
import androidx.compose.ui.text.font.FontStyle
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.signal.core.ui.CoreUiDependencies
|
||||||
import org.signal.core.ui.compose.ProvideIncognitoKeyboard
|
import org.signal.core.ui.compose.ProvideIncognitoKeyboard
|
||||||
|
|
||||||
private val typography = Typography().run {
|
private val typography = Typography().run {
|
||||||
@@ -190,7 +191,7 @@ private val darkSnackbarColors = SnackbarColors(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SignalTheme(
|
fun SignalTheme(
|
||||||
isDarkMode: Boolean = LocalConfiguration.current.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES,
|
isDarkMode: Boolean = LocalConfiguration.current.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES,
|
||||||
incognitoKeyboardEnabled: Boolean = false,
|
incognitoKeyboardEnabled: Boolean = CoreUiDependencies.isIncognitoKeyboardEnabled,
|
||||||
content: @Composable () -> Unit
|
content: @Composable () -> Unit
|
||||||
) {
|
) {
|
||||||
val extendedColors = if (isDarkMode) darkExtendedColors else lightExtendedColors
|
val extendedColors = if (isDarkMode) darkExtendedColors else lightExtendedColors
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.core.ui
|
||||||
|
|
||||||
|
import org.junit.rules.ExternalResource
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Since tests that depend on [org.signal.core.ui.compose.theme.SignalTheme] need
|
||||||
|
* [CoreUiDependencies] to be initialized, this rule provides a convenient way to do so.
|
||||||
|
*/
|
||||||
|
class CoreUiDependenciesRule(
|
||||||
|
private val isIncognitoKeyboardEnabled: Boolean = false
|
||||||
|
) : ExternalResource() {
|
||||||
|
override fun before() {
|
||||||
|
CoreUiDependencies.init(Provider(isIncognitoKeyboardEnabled))
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Provider(
|
||||||
|
val isIncognitoKeyboardEnabled: Boolean
|
||||||
|
): CoreUiDependencies.Provider {
|
||||||
|
override fun provideIsIncognitoKeyboardEnabled(): Boolean = isIncognitoKeyboardEnabled
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import android.app.Application
|
|||||||
import android.os.Build
|
import android.os.Build
|
||||||
import org.signal.core.models.ServiceId.ACI
|
import org.signal.core.models.ServiceId.ACI
|
||||||
import org.signal.core.models.ServiceId.PNI
|
import org.signal.core.models.ServiceId.PNI
|
||||||
|
import org.signal.core.ui.CoreUiDependencies
|
||||||
import org.signal.core.util.Base64
|
import org.signal.core.util.Base64
|
||||||
import org.signal.core.util.logging.AndroidLogger
|
import org.signal.core.util.logging.AndroidLogger
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
@@ -56,6 +57,10 @@ class RegistrationApplication : Application() {
|
|||||||
storageController = storageController
|
storageController = storageController
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CoreUiDependencies.init(object : CoreUiDependencies.Provider {
|
||||||
|
override fun provideIsIncognitoKeyboardEnabled(): Boolean = false
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createPushServiceSocket(configuration: SignalServiceConfiguration): PushServiceSocket {
|
private fun createPushServiceSocket(configuration: SignalServiceConfiguration): PushServiceSocket {
|
||||||
|
|||||||
@@ -15,28 +15,11 @@ import androidx.fragment.app.FragmentActivity
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import org.signal.core.ui.compose.theme.SignalTheme
|
import org.signal.core.ui.compose.theme.SignalTheme
|
||||||
import org.signal.mediasend.preupload.PreUploadManager
|
|
||||||
import org.signal.mediasend.select.MediaSelectScreen
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Abstract base activity for the media sending flow.
|
* Activity for the media sending flow.
|
||||||
*
|
|
||||||
* App-layer implementations must extend this class and provide:
|
|
||||||
* - [preUploadCallback] — For pre-upload job management
|
|
||||||
* - [repository] — For media validation, sending, and other app-layer operations
|
|
||||||
* - UI slots: [CameraSlot], [TextStoryEditorSlot], [MediaSelectSlot], [ImageEditorSlot], [VideoEditorSlot], [SendSlot]
|
|
||||||
*
|
|
||||||
* The concrete implementation should be registered in the app's manifest.
|
|
||||||
*/
|
*/
|
||||||
abstract class MediaSendActivity : FragmentActivity() {
|
abstract class MediaSendActivity : FragmentActivity() {
|
||||||
|
|
||||||
/** Pre-upload callback implementation for job management. */
|
|
||||||
protected abstract val preUploadCallback: PreUploadManager.Callback
|
|
||||||
|
|
||||||
/** Repository implementation for app-layer operations. */
|
|
||||||
protected abstract val repository: MediaSendRepository
|
|
||||||
|
|
||||||
/** Contract args extracted from intent. Available after super.onCreate(). */
|
|
||||||
protected lateinit var contractArgs: MediaSendActivityContract.Args
|
protected lateinit var contractArgs: MediaSendActivityContract.Args
|
||||||
private set
|
private set
|
||||||
|
|
||||||
@@ -49,11 +32,8 @@ abstract class MediaSendActivity : FragmentActivity() {
|
|||||||
setContent {
|
setContent {
|
||||||
val viewModel by viewModels<MediaSendViewModel>(factoryProducer = {
|
val viewModel by viewModels<MediaSendViewModel>(factoryProducer = {
|
||||||
MediaSendViewModel.Factory(
|
MediaSendViewModel.Factory(
|
||||||
context = applicationContext,
|
|
||||||
args = contractArgs,
|
args = contractArgs,
|
||||||
isMeteredFlow = MeteredConnectivity.isMetered(applicationContext),
|
isMeteredFlow = MeteredConnectivity.isMetered(applicationContext)
|
||||||
repository = repository,
|
|
||||||
preUploadCallback = preUploadCallback
|
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -62,68 +42,35 @@ abstract class MediaSendActivity : FragmentActivity() {
|
|||||||
if (state.isCameraFirst) MediaSendNavKey.Capture.Camera else MediaSendNavKey.Select
|
if (state.isCameraFirst) MediaSendNavKey.Capture.Camera else MediaSendNavKey.Select
|
||||||
)
|
)
|
||||||
|
|
||||||
Theme {
|
SignalTheme {
|
||||||
Surface {
|
Surface {
|
||||||
MediaSendNavDisplay(
|
MediaSendNavDisplay(
|
||||||
state = state,
|
state = state,
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
callback = viewModel,
|
callback = viewModel,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
cameraSlot = { CameraSlot() },
|
cameraSlot = { },
|
||||||
textStoryEditorSlot = { TextStoryEditorSlot() },
|
textStoryEditorSlot = { },
|
||||||
mediaSelectSlot = {
|
videoEditorSlot = { },
|
||||||
MediaSelectScreen(
|
sendSlot = { }
|
||||||
state = state,
|
|
||||||
backStack = backStack,
|
|
||||||
callback = viewModel
|
|
||||||
)
|
|
||||||
},
|
|
||||||
videoEditorSlot = { VideoEditorSlot() },
|
|
||||||
sendSlot = { SendSlot() }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Theme wrapper */
|
|
||||||
@Composable
|
|
||||||
protected open fun Theme(content: @Composable () -> Unit) {
|
|
||||||
SignalTheme(incognitoKeyboardEnabled = false) {
|
|
||||||
content()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Camera capture UI slot. */
|
|
||||||
@Composable
|
|
||||||
protected abstract fun CameraSlot()
|
|
||||||
|
|
||||||
/** Text story editor UI slot. */
|
|
||||||
@Composable
|
|
||||||
protected abstract fun TextStoryEditorSlot()
|
|
||||||
|
|
||||||
/** Video editor UI slot. */
|
|
||||||
@Composable
|
|
||||||
protected abstract fun VideoEditorSlot()
|
|
||||||
|
|
||||||
/** Send/review UI slot. */
|
|
||||||
@Composable
|
|
||||||
protected abstract fun SendSlot()
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Creates an intent for a concrete [MediaSendActivity] subclass.
|
* Creates an intent for [MediaSendActivity].
|
||||||
*
|
*
|
||||||
* @param context The context.
|
* @param context The context.
|
||||||
* @param activityClass The concrete activity class to launch.
|
|
||||||
* @param args The activity arguments.
|
* @param args The activity arguments.
|
||||||
*/
|
*/
|
||||||
fun <T : MediaSendActivity> createIntent(
|
fun createIntent(
|
||||||
context: Context,
|
context: Context,
|
||||||
activityClass: Class<T>,
|
|
||||||
args: MediaSendActivityContract.Args = MediaSendActivityContract.Args()
|
args: MediaSendActivityContract.Args = MediaSendActivityContract.Args()
|
||||||
): Intent {
|
): Intent {
|
||||||
return Intent(context, activityClass).apply {
|
return Intent(context, MediaSendActivity::class.java).apply {
|
||||||
putExtra(MediaSendActivityContract.EXTRA_ARGS, args)
|
putExtra(MediaSendActivityContract.EXTRA_ARGS, args)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ import org.signal.core.models.media.Media
|
|||||||
* class MyMediaSendContract : MediaSendActivityContract(MyMediaSendActivity::class.java)
|
* class MyMediaSendContract : MediaSendActivityContract(MyMediaSendActivity::class.java)
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
open class MediaSendActivityContract(
|
class MediaSendActivityContract : ActivityResultContract<MediaSendActivityContract.Args, MediaSendActivityContract.Result?>() {
|
||||||
private val activityClass: Class<out MediaSendActivity>? = null
|
|
||||||
) : ActivityResultContract<MediaSendActivityContract.Args, MediaSendActivityContract.Result?>() {
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the intent to launch the media send activity.
|
* Creates the intent to launch the media send activity.
|
||||||
@@ -40,12 +38,7 @@ open class MediaSendActivityContract(
|
|||||||
* Subclasses should override this if not using the constructor parameter.
|
* Subclasses should override this if not using the constructor parameter.
|
||||||
*/
|
*/
|
||||||
override fun createIntent(context: Context, input: Args): Intent {
|
override fun createIntent(context: Context, input: Args): Intent {
|
||||||
val clazz = activityClass
|
return MediaSendActivity.createIntent(context, input)
|
||||||
?: throw IllegalStateException(
|
|
||||||
"MediaSendActivityContract requires either a concrete activity class in the constructor " +
|
|
||||||
"or an overridden createIntent() method. MediaSendActivity is abstract and cannot be launched directly."
|
|
||||||
)
|
|
||||||
return MediaSendActivity.createIntent(context, clazz, input)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun parseResult(resultCode: Int, intent: Intent?): Result? {
|
override fun parseResult(resultCode: Int, intent: Intent?): Result? {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.mediasend
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import org.signal.mediasend.preupload.PreUploadRepository
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MediaSend Feature Module dependencies
|
||||||
|
*/
|
||||||
|
object MediaSendDependencies {
|
||||||
|
private lateinit var _application: Application
|
||||||
|
private lateinit var _provider: Provider
|
||||||
|
|
||||||
|
@Synchronized
|
||||||
|
fun init(application: Application, provider: Provider) {
|
||||||
|
if (this::_application.isInitialized || this::_provider.isInitialized) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_application = application
|
||||||
|
_provider = provider
|
||||||
|
}
|
||||||
|
|
||||||
|
val application
|
||||||
|
get() = _application
|
||||||
|
|
||||||
|
val preUploadRepository: PreUploadRepository
|
||||||
|
get() = _provider.providePreUploadRepository()
|
||||||
|
|
||||||
|
val mediaSendRepository: MediaSendRepository
|
||||||
|
get() = _provider.provideMediaSendRepository()
|
||||||
|
|
||||||
|
interface Provider {
|
||||||
|
fun provideMediaSendRepository(): MediaSendRepository
|
||||||
|
fun providePreUploadRepository(): PreUploadRepository
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ import androidx.navigationevent.compose.rememberNavigationEventDispatcherOwner
|
|||||||
import org.signal.core.ui.compose.AllDevicePreviews
|
import org.signal.core.ui.compose.AllDevicePreviews
|
||||||
import org.signal.core.ui.compose.Previews
|
import org.signal.core.ui.compose.Previews
|
||||||
import org.signal.mediasend.edit.MediaEditScreen
|
import org.signal.mediasend.edit.MediaEditScreen
|
||||||
|
import org.signal.mediasend.select.MediaSelectScreen
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Enforces the following flow of:
|
* Enforces the following flow of:
|
||||||
@@ -32,7 +33,6 @@ fun MediaSendNavDisplay(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
cameraSlot: @Composable () -> Unit = {},
|
cameraSlot: @Composable () -> Unit = {},
|
||||||
textStoryEditorSlot: @Composable () -> Unit = {},
|
textStoryEditorSlot: @Composable () -> Unit = {},
|
||||||
mediaSelectSlot: @Composable () -> Unit = {},
|
|
||||||
videoEditorSlot: @Composable () -> Unit = {},
|
videoEditorSlot: @Composable () -> Unit = {},
|
||||||
sendSlot: @Composable () -> Unit = {}
|
sendSlot: @Composable () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
@@ -50,7 +50,11 @@ fun MediaSendNavDisplay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
MediaSendNavKey.Select -> NavEntry(key) {
|
MediaSendNavKey.Select -> NavEntry(key) {
|
||||||
mediaSelectSlot()
|
MediaSelectScreen(
|
||||||
|
state = state,
|
||||||
|
backStack = backStack,
|
||||||
|
callback = callback
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
is MediaSendNavKey.Edit -> NavEntry(MediaSendNavKey.Edit) {
|
is MediaSendNavKey.Edit -> NavEntry(MediaSendNavKey.Edit) {
|
||||||
@@ -82,7 +86,6 @@ private fun MediaSendNavDisplayPreview() {
|
|||||||
callback = MediaSendCallback.Empty,
|
callback = MediaSendCallback.Empty,
|
||||||
cameraSlot = { BoxWithText("Camera Slot") },
|
cameraSlot = { BoxWithText("Camera Slot") },
|
||||||
textStoryEditorSlot = { BoxWithText("Text Story Editor Slot") },
|
textStoryEditorSlot = { BoxWithText("Text Story Editor Slot") },
|
||||||
mediaSelectSlot = { BoxWithText("Media Select Slot") },
|
|
||||||
videoEditorSlot = { BoxWithText("Video Editor Slot") },
|
videoEditorSlot = { BoxWithText("Video Editor Slot") },
|
||||||
sendSlot = { BoxWithText("Send Slot") }
|
sendSlot = { BoxWithText("Send Slot") }
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
package org.signal.mediasend
|
package org.signal.mediasend
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.lifecycle.SavedStateHandle
|
import androidx.lifecycle.SavedStateHandle
|
||||||
@@ -38,7 +37,7 @@ import org.signal.core.util.StringUtil
|
|||||||
import org.signal.imageeditor.core.model.EditorElement
|
import org.signal.imageeditor.core.model.EditorElement
|
||||||
import org.signal.imageeditor.core.model.EditorModel
|
import org.signal.imageeditor.core.model.EditorModel
|
||||||
import org.signal.imageeditor.core.renderers.UriGlideRenderer
|
import org.signal.imageeditor.core.renderers.UriGlideRenderer
|
||||||
import org.signal.mediasend.preupload.PreUploadManager
|
import org.signal.mediasend.preupload.PreUploadController
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import kotlin.time.Duration.Companion.milliseconds
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
|
||||||
@@ -54,7 +53,7 @@ class MediaSendViewModel(
|
|||||||
isMeteredFlow: Flow<Boolean>,
|
isMeteredFlow: Flow<Boolean>,
|
||||||
private val savedStateHandle: SavedStateHandle,
|
private val savedStateHandle: SavedStateHandle,
|
||||||
private val repository: MediaSendRepository,
|
private val repository: MediaSendRepository,
|
||||||
private val preUploadManager: PreUploadManager
|
private val preUploadController: PreUploadController
|
||||||
) : ViewModel(), MediaSendCallback {
|
) : ViewModel(), MediaSendCallback {
|
||||||
|
|
||||||
private val defaultState = MediaSendState(
|
private val defaultState = MediaSendState(
|
||||||
@@ -309,8 +308,8 @@ class MediaSendViewModel(
|
|||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
repository.deleteBlobs(media.toList())
|
repository.deleteBlobs(media.toList())
|
||||||
}
|
}
|
||||||
preUploadManager.cancelUpload(media)
|
preUploadController.cancelUpload(media)
|
||||||
preUploadManager.updateDisplayOrder(newSelection)
|
preUploadController.updateDisplayOrder(newSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -323,9 +322,9 @@ class MediaSendViewModel(
|
|||||||
val updatedSelection = snapshot.selectedMedia.map { oldToNew[it] ?: it }
|
val updatedSelection = snapshot.selectedMedia.map { oldToNew[it] ?: it }
|
||||||
updateState { copy(selectedMedia = updatedSelection) }
|
updateState { copy(selectedMedia = updatedSelection) }
|
||||||
|
|
||||||
preUploadManager.applyMediaUpdates(oldToNew, snapshot.recipientId)
|
preUploadController.applyMediaUpdates(oldToNew, snapshot.recipientId)
|
||||||
preUploadManager.updateCaptions(updatedSelection)
|
preUploadController.updateCaptions(updatedSelection)
|
||||||
preUploadManager.updateDisplayOrder(updatedSelection)
|
preUploadController.updateDisplayOrder(updatedSelection)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -333,7 +332,7 @@ class MediaSendViewModel(
|
|||||||
*/
|
*/
|
||||||
fun setDisplayOrder(mediaInOrder: List<Media>) {
|
fun setDisplayOrder(mediaInOrder: List<Media>) {
|
||||||
updateState { copy(selectedMedia = mediaInOrder) }
|
updateState { copy(selectedMedia = mediaInOrder) }
|
||||||
preUploadManager.updateDisplayOrder(mediaInOrder)
|
preUploadController.updateDisplayOrder(mediaInOrder)
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
@@ -350,9 +349,9 @@ class MediaSendViewModel(
|
|||||||
media.filter { ContentTypeUtil.isStorySupportedType(it.contentType) }
|
media.filter { ContentTypeUtil.isStorySupportedType(it.contentType) }
|
||||||
}
|
}
|
||||||
|
|
||||||
preUploadManager.startUpload(filteredPreUploadMedia, snapshot.recipientId)
|
preUploadController.startUpload(filteredPreUploadMedia, snapshot.recipientId)
|
||||||
preUploadManager.updateCaptions(snapshot.selectedMedia)
|
preUploadController.updateCaptions(snapshot.selectedMedia)
|
||||||
preUploadManager.updateDisplayOrder(snapshot.selectedMedia)
|
preUploadController.updateDisplayOrder(snapshot.selectedMedia)
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
@@ -369,7 +368,7 @@ class MediaSendViewModel(
|
|||||||
if (snapshot.sentMediaQuality == sentMediaQuality) return
|
if (snapshot.sentMediaQuality == sentMediaQuality) return
|
||||||
|
|
||||||
updateState { copy(sentMediaQuality = sentMediaQuality, isPreUploadEnabled = false) }
|
updateState { copy(sentMediaQuality = sentMediaQuality, isPreUploadEnabled = false) }
|
||||||
preUploadManager.cancelAllUploads()
|
preUploadController.cancelAllUploads()
|
||||||
|
|
||||||
// Re-clamp video durations based on new quality
|
// Re-clamp video durations based on new quality
|
||||||
val maxVideoDurationUs = getMaxVideoDurationUs()
|
val maxVideoDurationUs = getMaxVideoDurationUs()
|
||||||
@@ -404,7 +403,7 @@ class MediaSendViewModel(
|
|||||||
savedStateHandle[KEY_EDITED_VIDEO_URIS] = ArrayList(editedVideoUris)
|
savedStateHandle[KEY_EDITED_VIDEO_URIS] = ArrayList(editedVideoUris)
|
||||||
|
|
||||||
val media = state.value.selectedMedia.firstOrNull { it.uri == uri } ?: return
|
val media = state.value.selectedMedia.firstOrNull { it.uri == uri } ?: return
|
||||||
preUploadManager.cancelUpload(media)
|
preUploadController.cancelUpload(media)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -443,7 +442,7 @@ class MediaSendViewModel(
|
|||||||
if (unedited && durationEdited) {
|
if (unedited && durationEdited) {
|
||||||
val media = snapshot.selectedMedia.firstOrNull { it.uri == uri }
|
val media = snapshot.selectedMedia.firstOrNull { it.uri == uri }
|
||||||
if (media != null) {
|
if (media != null) {
|
||||||
preUploadManager.cancelUpload(media)
|
preUploadController.cancelUpload(media)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -532,7 +531,7 @@ class MediaSendViewModel(
|
|||||||
|
|
||||||
fun onMediaDragFinished() {
|
fun onMediaDragFinished() {
|
||||||
lastMediaDrag = Pair(0, 0)
|
lastMediaDrag = Pair(0, 0)
|
||||||
preUploadManager.updateDisplayOrder(internalState.value.selectedMedia)
|
preUploadController.updateDisplayOrder(internalState.value.selectedMedia)
|
||||||
}
|
}
|
||||||
|
|
||||||
//endregion
|
//endregion
|
||||||
@@ -704,8 +703,8 @@ class MediaSendViewModel(
|
|||||||
//region Lifecycle
|
//region Lifecycle
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
preUploadManager.cancelAllUploads()
|
preUploadController.cancelAllUploads()
|
||||||
preUploadManager.deleteAbandonedAttachments()
|
preUploadController.deleteAbandonedAttachments()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shouldPreUpload(metered: Boolean): Boolean = !metered
|
private fun shouldPreUpload(metered: Boolean): Boolean = !metered
|
||||||
@@ -715,26 +714,22 @@ class MediaSendViewModel(
|
|||||||
//region Factory
|
//region Factory
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
private val context: Context,
|
|
||||||
private val args: MediaSendActivityContract.Args,
|
private val args: MediaSendActivityContract.Args,
|
||||||
private val identityChangesSince: Long = System.currentTimeMillis(),
|
private val identityChangesSince: Long = System.currentTimeMillis(),
|
||||||
private val isMeteredFlow: Flow<Boolean>,
|
private val isMeteredFlow: Flow<Boolean>
|
||||||
private val repository: MediaSendRepository,
|
|
||||||
private val preUploadCallback: PreUploadManager.Callback
|
|
||||||
) : ViewModelProvider.Factory {
|
) : ViewModelProvider.Factory {
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
|
||||||
val savedStateHandle = extras.createSavedStateHandle()
|
val savedStateHandle = extras.createSavedStateHandle()
|
||||||
val manager = PreUploadManager(context.applicationContext, preUploadCallback)
|
|
||||||
|
|
||||||
return MediaSendViewModel(
|
return MediaSendViewModel(
|
||||||
args = args,
|
args = args,
|
||||||
identityChangesSince = identityChangesSince,
|
identityChangesSince = identityChangesSince,
|
||||||
isMeteredFlow = isMeteredFlow,
|
isMeteredFlow = isMeteredFlow,
|
||||||
savedStateHandle = savedStateHandle,
|
savedStateHandle = savedStateHandle,
|
||||||
repository = repository,
|
repository = MediaSendDependencies.mediaSendRepository,
|
||||||
preUploadManager = manager
|
preUploadController = PreUploadController()
|
||||||
) as T
|
) as T
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import org.signal.core.util.ThreadUtil
|
|||||||
import org.signal.core.util.concurrent.SignalExecutors
|
import org.signal.core.util.concurrent.SignalExecutors
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
import org.signal.mediasend.MediaRecipientId
|
import org.signal.mediasend.MediaRecipientId
|
||||||
|
import org.signal.mediasend.MediaSendDependencies
|
||||||
import java.util.LinkedHashMap
|
import java.util.LinkedHashMap
|
||||||
import java.util.concurrent.Executor
|
import java.util.concurrent.Executor
|
||||||
|
|
||||||
@@ -27,12 +28,10 @@ import java.util.concurrent.Executor
|
|||||||
*
|
*
|
||||||
* This class is stateful.
|
* This class is stateful.
|
||||||
*/
|
*/
|
||||||
class PreUploadManager(
|
class PreUploadController {
|
||||||
context: Context,
|
|
||||||
private val callback: Callback
|
|
||||||
) {
|
|
||||||
|
|
||||||
private val context: Context = context.applicationContext
|
private val callback: PreUploadRepository = MediaSendDependencies.preUploadRepository
|
||||||
|
private val context: Context = MediaSendDependencies.application
|
||||||
private val uploadResults: LinkedHashMap<Media, PreUploadResult> = LinkedHashMap()
|
private val uploadResults: LinkedHashMap<Media, PreUploadResult> = LinkedHashMap()
|
||||||
private val executor: Executor =
|
private val executor: Executor =
|
||||||
SignalExecutors.newCachedSingleThreadExecutor("signal-PreUpload", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD)
|
SignalExecutors.newCachedSingleThreadExecutor("signal-PreUpload", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD)
|
||||||
@@ -234,74 +233,7 @@ class PreUploadManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Callbacks that perform the real side-effects (DB ops, job scheduling/cancelation, etc).
|
|
||||||
*
|
|
||||||
* This keeps `feature/media-send` free of direct dependencies on app-specific systems.
|
|
||||||
*
|
|
||||||
* Threading: all callback methods are invoked on this manager's serialized background executor
|
|
||||||
* thread (i.e., not the main thread).
|
|
||||||
*/
|
|
||||||
interface Callback {
|
|
||||||
/**
|
|
||||||
* Performs pre-upload side-effects (e.g., create attachment state + enqueue jobs).
|
|
||||||
*
|
|
||||||
* @param context Application context.
|
|
||||||
* @param media The media item being pre-uploaded.
|
|
||||||
* @param recipientId Optional recipient identifier, if known.
|
|
||||||
* @return A [PreUploadResult] if enqueued, or `null` if it failed or should not pre-upload.
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun preUpload(context: Context, media: Media, recipientId: MediaRecipientId?): PreUploadResult?
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Cancels any scheduled/running work for the provided job ids.
|
|
||||||
*
|
|
||||||
* @param context Application context.
|
|
||||||
* @param jobIds Job identifiers to cancel.
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun cancelJobs(context: Context, jobIds: List<String>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes any persisted attachment state for [attachmentId].
|
|
||||||
*
|
|
||||||
* @param context Application context.
|
|
||||||
* @param attachmentId Attachment identifier to delete.
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun deleteAttachment(context: Context, attachmentId: Long)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the caption for [attachmentId].
|
|
||||||
*
|
|
||||||
* @param context Application context.
|
|
||||||
* @param attachmentId Attachment identifier.
|
|
||||||
* @param caption New caption (or `null` to clear).
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun updateAttachmentCaption(context: Context, attachmentId: Long, caption: String?)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates display order for attachments.
|
|
||||||
*
|
|
||||||
* @param context Application context.
|
|
||||||
* @param orderMap Map of attachment id -> display order index.
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun updateDisplayOrder(context: Context, orderMap: Map<Long, Int>)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes any pre-uploaded attachments that are no longer referenced.
|
|
||||||
*
|
|
||||||
* @param context Application context.
|
|
||||||
* @return The number of attachments deleted.
|
|
||||||
*/
|
|
||||||
@WorkerThread
|
|
||||||
fun deleteAbandonedPreuploadedAttachments(context: Context): Int
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
private companion object {
|
||||||
private val TAG = Log.tag(PreUploadManager::class.java)
|
private val TAG = Log.tag(PreUploadController::class.java)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2026 Signal Messenger, LLC
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.signal.mediasend.preupload
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
import org.signal.core.models.media.Media
|
||||||
|
import org.signal.mediasend.MediaRecipientId
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callbacks that perform the real side-effects (DB ops, job scheduling/cancelation, etc).
|
||||||
|
*
|
||||||
|
* This keeps `feature/media-send` free of direct dependencies on app-specific systems.
|
||||||
|
*
|
||||||
|
* Threading: all callback methods are invoked on this manager's serialized background executor
|
||||||
|
* thread (i.e., not the main thread).
|
||||||
|
*/
|
||||||
|
interface PreUploadRepository {
|
||||||
|
/**
|
||||||
|
* Performs pre-upload side-effects (e.g., create attachment state + enqueue jobs).
|
||||||
|
*
|
||||||
|
* @param context Application context.
|
||||||
|
* @param media The media item being pre-uploaded.
|
||||||
|
* @param recipientId Optional recipient identifier, if known.
|
||||||
|
* @return A [PreUploadResult] if enqueued, or `null` if it failed or should not pre-upload.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun preUpload(context: Context, media: Media, recipientId: MediaRecipientId?): PreUploadResult?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels any scheduled/running work for the provided job ids.
|
||||||
|
*
|
||||||
|
* @param context Application context.
|
||||||
|
* @param jobIds Job identifiers to cancel.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun cancelJobs(context: Context, jobIds: List<String>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes any persisted attachment state for [attachmentId].
|
||||||
|
*
|
||||||
|
* @param context Application context.
|
||||||
|
* @param attachmentId Attachment identifier to delete.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun deleteAttachment(context: Context, attachmentId: Long)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the caption for [attachmentId].
|
||||||
|
*
|
||||||
|
* @param context Application context.
|
||||||
|
* @param attachmentId Attachment identifier.
|
||||||
|
* @param caption New caption (or `null` to clear).
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun updateAttachmentCaption(context: Context, attachmentId: Long, caption: String?)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates display order for attachments.
|
||||||
|
*
|
||||||
|
* @param context Application context.
|
||||||
|
* @param orderMap Map of attachment id -> display order index.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun updateDisplayOrder(context: Context, orderMap: Map<Long, Int>)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes any pre-uploaded attachments that are no longer referenced.
|
||||||
|
*
|
||||||
|
* @param context Application context.
|
||||||
|
* @return The number of attachments deleted.
|
||||||
|
*/
|
||||||
|
@WorkerThread
|
||||||
|
fun deleteAbandonedPreuploadedAttachments(context: Context): Int
|
||||||
|
}
|
||||||
@@ -59,6 +59,7 @@ dependencies {
|
|||||||
implementation(libs.google.libphonenumber)
|
implementation(libs.google.libphonenumber)
|
||||||
|
|
||||||
// Testing
|
// Testing
|
||||||
|
testImplementation(testFixtures(project(":core:ui")))
|
||||||
testImplementation(testLibs.junit.junit)
|
testImplementation(testLibs.junit.junit)
|
||||||
testImplementation(testLibs.mockk)
|
testImplementation(testLibs.mockk)
|
||||||
testImplementation(testLibs.assertk)
|
testImplementation(testLibs.assertk)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
import org.signal.core.ui.CoreUiDependenciesRule
|
||||||
import org.signal.core.ui.compose.theme.SignalTheme
|
import org.signal.core.ui.compose.theme.SignalTheme
|
||||||
import org.signal.registration.screens.util.MockMultiplePermissionsState
|
import org.signal.registration.screens.util.MockMultiplePermissionsState
|
||||||
import org.signal.registration.screens.util.MockPermissionsState
|
import org.signal.registration.screens.util.MockPermissionsState
|
||||||
@@ -37,6 +38,9 @@ class RegistrationNavigationTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createComposeRule()
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val coreUiDependenciesRule = CoreUiDependenciesRule()
|
||||||
|
|
||||||
private lateinit var viewModel: RegistrationViewModel
|
private lateinit var viewModel: RegistrationViewModel
|
||||||
private lateinit var mockRepository: RegistrationRepository
|
private lateinit var mockRepository: RegistrationRepository
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
import org.signal.core.ui.CoreUiDependenciesRule
|
||||||
import org.signal.core.ui.compose.theme.SignalTheme
|
import org.signal.core.ui.compose.theme.SignalTheme
|
||||||
import org.signal.registration.test.TestTags
|
import org.signal.registration.test.TestTags
|
||||||
|
|
||||||
@@ -29,6 +30,9 @@ class PhoneNumberScreenTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createComposeRule()
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val coreUiDependenciesRule = CoreUiDependenciesRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `Next button is disabled when fields are empty`() {
|
fun `Next button is disabled when fields are empty`() {
|
||||||
// Given
|
// Given
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
import org.signal.core.ui.CoreUiDependenciesRule
|
||||||
import org.signal.core.ui.compose.theme.SignalTheme
|
import org.signal.core.ui.compose.theme.SignalTheme
|
||||||
import org.signal.registration.test.TestTags
|
import org.signal.registration.test.TestTags
|
||||||
|
|
||||||
@@ -31,6 +32,9 @@ class VerificationCodeScreenTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createComposeRule()
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val coreUiDependenciesRule = CoreUiDependenciesRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `screen displays title`() {
|
fun `screen displays title`() {
|
||||||
// Given
|
// Given
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import org.junit.Test
|
|||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
import org.robolectric.RobolectricTestRunner
|
import org.robolectric.RobolectricTestRunner
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
|
import org.signal.core.ui.CoreUiDependenciesRule
|
||||||
import org.signal.core.ui.compose.theme.SignalTheme
|
import org.signal.core.ui.compose.theme.SignalTheme
|
||||||
import org.signal.registration.test.TestTags
|
import org.signal.registration.test.TestTags
|
||||||
|
|
||||||
@@ -30,6 +31,9 @@ class WelcomeScreenTest {
|
|||||||
@get:Rule
|
@get:Rule
|
||||||
val composeTestRule = createComposeRule()
|
val composeTestRule = createComposeRule()
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val coreUiDependenciesRule = CoreUiDependenciesRule()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when Get Started is clicked, Continue event is emitted`() {
|
fun `when Get Started is clicked, Continue event is emitted`() {
|
||||||
// Given
|
// Given
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ org.gradle.java.installations.auto-download=false
|
|||||||
# Prevents lint crash when analyzing uncompiled kotlin gradle scripts
|
# Prevents lint crash when analyzing uncompiled kotlin gradle scripts
|
||||||
android.lint.useK2Uast=false
|
android.lint.useK2Uast=false
|
||||||
|
|
||||||
|
# Test fixtures support for Android modules
|
||||||
|
android.experimental.enableTestFixturesKotlinSupport=true
|
||||||
|
|
||||||
# Uncomment these to build libsignal from source.
|
# Uncomment these to build libsignal from source.
|
||||||
# libsignalClientPath=../libsignal
|
# libsignalClientPath=../libsignal
|
||||||
# org.gradle.dependency.verification=lenient
|
# org.gradle.dependency.verification=lenient
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ object SignalGlideDependencies {
|
|||||||
private lateinit var _application: Application
|
private lateinit var _application: Application
|
||||||
private lateinit var _provider: Provider
|
private lateinit var _provider: Provider
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@Synchronized
|
@Synchronized
|
||||||
fun init(application: Application, provider: Provider) {
|
fun init(application: Application, provider: Provider) {
|
||||||
if (this::_application.isInitialized || this::_provider.isInitialized) {
|
if (this::_application.isInitialized || this::_provider.isInitialized) {
|
||||||
|
|||||||
Reference in New Issue
Block a user