Add shared calling intent system.

This commit is contained in:
Alex Hart
2024-09-03 15:22:44 -03:00
committed by Cody Henthorne
parent e5b482c7ad
commit 4d23f11f6e
12 changed files with 277 additions and 90 deletions

View File

@@ -506,6 +506,14 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
sectionHeaderPref(DSLSettingsText.from("Calling options"))
switchPref(
title = DSLSettingsText.from("Use new calling UI"),
isChecked = state.newCallingUi,
onClick = {
viewModel.setUseNewCallingUi(!state.newCallingUi)
}
)
radioListPref(
title = DSLSettingsText.from("Audio processing method"),
listItems = CallManager.AudioProcessingMethod.values().map { it.name }.toTypedArray(),

View File

@@ -24,5 +24,6 @@ data class InternalSettingsState(
val pnpInitialized: Boolean,
val useConversationItemV2ForMedia: Boolean,
val hasPendingOneTimeDonation: Boolean,
val hevcEncoding: Boolean
val hevcEncoding: Boolean,
val newCallingUi: Boolean
)

View File

@@ -165,7 +165,8 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
pnpInitialized = SignalStore.misc.hasPniInitializedDevices,
useConversationItemV2ForMedia = SignalStore.internal.useConversationItemV2Media(),
hasPendingOneTimeDonation = SignalStore.inAppPayments.getPendingOneTimeDonation() != null,
hevcEncoding = SignalStore.internal.hevcEncoding
hevcEncoding = SignalStore.internal.hevcEncoding,
newCallingUi = SignalStore.internal.newCallingUi
)
fun onClearOnboardingState() {
@@ -176,6 +177,11 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
StoryOnboardingDownloadJob.enqueueIfNeeded()
}
fun setUseNewCallingUi(newCallingUi: Boolean) {
SignalStore.internal.newCallingUi = newCallingUi
refresh()
}
class Factory(private val repository: InternalSettingsRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return requireNotNull(modelClass.cast(InternalSettingsViewModel(repository)))

View File

@@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.webrtc.v2
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
@@ -91,6 +92,7 @@ class CallActivity : BaseActivity(), CallControlsCallback {
val callInfoCallbacks = CallInfoCallbacks(this, controlsAndInfoViewModel, compositeDisposable)
observeCallEvents()
viewModel.processCallIntent(CallIntent(intent))
setContent {
val lifecycleOwner = LocalLifecycleOwner.current
@@ -179,6 +181,13 @@ class CallActivity : BaseActivity(), CallControlsCallback {
}
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
if (intent != null) {
viewModel.processCallIntent(CallIntent(intent))
}
}
override fun onResume() {
Log.i(TAG, "onResume")
super.onResume()
@@ -203,13 +212,9 @@ class CallActivity : BaseActivity(), CallControlsCallback {
}
}
/*
TODO
if (enterPipOnResume) {
enterPipOnResume = false;
enterPipModeIfPossible();
if (viewModel.consumeEnterPipOnResume()) {
// TODO enterPipModeIfPossible()
}
*/
}
override fun onPause() {

View File

@@ -0,0 +1,138 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.components.webrtc.v2
import android.app.Activity
import android.content.Context
import android.content.Intent
import org.thoughtcrime.securesms.WebRtcCallActivity
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
/**
* CallIntent wraps an intent inside one of the call activities to allow for easy typed access to the necessary data within it.
*/
class CallIntent(
private val intent: Intent
) {
companion object {
private const val CALL_INTENT_PREFIX = "CallIntent"
private fun getActivityClass(): Class<out Activity> = if (RemoteConfig.newCallUi || SignalStore.internal.newCallingUi) {
CallActivity::class.java
} else {
WebRtcCallActivity::class.java
}
private fun getActionString(action: Action): String {
return "$CALL_INTENT_PREFIX.${action.code}"
}
private fun getExtraString(extra: Extra): String {
return "$CALL_INTENT_PREFIX.${extra.code}"
}
}
val action: Action by lazy { Action.fromIntent(intent) }
@get:JvmName("shouldEnableVideoIfAvailable")
var shouldEnableVideoIfAvailable: Boolean
get() = intent.getBooleanExtra(getExtraString(Extra.ENABLE_VIDEO_IF_AVAILABLE), false)
set(value) {
intent.putExtra(getExtraString(Extra.ENABLE_VIDEO_IF_AVAILABLE), value)
}
val isStartedFromFullScreen: Boolean by lazy { intent.getBooleanExtra(getExtraString(Extra.STARTED_FROM_FULLSCREEN), false) }
val isStartedFromCallLink: Boolean by lazy { intent.getBooleanExtra(getExtraString(Extra.STARTED_FROM_CALL_LINK), false) }
@get:JvmName("shouldLaunchInPip")
val shouldLaunchInPip: Boolean by lazy { intent.getBooleanExtra(getExtraString(Extra.LAUNCH_IN_PIP), false) }
override fun toString(): String {
return """
CallIntent
Action - $action
Enable video if available? $shouldEnableVideoIfAvailable
Started from full screen? $isStartedFromFullScreen
Started from call link? $isStartedFromCallLink
Launch in pip? $shouldLaunchInPip
""".trimIndent()
}
enum class Action(val code: String) {
VIEW(Intent.ACTION_VIEW),
ANSWER_AUDIO("ANSWER_ACTION"),
ANSWER_VIDEO("ANSWER_VIDEO_ACTION"),
DENY("DENY_ACTION"),
END_CALL("END_CALL_ACTION");
companion object {
fun fromIntent(intent: Intent): Action {
return intent.action?.let { a -> entries.firstOrNull { a == it.code || a == getActionString(it) } } ?: VIEW
}
}
}
private enum class Extra(val code: String) {
ENABLE_VIDEO_IF_AVAILABLE("ENABLE_VIDEO_IF_AVAILABLE"),
STARTED_FROM_FULLSCREEN("STARTED_FROM_FULLSCREEN"),
STARTED_FROM_CALL_LINK("STARTED_FROM_CALL_LINK"),
LAUNCH_IN_PIP("LAUNCH_IN_PIP")
}
/**
* Builds an intent to launch the call screen.
*/
class Builder(val context: Context) {
private val intent = Intent(context, getActivityClass())
init {
withAction(Action.VIEW)
}
fun withAddedIntentFlags(flags: Int): Builder {
intent.addFlags(flags)
return this
}
fun withIntentFlags(flags: Int): Builder {
intent.flags = flags
return this
}
fun withAction(action: Action?): Builder {
intent.action = action?.let { getActionString(action) }
return this
}
fun withEnableVideoIfAvailable(enableVideoIfAvailable: Boolean): Builder {
intent.putExtra(getExtraString(Extra.ENABLE_VIDEO_IF_AVAILABLE), enableVideoIfAvailable)
return this
}
fun withStartedFromFullScreen(startedFromFullScreen: Boolean): Builder {
intent.putExtra(getExtraString(Extra.STARTED_FROM_FULLSCREEN), startedFromFullScreen)
return this
}
fun withStartedFromCallLink(startedFromCallLink: Boolean): Builder {
intent.putExtra(getExtraString(Extra.STARTED_FROM_CALL_LINK), startedFromCallLink)
return this
}
fun withLaunchInPip(launchInPip: Boolean): Builder {
intent.putExtra(getExtraString(Extra.LAUNCH_IN_PIP), launchInPip)
return this
}
fun build(): Intent {
return intent
}
}
}

View File

@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.sms.MessageSender
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager
import org.whispersystems.signalservice.api.messages.calls.HangupMessage
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
/**
* Presentation logic and state holder for information that was generally done
@@ -48,10 +49,18 @@ class CallViewModel(
private var previousEvent: WebRtcViewModel? = null
private var enableVideoIfAvailable = false
private var lastProcessedIntentTimestamp = 0L
private var enterPipOnResume = false
private val internalCallScreenState = MutableStateFlow(CallScreenState())
val callScreenState: StateFlow<CallScreenState> = internalCallScreenState
fun consumeEnterPipOnResume(): Boolean {
val enter = enterPipOnResume
enterPipOnResume = false
return enter
}
fun unregisterEventBus() {
EventBus.getDefault().unregister(this)
}
@@ -337,4 +346,31 @@ class CallViewModel(
AppDependencies.signalCallManager.selectAudioDevice(SignalAudioManager.ChosenAudioDeviceIdentifier(managerDevice))
}
}
fun processCallIntent(callIntent: CallIntent) {
if (callIntent.action == CallIntent.Action.ANSWER_VIDEO) {
enableVideoIfAvailable = true
} else if (callIntent.action == CallIntent.Action.ANSWER_AUDIO || callIntent.isStartedFromFullScreen) {
enableVideoIfAvailable = false
} else {
enableVideoIfAvailable = callIntent.shouldEnableVideoIfAvailable
callIntent.shouldEnableVideoIfAvailable = false
}
when (callIntent.action) {
CallIntent.Action.ANSWER_AUDIO -> startCall(false)
CallIntent.Action.ANSWER_VIDEO -> startCall(true)
CallIntent.Action.DENY -> deny()
CallIntent.Action.END_CALL -> hangup()
CallIntent.Action.VIEW -> Unit
}
// Prevents some issues around intent re-use when dealing with picture-in-picture.
val now = System.currentTimeMillis()
if (now - lastProcessedIntentTimestamp > 1.seconds.inWholeMilliseconds) {
enterPipOnResume = callIntent.shouldLaunchInPip
}
lastProcessedIntentTimestamp = now
}
}