Follow a native call into picture-in-picture from the app

supportsPictureInPicture and smallestScreenSize have to be declared here, because a library cannot change a host Activity's manifest entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Valere
2026-09-22 21:31:03 +02:00
committed by Valere Fedronic
parent e786e2d22f
commit f052f755b6
5 changed files with 39 additions and 10 deletions
+2 -1
View File
@@ -53,9 +53,10 @@
--> -->
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|uiMode" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|keyboardHidden|uiMode"
android:exported="true" android:exported="true"
android:launchMode="singleTask" android:launchMode="singleTask"
android:supportsPictureInPicture="true"
android:theme="@style/Theme.ElementX.Splash" android:theme="@style/Theme.ElementX.Splash"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter> <intent-filter>
@@ -70,6 +70,19 @@ class MainActivity : NodeActivity() {
} }
} }
/**
* The pre-Android 12 way into picture-in-picture for a native call.
*
* Everything else about it is installed where the call is drawn, which knows whose call it is.
* This cannot be: an Activity override has no session in scope. From Android 12 the system is told
* up front that this Activity would like to shrink, and the call handles it without ever coming
* through here.
*/
override fun onUserLeaveHint() {
super.onUserLeaveHint()
appBindings.nativeCallPip().onUserLeaveHint(this)
}
@Composable @Composable
private fun MainContent(appBindings: AppBindings) { private fun MainContent(appBindings: AppBindings) {
val migrationState = appBindings.migrationEntryPoint().present() val migrationState = appBindings.migrationEntryPoint().present()
@@ -12,6 +12,7 @@ import dev.zacsweers.metro.AppScope
import dev.zacsweers.metro.ContributesTo import dev.zacsweers.metro.ContributesTo
import dev.zacsweers.metro.Multibinds import dev.zacsweers.metro.Multibinds
import io.element.android.features.api.MigrationEntryPoint import io.element.android.features.api.MigrationEntryPoint
import io.element.android.features.callnative.api.NativeCallPip
import io.element.android.features.enterprise.api.AppStartupHook import io.element.android.features.enterprise.api.AppStartupHook
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.lockscreen.api.LockScreenEntryPoint import io.element.android.features.lockscreen.api.LockScreenEntryPoint
@@ -52,6 +53,13 @@ interface AppBindings {
fun buildMeta(): BuildMeta fun buildMeta(): BuildMeta
/**
* For picture-in-picture. Only an Activity is told it is being left, and only the call knows
* whether that is worth shrinking for, so `MainActivity` needs this much of the native call and
* no more.
*/
fun nativeCallPip(): NativeCallPip
fun sentrySdkDsn(): SentrySdkDsn? fun sentrySdkDsn(): SentrySdkDsn?
@Multibinds(allowEmpty = true) @Multibinds(allowEmpty = true)
+1
View File
@@ -50,6 +50,7 @@ dependencies {
implementation(libs.coil) implementation(libs.coil)
implementation(projects.features.announcement.api) implementation(projects.features.announcement.api)
implementation(projects.features.callnative.api)
implementation(projects.features.ftue.api) implementation(projects.features.ftue.api)
implementation(projects.features.linknewdevice.api) implementation(projects.features.linknewdevice.api)
implementation(projects.features.share.api) implementation(projects.features.share.api)
@@ -48,6 +48,7 @@ import io.element.android.appnav.room.RoomFlowNode
import io.element.android.appnav.room.RoomNavigationTarget import io.element.android.appnav.room.RoomNavigationTarget
import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode import io.element.android.appnav.room.joined.JoinedRoomLoadedFlowNode
import io.element.android.compound.colors.SemanticColorsLightDark import io.element.android.compound.colors.SemanticColorsLightDark
import io.element.android.features.callnative.api.NativeCallHost
import io.element.android.features.createroom.api.CreateRoomEntryPoint import io.element.android.features.createroom.api.CreateRoomEntryPoint
import io.element.android.features.enterprise.api.EnterpriseService import io.element.android.features.enterprise.api.EnterpriseService
import io.element.android.features.enterprise.api.SessionEnterpriseService import io.element.android.features.enterprise.api.SessionEnterpriseService
@@ -144,6 +145,7 @@ class LoggedInFlowNode(
private val shareEntryPoint: ShareEntryPoint, private val shareEntryPoint: ShareEntryPoint,
private val matrixClient: MatrixClient, private val matrixClient: MatrixClient,
private val sendingQueue: SendQueues, private val sendingQueue: SendQueues,
private val nativeCallHost: NativeCallHost,
private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint, private val incomingVerificationEntryPoint: IncomingVerificationEntryPoint,
private val mediaPreviewConfigMigration: MediaPreviewConfigMigration, private val mediaPreviewConfigMigration: MediaPreviewConfigMigration,
private val sessionEnterpriseService: SessionEnterpriseService, private val sessionEnterpriseService: SessionEnterpriseService,
@@ -701,15 +703,19 @@ class LoggedInFlowNode(
) { ) {
CompositionLocalProvider(LocalMapTilerConfig provides updatedMapTilerConfig) { CompositionLocalProvider(LocalMapTilerConfig provides updatedMapTilerConfig) {
val isOnline by syncService.isOnline.collectAsState() val isOnline by syncService.isOnline.collectAsState()
ConnectivityIndicatorContainer( // Outside the connectivity indicator so the two strips stack rather than fight: an
isOnline = isOnline, // offline banner during a call belongs under the call bar, not over it.
modifier = modifier, nativeCallHost.Render(modifier = modifier) { callContentModifier ->
) { contentModifier -> ConnectivityIndicatorContainer(
Box(modifier = contentModifier) { isOnline = isOnline,
val ftueState by ftueService.state.collectAsState() modifier = callContentModifier,
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack)) ) { contentModifier ->
if (ftueState is FtueState.Complete) { Box(modifier = contentModifier) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent) val ftueState by ftueService.state.collectAsState()
BackstackView(transitionHandler = rememberLoggedInFlowTransitionHandler(backstack))
if (ftueState is FtueState.Complete) {
PermanentChild(permanentNavModel = permanentNavModel, navTarget = NavTarget.LoggedInPermanent)
}
} }
} }
} }