Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions compose/snippets/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ dependencies {
implementation(libs.androidx.glance.material3)

implementation(libs.androidx.window.core)
implementation(libs.androidx.window)

implementation(libs.accompanist.theme.adapter.appcompat)
implementation(libs.accompanist.theme.adapter.material3)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.compose.snippets.adaptivelayouts

import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.RequiresWindowSdkExtension
import androidx.window.WindowSdkExtensions
import androidx.window.layout.FoldingFeature
import androidx.window.layout.SupportedPosture
import androidx.window.layout.WindowInfoTracker
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.contract
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

// [START android_adaptive_fold_aware_flows]
class DisplayFeaturesActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
// [START_EXCLUDE]
super.onCreate(savedInstanceState)
// [END_EXCLUDE]

lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
WindowInfoTracker.getOrCreate(this@DisplayFeaturesActivity)
.windowLayoutInfo(this@DisplayFeaturesActivity)
.collect { newLayoutInfo ->
// Use newLayoutInfo to update the layout.
}
}
}
}
}
// [END android_adaptive_fold_aware_flows]

class MainActivity : ComponentActivity() {

// [START android_adaptive_fold_feature]
override fun onCreate(savedInstanceState: Bundle?) {
// [START_EXCLUDE]
super.onCreate(savedInstanceState)
// [END_EXCLUDE]
lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collects from WindowInfoTracker when the lifecycle is
// STARTED and stops collection when the lifecycle is STOPPED.
WindowInfoTracker.getOrCreate(this@MainActivity)
.windowLayoutInfo(this@MainActivity)
.collect { layoutInfo ->
// New posture information.
val foldingFeature = layoutInfo.displayFeatures
.filterIsInstance<FoldingFeature>()
.firstOrNull()
// Use information from the foldingFeature object.
}
}
}
}
// [END android_adaptive_fold_feature]
}

@OptIn(ExperimentalContracts::class)
// [START android_adaptive_tabletop_posture]
fun isTableTopPosture(foldFeature: FoldingFeature?): Boolean {
contract { returns(true) implies (foldFeature != null) }
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.HORIZONTAL
}
// [END android_adaptive_tabletop_posture]

@RequiresWindowSdkExtension(6)
fun checkSupportedPostures(context: Context) {
// [START android_adaptive_supported_postures]
if (WindowSdkExtensions.getInstance().extensionVersion >= 6) {
val postures = WindowInfoTracker.getOrCreate(context).supportedPostures
if (postures.contains(SupportedPosture.TABLETOP)) {
// Device supports tabletop posture.
}
}
// [END android_adaptive_supported_postures]
}

@OptIn(ExperimentalContracts::class)
// [START android_adaptive_book_posture]
fun isBookPosture(foldFeature: FoldingFeature?): Boolean {
contract { returns(true) implies (foldFeature != null) }
return foldFeature?.state == FoldingFeature.State.HALF_OPENED &&
foldFeature.orientation == FoldingFeature.Orientation.VERTICAL
}
// [END android_adaptive_book_posture]
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.compose.snippets.adaptivelayouts

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.core.content.ContextCompat
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.area.WindowAreaCapability
import androidx.window.area.WindowAreaController
import androidx.window.area.WindowAreaInfo
import androidx.window.area.WindowAreaPresentationSessionCallback
import androidx.window.area.WindowAreaSession
import androidx.window.area.WindowAreaSessionCallback
import androidx.window.area.WindowAreaSessionPresenter
import androidx.window.core.ExperimentalWindowApi
import java.util.concurrent.Executor
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch

@OptIn(ExperimentalWindowApi::class)
// [START android_adaptive_window_callback]
class ExampleActivity : ComponentActivity(), WindowAreaPresentationSessionCallback {
// [END android_adaptive_window_callback]

val logTag = "FoldableModes"

// [START android_adaptive_foldable_vars]
private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var windowAreaSession: WindowAreaSession? = null
private var windowAreaInfo: WindowAreaInfo? = null
private var capabilityStatus: WindowAreaCapability.Status =
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED

private val dualScreenOperation = WindowAreaCapability.Operation.OPERATION_PRESENT_ON_AREA
private val rearDisplayOperation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA
// [END android_adaptive_foldable_vars]

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val operation = dualScreenOperation

// [START android_adaptive_foldable_init]
displayExecutor = ContextCompat.getMainExecutor(this)
windowAreaController = WindowAreaController.getOrCreate()

lifecycleScope.launch(Dispatchers.Main) {
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
windowAreaController.windowAreaInfos
.map { info -> info.firstOrNull { it.type == WindowAreaInfo.Type.TYPE_REAR_FACING } }
.onEach { info -> windowAreaInfo = info }
.map { it?.getCapability(operation)?.status ?: WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED }
.distinctUntilChanged()
.collect {
capabilityStatus = it
}
}
}
// [END android_adaptive_foldable_init]
}

fun checkCapability() {
// [START android_adaptive_capability_check]
when (capabilityStatus) {
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED -> {
// The selected display mode is not supported on this device.
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNAVAILABLE -> {
// The selected display mode is not available.
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_AVAILABLE -> {
// The selected display mode is available and can be enabled.
}
WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE -> {
// The selected display mode is already active.
}
else -> {
// The selected display mode status is unknown.
}
}
// [END android_adaptive_capability_check]
}

// [START android_adaptive_toggle_dual_screen]
fun toggleDualScreenMode() {
if (windowAreaSession != null) {
windowAreaSession?.close()
}
else {
windowAreaInfo?.token?.let { token ->
windowAreaController.presentContentOnWindowArea(
token = token,
activity = this,
executor = displayExecutor,
windowAreaPresentationSessionCallback = this
)
}
}
}
// [END android_adaptive_toggle_dual_screen]

// [START android_adaptive_session_callbacks]
override fun onSessionStarted(session: WindowAreaSessionPresenter) {
windowAreaSession = session
session.setContentView(ComposeView(session.context).apply {
setContent {
MyScreen()
}
})
}

override fun onSessionEnded(t: Throwable?) {
if (t != null) {
Log.e(logTag, "Something was broken: ${t.message}")
}
}
Comment on lines +140 to +144
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The windowAreaSession variable should be reset to null when the session ends. This ensures that the toggleDualScreenMode logic correctly identifies that a new session needs to be started on the next interaction.

Suggested change
override fun onSessionEnded(t: Throwable?) {
if (t != null) {
Log.e(logTag, "Something was broken: ${t.message}")
}
}
override fun onSessionEnded(t: Throwable?) {
windowAreaSession = null
if (t != null) {
Log.e(logTag, "Something was broken: ${t.message}")
}
}


override fun onContainerVisibilityChanged(isVisible: Boolean) {
Log.d(logTag, "onContainerVisibilityChanged. isVisible = $isVisible")
}
// [END android_adaptive_session_callbacks]

@Composable
fun MyScreen() {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Text(text = "Hello world")
}
}
}

@OptIn(ExperimentalWindowApi::class)
class RearDisplayActivity : ComponentActivity(), WindowAreaSessionCallback {

val logTag = "RearDisplayActivity"

private lateinit var windowAreaController: WindowAreaController
private lateinit var displayExecutor: Executor
private var windowAreaSession: WindowAreaSession? = null
private var windowAreaInfo: WindowAreaInfo? = null
private var capabilityStatus: WindowAreaCapability.Status =
WindowAreaCapability.Status.WINDOW_AREA_STATUS_UNSUPPORTED
private val operation = WindowAreaCapability.Operation.OPERATION_TRANSFER_ACTIVITY_TO_AREA

// [START android_adaptive_toggle_rear_display]
fun toggleRearDisplayMode() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The RearDisplayActivity is missing initialization logic for its properties. windowAreaController and displayExecutor are lateinit and will cause a crash if toggleRearDisplayMode is called. Additionally, there is no code to update capabilityStatus or windowAreaInfo, which are required for the toggle logic to function. Consider adding an onCreate block similar to the one in ExampleActivity.

if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Missing space after if keyword.

Suggested change
if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {
if (capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) {

if(windowAreaSession == null) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Missing space after if keyword.

Suggested change
if(windowAreaSession == null) {
if (windowAreaSession == null) {

windowAreaSession = windowAreaInfo?.getActiveSession(
operation
)
}
windowAreaSession?.close()
} else {
windowAreaInfo?.token?.let { token ->
windowAreaController.transferActivityToWindowArea(
token = token,
activity = this,
executor = displayExecutor,
windowAreaSessionCallback = this
)
}
}
}
// [END android_adaptive_toggle_rear_display]

// [START android_adaptive_rear_display_callbacks]
override fun onSessionStarted(session: WindowAreaSession) {
Log.d(logTag, "onSessionStarted")
}
Comment on lines +198 to +200
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The windowAreaSession variable must be updated when the session starts so that it can be tracked and closed later.

Suggested change
override fun onSessionStarted(session: WindowAreaSession) {
Log.d(logTag, "onSessionStarted")
}
override fun onSessionStarted(session: WindowAreaSession) {
windowAreaSession = session
Log.d(logTag, "onSessionStarted")
}


override fun onSessionEnded(t: Throwable?) {
if (t != null) {
Log.e(logTag, "Something was broken: ${t.message}")
}
}
Comment on lines +202 to +206
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The windowAreaSession variable should be reset to null when the session ends to maintain correct state for the toggle logic.

Suggested change
override fun onSessionEnded(t: Throwable?) {
if (t != null) {
Log.e(logTag, "Something was broken: ${t.message}")
}
}
override fun onSessionEnded(t: Throwable?) {
windowAreaSession = null
if (t != null) {
Log.e(logTag, "Something was broken: ${t.message}")
}
}

// [END android_adaptive_rear_display_callbacks]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2026 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.example.compose.snippets.adaptivelayouts

import android.content.Context
import android.content.Intent
import android.net.Uri
import android.view.Display
import android.view.WindowManager
import androidx.activity.ComponentActivity

class MultiWindowActivity : ComponentActivity() {

// [START android_adaptive_launch_adjacent]
fun openUrlInAdjacentWindow(url: String) {
Intent(Intent.ACTION_VIEW).apply {
data = Uri.parse(url)
addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT or Intent.FLAG_ACTIVITY_NEW_TASK)
}.also { intent -> startActivity(intent) }
}
// [END android_adaptive_launch_adjacent]

// [START android_adaptive_top_resumed]
override fun onTopResumedActivityChanged(topResumed: Boolean) {
super.onTopResumedActivityChanged(topResumed)
if (topResumed) {
// Top resumed activity.
// Can be a signal to re-acquire exclusive resources.
} else {
// No longer the top resumed activity.
}
}
// [END android_adaptive_top_resumed]

fun showWindowMetrics(context: Context, display: Display) {
// [START android_adaptive_metrics_other_displays]
val windowMetrics = context.createDisplayContext(display)
.createWindowContext(WindowManager.LayoutParams.TYPE_APPLICATION, null)
.getSystemService(WindowManager::class.java)
.maximumWindowMetrics
// [END android_adaptive_metrics_other_displays]
}
}
Loading