-
Notifications
You must be signed in to change notification settings - Fork 357
Add code snippets for foldables and multi-window #883
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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}") | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| 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() { | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||||||||||||||||||
| if(capabilityStatus == WindowAreaCapability.Status.WINDOW_AREA_STATUS_ACTIVE) { | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||
| if(windowAreaSession == null) { | ||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||||||||||||||||||||||||
| 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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| override fun onSessionEnded(t: Throwable?) { | ||||||||||||||||||||||||
| if (t != null) { | ||||||||||||||||||||||||
| Log.e(logTag, "Something was broken: ${t.message}") | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
Comment on lines
+202
to
+206
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The
Suggested change
|
||||||||||||||||||||||||
| // [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] | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
windowAreaSessionvariable should be reset tonullwhen the session ends. This ensures that thetoggleDualScreenModelogic correctly identifies that a new session needs to be started on the next interaction.