Skip to content
This repository has been archived by the owner on Aug 15, 2023. It is now read-only.

Commit

Permalink
Merge pull request #3 from googlecodelabs/cevalien/wm-beta-01
Browse files Browse the repository at this point in the history
Update Jetpack WindowManager sample to v1.0.0-beta02 of the library.
  • Loading branch information
pfmaggi committed Sep 23, 2021
2 parents 48aa02e + e404280 commit 104971c
Show file tree
Hide file tree
Showing 5 changed files with 207 additions and 68 deletions.
8 changes: 4 additions & 4 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.4.21"
ext.kotlin_version = "1.5.0"
repositories {
google()
jcenter()
mavenCentral()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.1"
classpath "com.android.tools.build:gradle:4.1.3"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"

// NOTE: Do not place your application dependencies here; they belong
Expand All @@ -35,7 +35,7 @@ buildscript {
allprojects {
repositories {
google()
jcenter()
mavenCentral()
}
}

Expand Down
33 changes: 24 additions & 9 deletions window-manager/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ plugins {
}

android {
compileSdkVersion 30
compileSdkVersion 31

defaultConfig {
applicationId "com.codelab.foldables.window_manager"
Expand Down Expand Up @@ -51,19 +51,34 @@ android {
buildFeatures {
viewBinding true
}
// It seems there is an issue with an Android limitation already reported, when using
// kotlinx-coroutines-test.
// https://github.com/Kotlin/kotlinx.coroutines/issues/2023#issuecomment-858644393
// and https://issuetracker.google.com/161465530
// This is the current workaround to make it working:
packagingOptions {
exclude 'META-INF/AL2.0'
exclude 'META-INF/LGPL2.1'
}
}

dependencies {

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.2.1'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'androidx.core:core-ktx:1.6.0'
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'com.google.android.material:material:1.4.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0'

//Published
implementation "androidx.window:window:1.0.0-beta02"

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'

implementation "androidx.window:window:1.0.0-alpha03"
androidTestImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.0"

testImplementation 'junit:junit:4.13.1'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'
//Published
androidTestImplementation "androidx.window:window-testing:1.0.0-beta02"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/*
*
* * Copyright (C) 2021 Google Inc.
* *
* * 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
* *
* * http://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.codelab.foldables.window_manager

import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.assertion.PositionAssertions
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.rules.ActivityScenarioRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository
import androidx.window.layout.WindowLayoutInfo
import androidx.window.testing.layout.FoldingFeature
import androidx.window.testing.layout.WindowLayoutInfoPublisherRule
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.rules.RuleChain
import org.junit.rules.TestRule
import org.junit.runner.RunWith

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
class MainActivityTest {
private val activityRule = ActivityScenarioRule(MainActivity::class.java)
private val publisherRule = WindowLayoutInfoPublisherRule()

private val testScope = TestCoroutineScope()

@get:Rule
val testRule: TestRule

init {
testRule = RuleChain.outerRule(publisherRule).around(activityRule)
}

@Test
fun testText_is_left_of_Vertical_FoldingFeature() = runBlockingTest {
activityRule.scenario.onActivity { activity ->
val hinge = FoldingFeature(
activity = activity,
state = FoldingFeature.State.FLAT,
orientation = FoldingFeature.Orientation.VERTICAL,
size = 2
)
val expected =
WindowLayoutInfo.Builder().setDisplayFeatures(listOf(hinge)).build()

val value = testScope.async {
activity.windowInfoRepository().windowLayoutInfo.first()
}
publisherRule.overrideWindowLayoutInfo(expected)
runBlockingTest {
assertEquals(
expected,
value.await()
)
}
}
onView(withId(R.id.layout_change)).check(
PositionAssertions.isCompletelyLeftOf(withId(R.id.folding_feature))
)
}

@Test
fun testText_is_below_of_Horizontal_FoldingFeature() = runBlockingTest {
activityRule.scenario.onActivity { activity ->
val hinge = FoldingFeature(
activity = activity,
state = FoldingFeature.State.FLAT,
orientation = FoldingFeature.Orientation.HORIZONTAL,
size = 2
)
val expected =
WindowLayoutInfo.Builder().setDisplayFeatures(listOf(hinge)).build()

val value = testScope.async {
activity.windowInfoRepository().windowLayoutInfo.first()
}
publisherRule.overrideWindowLayoutInfo(expected)
runBlockingTest {
assertEquals(
expected,
value.await()
)
}
}
onView(withId(R.id.layout_change)).check(
PositionAssertions.isCompletelyBelow(withId(R.id.folding_feature))
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,113 +20,130 @@ package com.codelab.foldables.window_manager

import android.graphics.Rect
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.util.Consumer
import androidx.window.WindowLayoutInfo
import androidx.window.WindowManager
import androidx.window.layout.FoldingFeature
import androidx.window.layout.WindowInfoRepository
import androidx.window.layout.WindowInfoRepository.Companion.windowInfoRepository
import androidx.window.layout.WindowLayoutInfo
import androidx.window.layout.WindowMetricsCalculator
import com.codelab.foldables.window_manager.databinding.ActivityMainBinding
import java.util.concurrent.Executor
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {

private lateinit var wm: WindowManager
private val layoutStateChangeCallback = LayoutStateChangeCallback()
private lateinit var binding: ActivityMainBinding

private fun runOnUiThreadExecutor(): Executor {
val handler = Handler(Looper.getMainLooper())
return Executor() {
handler.post(it)
}
}
private lateinit var windowInfoRepository: WindowInfoRepository
private val scope = MainScope()

@ExperimentalCoroutinesApi
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

wm = WindowManager(this)
}
windowInfoRepository = windowInfoRepository()

override fun onAttachedToWindow() {
super.onAttachedToWindow()
wm.registerLayoutChangeCallback(
runOnUiThreadExecutor(),
layoutStateChangeCallback
)
obtainWindowMetrics()
onWindowLayoutInfoChange(windowInfoRepository)
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
wm.unregisterLayoutChangeCallback(layoutStateChangeCallback)
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}

private fun printLayoutStateChange(newLayoutInfo: WindowLayoutInfo) {
private fun obtainWindowMetrics() {
val wmc = WindowMetricsCalculator.getOrCreate()
val currentWM = wmc.computeCurrentWindowMetrics(this).bounds.flattenToString()
val maximumWM = wmc.computeMaximumWindowMetrics(this).bounds.flattenToString()
binding.windowMetrics.text =
"CurrentWindowMetrics: ${wm.currentWindowMetrics.bounds.flattenToString()}\n" +
"MaximumWindowMetrics: ${wm.maximumWindowMetrics.bounds.flattenToString()}"
"CurrentWindowMetrics: ${currentWM}\nMaximumWindowMetrics: ${maximumWM}"
}

private fun onWindowLayoutInfoChange(windowInfoRepository: WindowInfoRepository) {
scope.launch {
windowInfoRepository.windowLayoutInfo.collect { value ->
updateUI(value)
}
}
}

private fun updateUI(newLayoutInfo: WindowLayoutInfo) {
binding.layoutChange.text = newLayoutInfo.toString()
if (newLayoutInfo.displayFeatures.size > 0) {
if (newLayoutInfo.displayFeatures.isNotEmpty()) {
binding.configurationChanged.text = "Spanned across displays"
alignViewToDeviceFeatureBoundaries(newLayoutInfo)
alignViewToFoldingFeatureBounds(newLayoutInfo)
} else {
binding.configurationChanged.text = "One logic/physical display - unspanned"
}
}

private fun alignViewToDeviceFeatureBoundaries(newLayoutInfo: WindowLayoutInfo) {
private fun alignViewToFoldingFeatureBounds(newLayoutInfo: WindowLayoutInfo) {
val constraintLayout = binding.constraintLayout
val set = ConstraintSet()
set.clone(constraintLayout)

//We get the display feature bounds.
val rect = newLayoutInfo.displayFeatures[0].bounds
//We get the folding feature bounds.
val foldingFeature = newLayoutInfo.displayFeatures[0] as FoldingFeature
val rect = foldingFeature.bounds

//Sets the view to match the height and width of the device feature
//Some devices have a 0px width folding feature. We set a minimum of 1px so we
//can show the view that mirrors the folding feature in the UI and use it as reference.
val horizontalFoldingFeatureHeight =
if (rect.bottom - rect.top > 0) rect.bottom - rect.top
else 1
val verticalFoldingFeatureWidth =
if (rect.right - rect.left > 0) rect.right - rect.left
else 1

//Sets the view to match the height and width of the folding feature
set.constrainHeight(
R.id.device_feature,
rect.bottom - rect.top
R.id.folding_feature,
horizontalFoldingFeatureHeight
)
set.constrainWidth(
R.id.folding_feature,
verticalFoldingFeatureWidth
)
set.constrainWidth(R.id.device_feature, rect.right - rect.left)

set.connect(
R.id.device_feature, ConstraintSet.START,
R.id.folding_feature, ConstraintSet.START,
ConstraintSet.PARENT_ID, ConstraintSet.START, 0
)
set.connect(
R.id.device_feature, ConstraintSet.TOP,
R.id.folding_feature, ConstraintSet.TOP,
ConstraintSet.PARENT_ID, ConstraintSet.TOP, 0
)

if (rect.top == 0) {
// Device feature is placed vertically
set.setMargin(R.id.device_feature, ConstraintSet.START, rect.left)
if (foldingFeature.orientation == FoldingFeature.Orientation.VERTICAL) {
set.setMargin(R.id.folding_feature, ConstraintSet.START, rect.left)
set.connect(
R.id.layout_change, ConstraintSet.END,
R.id.device_feature, ConstraintSet.START, 0
R.id.folding_feature, ConstraintSet.START, 0
)
} else {
//Device feature is placed horizontally
//FoldingFeature is Horizontal
val statusBarHeight = calculateStatusBarHeight()
val toolBarHeight = calculateToolbarHeight()
set.setMargin(
R.id.device_feature, ConstraintSet.TOP,
R.id.folding_feature, ConstraintSet.TOP,
rect.top - statusBarHeight - toolBarHeight
)
set.connect(
R.id.layout_change, ConstraintSet.TOP,
R.id.device_feature, ConstraintSet.BOTTOM, 0
R.id.folding_feature, ConstraintSet.BOTTOM, 0
)
}

//Set the view to visible and apply constraints
set.setVisibility(R.id.device_feature, View.VISIBLE)
set.setVisibility(R.id.folding_feature, View.VISIBLE)
set.applyTo(constraintLayout)
}

Expand All @@ -144,10 +161,4 @@ class MainActivity : AppCompatActivity() {
window.decorView.getWindowVisibleDisplayFrame(rect)
return rect.top
}

inner class LayoutStateChangeCallback : Consumer<WindowLayoutInfo> {
override fun accept(newLayoutInfo: WindowLayoutInfo) {
printLayoutStateChange(newLayoutInfo)
}
}
}
2 changes: 1 addition & 1 deletion window-manager/src/main/res/layout/activity_main.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@

<!-- It's not important where this view is placed by default, it will be adjusted dynamically in runtime -->
<View
android:id="@+id/device_feature"
android:id="@+id/folding_feature"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/holo_red_dark"
Expand Down

0 comments on commit 104971c

Please sign in to comment.