From c1730c3a1757f15a3ca80443a650989f61718e13 Mon Sep 17 00:00:00 2001 From: Infi Date: Wed, 24 Sep 2025 00:24:05 +0200 Subject: [PATCH] feat(telecom): init Signed-off-by: Infi --- app/build.gradle.kts | 1 + app/src/main/AndroidManifest.xml | 6 + .../activities/voice/IncomingActivity.kt | 289 ++++++++++++++++++ .../revolt/screens/labs/LabsHomeScreen.kt | 9 + .../revolt/screens/labs/LabsRootScreen.kt | 4 + .../screens/labs/ui/sandbox/TelecomSandbox.kt | 121 ++++++++ .../main/res/drawable/icn_call_24dp__fill.xml | 10 + 7 files changed, 440 insertions(+) create mode 100644 app/src/main/java/chat/revolt/activities/voice/IncomingActivity.kt create mode 100644 app/src/main/java/chat/revolt/screens/labs/ui/sandbox/TelecomSandbox.kt create mode 100644 app/src/main/res/drawable/icn_call_24dp__fill.xml 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 @@ + + +