Skip to content
This repository was archived by the owner on Jul 11, 2025. It is now read-only.
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
19 changes: 16 additions & 3 deletions WindowManager/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,21 @@ android {

dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.core:core-ktx:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.window:window:1.0.0-alpha06'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.0-alpha01'

def window_version = '1.0.0-alpha07'
implementation "androidx.window:window:$window_version"

androidTestImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.arch.core:core-testing:2.1.0'

androidTestImplementation 'androidx.test:rules:1.3.0'
androidTestImplementation 'androidx.test:runner:1.4.0-beta02'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0-beta02'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-intents:3.3.0'
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/*
* Copyright 2021 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.windowmanagersample

import android.app.Activity

/**
* A test [Activity] for testing purposes.
*/
public class TestActivity : Activity()

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -19,59 +19,66 @@ package com.example.windowmanagersample
import android.graphics.drawable.ColorDrawable
import android.os.Bundle
import android.view.View
import androidx.core.util.Consumer
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.window.FoldingFeature
import androidx.window.WindowInfoRepo
import androidx.window.WindowLayoutInfo
import androidx.window.WindowManager
import com.example.windowmanagersample.backend.MidScreenFoldBackend
import androidx.window.windowInfoRepository
import com.example.windowmanagersample.databinding.ActivityDisplayFeaturesBinding
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

/** Demo activity that shows all display features and current device state on the screen. */
class DisplayFeaturesActivity : BaseSampleActivity() {
class DisplayFeaturesActivity : AppCompatActivity() {

private lateinit var windowManager: WindowManager
private val stateLog: StringBuilder = StringBuilder()

private val displayFeatureViews = ArrayList<View>()

// Store most recent values for the device state and window layout
private val stateContainer = StateContainer()
private lateinit var binding: ActivityDisplayFeaturesBinding
private lateinit var windowInfoRepo: WindowInfoRepo

@ExperimentalCoroutinesApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

binding = ActivityDisplayFeaturesBinding.inflate(layoutInflater)
setContentView(binding.root)

windowManager = getTestBackend()?.let { backend ->
binding.deviceStateToggleButton.visibility = View.VISIBLE
binding.deviceStateToggleButton.setOnClickListener {
if (backend is MidScreenFoldBackend) {
backend.toggleDeviceHalfOpenedState(this)
}
windowInfoRepo = windowInfoRepository()

// Create a new coroutine since repeatOnLifecycle is a suspend function
lifecycleScope.launch {
// The block passed to repeatOnLifecycle is executed when the lifecycle
// is at least STARTED and is cancelled when the lifecycle is STOPPED.
// It automatically restarts the block when the lifecycle is STARTED again.
lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
// Safely collect from windowInfoRepo when the lifecycle is STARTED
// and stops collection when the lifecycle is STOPPED
windowInfoRepo.windowLayoutInfo()
// Throttle first event 10ms to allow the UI to pickup the posture
.throttleFirst(10)
.collect { newLayoutInfo ->
// New posture information
updateStateLog(newLayoutInfo)
updateCurrentState(newLayoutInfo)
}
}
WindowManager(this, backend)
} ?: WindowManager(this)
}

stateLog.clear()
stateLog.append(getString(R.string.state_update_log)).append("\n")
}

override fun onStart() {
super.onStart()
windowManager.registerLayoutChangeCallback(mainThreadExecutor, stateContainer)
}

override fun onStop() {
super.onStop()
windowManager.unregisterLayoutChangeCallback(stateContainer)
}

/** Updates the device state and display feature positions. */
internal fun updateCurrentState(layoutInfo: WindowLayoutInfo?) {
private fun updateCurrentState(layoutInfo: WindowLayoutInfo) {
// Cleanup previously added feature views
val rootLayout = binding.featureContainerLayout
for (featureView in displayFeatureViews) {
Expand All @@ -82,62 +89,60 @@ class DisplayFeaturesActivity : BaseSampleActivity() {
// Update the UI with the current state
val stateStringBuilder = StringBuilder()

layoutInfo?.let { windowLayoutInfo ->
stateStringBuilder.append(getString(R.string.window_layout))
.append(": ")
stateStringBuilder.append(getString(R.string.window_layout))
.append(": ")

// Add views that represent display features
for (displayFeature in windowLayoutInfo.displayFeatures) {
val lp = getLayoutParamsForFeatureInFrameLayout(displayFeature, rootLayout)
?: continue
// Add views that represent display features
for (displayFeature in layoutInfo.displayFeatures) {
val lp = getLayoutParamsForFeatureInFrameLayout(displayFeature, rootLayout)
?: continue

// Make sure that zero-wide and zero-high features are still shown
if (lp.width == 0) {
lp.width = 1
}
if (lp.height == 0) {
lp.height = 1
}
// Make sure that zero-wide and zero-high features are still shown
if (lp.width == 0) {
lp.width = 1
}
if (lp.height == 0) {
lp.height = 1
}

val featureView = View(this)
val foldFeature = displayFeature as? FoldingFeature
val featureView = View(this)
val foldFeature = displayFeature as? FoldingFeature

val color = if (foldFeature != null) {
if (foldFeature.isSeparating) {
stateStringBuilder.append(getString(R.string.screens_are_separated))
getColor(R.color.color_feature_separating)
} else {
stateStringBuilder.append(getString(R.string.screens_are_not_separated))
getColor(R.color.color_feature_not_separating)
}
val color = if (foldFeature != null) {
if (foldFeature.isSeparating) {
stateStringBuilder.append(getString(R.string.screens_are_separated))
getColor(R.color.color_feature_separating)
} else {
getColor(R.color.color_feature_unknown)
}
if (foldFeature != null) {
stateStringBuilder
.append(" - ")
.append(
if (foldFeature.orientation == FoldingFeature.ORIENTATION_HORIZONTAL) {
getString(R.string.screen_is_horizontal)
} else {
getString(R.string.screen_is_vertical)
}
)
stateStringBuilder.append(getString(R.string.screens_are_not_separated))
getColor(R.color.color_feature_not_separating)
}
featureView.foreground = ColorDrawable(color)
} else {
getColor(R.color.color_feature_unknown)
}
if (foldFeature != null) {
stateStringBuilder
.append(" - ")
.append(
if (foldFeature.orientation == FoldingFeature.ORIENTATION_HORIZONTAL) {
getString(R.string.screen_is_horizontal)
} else {
getString(R.string.screen_is_vertical)
}
)
}
featureView.foreground = ColorDrawable(color)

rootLayout.addView(featureView, lp)
featureView.id = View.generateViewId()
rootLayout.addView(featureView, lp)
featureView.id = View.generateViewId()

displayFeatureViews.add(featureView)
}
displayFeatureViews.add(featureView)
}

binding.currentState.text = stateStringBuilder.toString()
}

/** Adds the current state to the text log of changes on screen. */
internal fun updateStateLog(layoutInfo: WindowLayoutInfo) {
private fun updateStateLog(layoutInfo: WindowLayoutInfo) {
stateLog.append(getCurrentTimeString())
.append(" ")
.append(layoutInfo)
Expand All @@ -150,14 +155,4 @@ class DisplayFeaturesActivity : BaseSampleActivity() {
val currentDate = sdf.format(Date())
return currentDate.toString()
}

inner class StateContainer : Consumer<WindowLayoutInfo> {
var lastLayoutInfo: WindowLayoutInfo? = null

override fun accept(newLayoutInfo: WindowLayoutInfo) {
updateStateLog(newLayoutInfo)
lastLayoutInfo = newLayoutInfo
updateCurrentState(lastLayoutInfo)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright 2021 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.windowmanagersample

import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.withContext

fun <T> Flow<T>.throttleFirst(waitForMillis: Long): Flow<T> = flow {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure on this function. @manuelvicnt

coroutineScope {
val context = coroutineContext
var delayPost: Deferred<Unit>? = null
var throttleEvent = true
collect {
delayPost?.cancel()
delayPost = async(Dispatchers.Default) {
if (throttleEvent) { delay(waitForMillis) }
withContext(context) {
emit(it)
throttleEvent = false
}
}
}
}
}
Loading