diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index a807dbcf..649f6b67 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -256,6 +256,7 @@ dependencies {
implementation("androidx.webkit:webkit:1.14.0")
implementation("androidx.core:core-splashscreen:1.2.0-beta02")
implementation("androidx.palette:palette-ktx:1.0.0")
+ implementation("androidx.core:core-telecom:1.0.0")
// Libraries used for legacy View-based UI
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index af21b348..607c92f7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -5,6 +5,7 @@
+
+
+
{
+ swiperState.animateTo(CallSwiperState.Initial)
+ }
+
+ CallSwiperState.Decline -> {
+ swiperState.animateTo(CallSwiperState.Initial)
+ }
+
+ else -> {}
+ }
+ }
+
+ val swiperRotation = remember(swiperState.offset) {
+ if (swiperState.offset.isNaN()) 0f
+ else {
+ val declineAnchor = 35f
+ val initialAnchor = (swiperWidthPx / 2f) - (swiperThirdPx / 2f)
+ val acceptAnchor = swiperWidthPx - swiperThirdPx - 35f
+
+ when {
+ swiperState.offset <= initialAnchor -> {
+ // Moving towards decline: interpolate from 0° to -90°
+ val progress =
+ (initialAnchor - swiperState.offset) / (initialAnchor - declineAnchor)
+ -225f * progress.coerceIn(0f, 1f)
+ }
+
+ swiperState.offset >= initialAnchor -> {
+ // Moving towards accept: interpolate from 0° to 90°
+ val progress =
+ (swiperState.offset - initialAnchor) / (acceptAnchor - initialAnchor)
+ 45f * progress.coerceIn(0f, 1f)
+ }
+
+ else -> 0f // At initial position
+ }
+ }
+ }
+
+ Box(
+ Modifier
+ .fillMaxSize()
+ .background(bgColour),
+ contentAlignment = Alignment.Center
+ ) {
+ Column(
+ verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.CenterVertically),
+ horizontalAlignment = Alignment.CenterHorizontally,
+ ) {
+ Text(
+ "Incoming Call on Revolt",
+ style = MaterialTheme.typography.headlineMedium,
+ fontSize = 24.sp
+ )
+ Text(
+ "cat",
+ style = MaterialTheme.typography.displayLargeEmphasized,
+ fontWeight = FontWeight.SemiBold
+ )
+ with(density) {
+ RemoteImage(
+ url = "https://cdn.revoltusercontent.com/attachments/K1CDpnvORz2fzUhgq47mcL7N4gccWGqNYYeGaJVvyp/image.png",
+ description = null,
+ modifier = Modifier
+ .clip(CircleShape)
+ .fillMaxWidth(0.5f)
+ .aspectRatio(1f),
+ )
+ }
+ Spacer(Modifier.fillMaxHeight(.33f))
+ Box(
+ modifier = Modifier
+ .clip(CircleShape)
+ .height(84.dp)
+ .width(swiperWidth)
+ .then(
+ if (isSystemInDarkTheme()) Modifier
+ .background(MaterialTheme.colorScheme.surfaceVariant)
+ .background(
+ Brush.linearGradient(
+ listOf(
+ presenceColour(Presence.Dnd).copy(alpha = 0.05f),
+ presenceColour(Presence.Online).copy(alpha = 0.05f),
+ )
+ )
+ ) else Modifier.background(MaterialTheme.colorScheme.surfaceBright)
+ ),
+ ) {
+ Text(
+ "Decline",
+ color = lerp(
+ presenceColour(Presence.Dnd),
+ MaterialTheme.colorScheme.onSurface,
+ swiperLabelColourLerpAnim
+ ),
+ modifier = Modifier
+ .align(Alignment.CenterStart)
+ .padding(start = 30.dp)
+ )
+ Text(
+ "Accept",
+ color = lerp(
+ presenceColour(Presence.Online),
+ MaterialTheme.colorScheme.onSurface,
+ swiperLabelColourLerpAnim
+ ),
+ modifier = Modifier
+ .align(Alignment.CenterEnd)
+ .padding(end = 30.dp)
+ )
+ Box(
+ contentAlignment = Alignment.Center,
+ modifier = Modifier
+ .width(swiperWidthThird)
+ .height(64.dp)
+ .offset {
+ IntOffset(
+ x = swiperState.requireOffset().roundToInt(),
+ y = with(density) { (84.dp - 64.dp).toPx() / 2 }.roundToInt()
+ )
+ }
+ .anchoredDraggable(
+ swiperState,
+ Orientation.Horizontal,
+ flingBehavior =
+ AnchoredDraggableDefaults.flingBehavior(
+ swiperState,
+ positionalThreshold = { distance -> distance * 0.25f },
+ ),
+ )
+ .clip(CircleShape)
+ .background(MaterialTheme.colorScheme.tertiaryContainer)
+ ) {
+ Icon(
+ painter = painterResource(R.drawable.icn_call_24dp__fill),
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.onTertiaryContainer,
+ modifier = Modifier
+ .rotate(swiperRotation)
+ )
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/chat/revolt/screens/labs/LabsHomeScreen.kt b/app/src/main/java/chat/revolt/screens/labs/LabsHomeScreen.kt
index 2f2db7aa..167376ff 100644
--- a/app/src/main/java/chat/revolt/screens/labs/LabsHomeScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/labs/LabsHomeScreen.kt
@@ -219,6 +219,15 @@ fun LabsHomeScreen(navController: NavController, topNav: NavController) {
}
)
HorizontalDivider()
+ ListItem(
+ headlineContent = {
+ Text("Telecom")
+ },
+ modifier = Modifier.clickable {
+ navController.navigate("sandboxes/telecom")
+ }
+ )
+ HorizontalDivider()
}
}
}
diff --git a/app/src/main/java/chat/revolt/screens/labs/LabsRootScreen.kt b/app/src/main/java/chat/revolt/screens/labs/LabsRootScreen.kt
index bb6ccd13..68cd4920 100644
--- a/app/src/main/java/chat/revolt/screens/labs/LabsRootScreen.kt
+++ b/app/src/main/java/chat/revolt/screens/labs/LabsRootScreen.kt
@@ -19,6 +19,7 @@ import chat.revolt.screens.labs.ui.sandbox.GradientEditorSandbox
import chat.revolt.screens.labs.ui.sandbox.JBMSandbox
import chat.revolt.screens.labs.ui.sandbox.NewCardSandboxScreen
import chat.revolt.screens.labs.ui.sandbox.SettingsDslSandbox
+import chat.revolt.screens.labs.ui.sandbox.TelecomSandbox
annotation class LabsFeature
@@ -87,6 +88,9 @@ fun LabsRootScreen(topNav: NavController) {
composable("sandboxes/newcard") {
NewCardSandboxScreen(labsNav)
}
+ composable("sandboxes/telecom") {
+ TelecomSandbox(labsNav)
+ }
}
}
}
diff --git a/app/src/main/java/chat/revolt/screens/labs/ui/sandbox/TelecomSandbox.kt b/app/src/main/java/chat/revolt/screens/labs/ui/sandbox/TelecomSandbox.kt
new file mode 100644
index 00000000..00e6e359
--- /dev/null
+++ b/app/src/main/java/chat/revolt/screens/labs/ui/sandbox/TelecomSandbox.kt
@@ -0,0 +1,121 @@
+package chat.revolt.screens.labs.ui.sandbox
+
+import android.content.Intent
+import android.os.Build
+import androidx.compose.foundation.layout.Column
+import androidx.compose.material3.Button
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.core.net.toUri
+import androidx.core.telecom.CallAttributesCompat
+import androidx.core.telecom.CallControlScope
+import androidx.core.telecom.CallsManager
+import androidx.navigation.NavController
+import chat.revolt.activities.voice.IncomingActivity
+import chat.revolt.settings.dsl.SettingsPage
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import logcat.LogPriority
+import logcat.asLog
+import logcat.logcat
+
+@Composable
+fun TelecomSandbox(navController: NavController) {
+ val context = LocalContext.current
+ val callsManager = remember {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) CallsManager(context) else null
+ }
+ val didRegister = remember { mutableStateOf(false) }
+ val callControlScope = remember { mutableStateOf(null) }
+ val scope = rememberCoroutineScope()
+
+ SettingsPage(
+ navController,
+ title = {
+ Text(
+ text = "Telecom Sandbox",
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis,
+ )
+ }
+ ) {
+ Column {
+ Button(
+ onClick = {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val capabilities: @CallsManager.Companion.Capability Int =
+ (CallsManager.CAPABILITY_BASELINE or
+ CallsManager.CAPABILITY_SUPPORTS_VIDEO_CALLING)
+
+ callsManager?.registerAppWithTelecom(capabilities)
+ didRegister.value = true
+ } else {
+ showSnackbar("Android version too old for Core-Telecom API")
+ }
+ }
+ ) {
+ Text(if (didRegister.value) "Registered" else "Register Revolt for Calls")
+ }
+
+ Button(
+ onClick = {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val address = "revolt:01F92C5ZXBQWQ8KY7J8KY917NM".toUri()
+ val attr = CallAttributesCompat(
+ displayName = "Jennifer",
+ address = address,
+ direction = CallAttributesCompat.DIRECTION_INCOMING,
+ callType = CallAttributesCompat.CALL_TYPE_AUDIO_CALL,
+ callCapabilities = 0
+ )
+ scope.launch {
+ delay(5000)
+ try {
+ callsManager?.addCall(
+ attr,
+ { someInt ->
+ logcat { "Call answered with $someInt" }
+ // answer the call here, e.g. start an activity or service...
+ }, // Watch needs to know if it can answer the call.
+ { cause ->
+ logcat { "Call ended with cause $cause" }
+ },
+ {
+ logcat { "Call state changed to active" }
+ },
+ {
+ logcat { "Call state changed to inactive (hold)" }
+ }
+ ) {
+ // The call was successfully added once this scope runs.
+ callControlScope.value = this
+ }
+ } catch (addCallException: Exception) {
+ logcat(LogPriority.ERROR) { addCallException.asLog() }
+ }
+ }
+ } else {
+ showSnackbar("Android version too old for Core-Telecom API")
+ }
+ }
+ ) {
+ Text("Place incoming call in 5 seconds")
+ }
+
+ Button(
+ onClick = {
+ val intent = Intent(context, IncomingActivity::class.java)
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+ context.startActivity(intent)
+ }
+ ) {
+ Text("Just open incoming call activity")
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/icn_call_24dp__fill.xml b/app/src/main/res/drawable/icn_call_24dp__fill.xml
new file mode 100644
index 00000000..83296e0f
--- /dev/null
+++ b/app/src/main/res/drawable/icn_call_24dp__fill.xml
@@ -0,0 +1,10 @@
+
+
+