[go: nahoru, domu]

blob: ca37b0ca3ab6e15ec1aa0a5b76aed62aa11f842a [file] [log] [blame]
/*
* Copyright (C) 2020 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
*
* 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 androidx.core.view
import android.os.Build
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
import android.widget.EditText
import androidx.annotation.RequiresApi
import androidx.core.graphics.Insets
import androidx.core.test.R
import androidx.test.core.app.ActivityScenario
import androidx.test.espresso.Espresso.onView
import androidx.test.espresso.action.ViewActions.click
import androidx.test.espresso.action.ViewActions.closeSoftKeyboard
import androidx.test.espresso.matcher.ViewMatchers.assertThat
import androidx.test.espresso.matcher.ViewMatchers.withId
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.LargeTest
import androidx.test.filters.SdkSuppress
import androidx.testutils.withActivity
import org.hamcrest.Matchers.`is`
import org.hamcrest.Matchers.equalTo
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.fail
import org.junit.Assume.assumeFalse
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.junit.runner.RunWith
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicReference
import kotlin.concurrent.thread
@Suppress("DEPRECATION")
@SdkSuppress(minSdkVersion = 23)
@RequiresApi(23) // ViewCompat.getRootWindowInsets()
@LargeTest
@RunWith(AndroidJUnit4::class)
public class WindowInsetsControllerCompatActivityTest {
private lateinit var container: View
private lateinit var windowInsetsController: WindowInsetsControllerCompat
private lateinit var scenario: ActivityScenario<WindowInsetsCompatActivity>
@Before
public fun setup() {
scenario = ActivityScenario.launch(WindowInsetsCompatActivity::class.java)
container = scenario.withActivity { findViewById(R.id.container) }
windowInsetsController = ViewCompat.getWindowInsetsController(container)!!
scenario.withActivity {
WindowCompat.setDecorFitsSystemWindows(window, true)
}
// Close the IME if it's open, so we start from a known scenario
onView(withId(R.id.edittext)).perform(closeSoftKeyboard())
scenario.withActivity {
ViewCompat.getWindowInsetsController(container)!!.systemBarsBehavior =
WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Needed on API 23 to report the nav bar insets
this.window.addFlags(FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
}
/**
* IME visibility is only reliable on API 23+, where we have access to the root WindowInsets
*/
@SdkSuppress(minSdkVersion = 23)
@Test
public fun toggleIME() {
// Test do not currently work on Cuttlefish
assumeNotCuttlefish()
val container: View = scenario.withActivity { findViewById(R.id.container) }
scenario.withActivity { findViewById<View>(R.id.edittext).requestFocus() }
val windowInsetsController = ViewCompat.getWindowInsetsController(container)!!
scenario.onActivity { windowInsetsController.hide(WindowInsetsCompat.Type.ime()) }
container.assertInsetsVisibility(WindowInsetsCompat.Type.ime(), false)
testShow(WindowInsetsCompat.Type.ime())
testHide(WindowInsetsCompat.Type.ime())
}
/**
* IME visibility is only reliable on API 23+, where we have access to the root WindowInsets
*/
@SdkSuppress(minSdkVersion = 23)
@Test
public fun do_not_show_IME_if_TextView_not_focused() {
val editText = scenario.withActivity {
findViewById<EditText>(R.id.edittext)
}
// We hide the edit text to ensure it won't be automatically focused
scenario.onActivity {
editText.visibility = View.GONE
assertThat(editText.isFocused, `is`(false))
}
val type = WindowInsetsCompat.Type.ime()
scenario.onActivity { windowInsetsController.show(type) }
container.assertInsetsVisibility(type, false)
}
/**
* IME visibility is only reliable on API 23+, where we have access to the root WindowInsets
*/
@SdkSuppress(minSdkVersion = 23)
@Test
fun show_IME_fromEditText() {
// Test do not currently work on Cuttlefish
assumeNotCuttlefish()
val type = WindowInsetsCompat.Type.ime()
val editText = scenario.withActivity { findViewById(R.id.edittext) }
val controller = scenario.withActivity { ViewCompat.getWindowInsetsController(editText)!! }
scenario.onActivity {
editText.requestFocus()
}
assertThat(editText.isFocused, `is`(true))
if (Build.VERSION.SDK_INT == 30) {
// Dirty hack until we figure out why the IME is not showing if we don't wait before
Thread.sleep(100)
}
controller.show(type)
container.assertInsetsVisibility(type, true)
}
/**
* IME visibility is only reliable on API 23+, where we have access to the root WindowInsets
*/
@SdkSuppress(minSdkVersion = 23)
@Test
public fun hide_IME() {
// Test do not currently work on Cuttlefish
assumeNotCuttlefish()
onView(withId(R.id.edittext)).perform(click())
container.assertInsetsVisibility(WindowInsetsCompat.Type.ime(), true)
testHide(WindowInsetsCompat.Type.ime())
}
@Test
public fun toggle_StatusBar() {
container.assertInsetsVisibility(WindowInsetsCompat.Type.statusBars(), true)
testHide(WindowInsetsCompat.Type.statusBars())
testShow(WindowInsetsCompat.Type.statusBars())
}
@Test
public fun toggle_NavBar() {
testHide(WindowInsetsCompat.Type.navigationBars())
testShow(WindowInsetsCompat.Type.navigationBars())
}
private fun testHide(type: Int) {
scenario.onActivity { windowInsetsController.hide(type) }
container.assertInsetsVisibility(type, false)
}
private fun testShow(type: Int) {
// Now open the IME using the InsetsController The IME should
// now be open
scenario.onActivity { windowInsetsController.show(type) }
container.assertInsetsVisibility(type, true)
}
@SdkSuppress(minSdkVersion = 23)
@Test
public fun systemBar_light() {
scenario.onActivity {
windowInsetsController.setAppearanceLightStatusBars(true)
}
if (Build.VERSION.SDK_INT < 30) {
// The view's systemUiVisibility flags are not changed on API 30+
val systemUiVisibility = scenario.withActivity { window.decorView }.systemUiVisibility
assertThat(
systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR,
equalTo(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR)
)
}
assertThat(windowInsetsController.isAppearanceLightStatusBars(), `is`(true))
}
@SdkSuppress(minSdkVersion = 26)
@Test
public fun navigationBar_light() {
scenario.onActivity {
windowInsetsController.setAppearanceLightNavigationBars(true)
}
val systemUiVisibility = scenario.withActivity { window.decorView }.systemUiVisibility
if (Build.VERSION.SDK_INT < 30) {
// The view's systemUiVisibility flags are not changed on API 30+
assertThat(
systemUiVisibility and View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR,
equalTo(View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR)
)
}
assertThat(
windowInsetsController.isAppearanceLightNavigationBars(), `is`(true)
)
}
/**
* IME visibility is only reliable on API 23+, where we have access to the root WindowInsets
*/
@SdkSuppress(minSdkVersion = 23)
@Ignore("The listener isn't called when changing the visibility")
@Test
public fun ime_toggle_check_with_listener() {
// Test do not currently work on Cuttlefish
assumeNotCuttlefish()
val type = WindowInsetsCompat.Type.ime()
val container: View = scenario.withActivity { findViewById(R.id.container) }
// Tell the window that our view will fit system windows
container.doAndAwaitNextInsets {
scenario.onActivity { activity ->
WindowCompat.setDecorFitsSystemWindows(activity.window, false)
}
}.let { insets ->
// Assert that the IME visibility is false and the insets are empty
assertThat(insets.isVisible(type), `is`(false))
assertEquals(Insets.NONE, insets.getInsets(type))
}
val windowInsetsController = ViewCompat.getWindowInsetsController(container)!!
// Now open the IME using the InsetsController The IME should
// now be open
// container.doAndAwaitNextInsets {
scenario.onActivity {
windowInsetsController.show(type)
}
// Finally dismiss the IME
container.doAndAwaitNextInsets {
scenario.onActivity {
windowInsetsController.hide(type)
}
}.let { insets ->
// Assert that the IME visibility is false and the insets are empty
assertThat(insets.isVisible(type), `is`(false))
assertEquals(Insets.NONE, insets.getInsets(type))
}
}
@Test
@SdkSuppress(maxSdkVersion = 29) // Flag deprecated in 30+
public fun systemBarsBehavior_swipe() {
scenario.onActivity {
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_SWIPE
}
val decorView = scenario.withActivity { window.decorView }
val sysUiVis = decorView.systemUiVisibility
assertEquals(
View.SYSTEM_UI_FLAG_IMMERSIVE,
sysUiVis and View.SYSTEM_UI_FLAG_IMMERSIVE
)
assertEquals(0, sysUiVis and View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
}
@Test
@SdkSuppress(maxSdkVersion = 29) // Flag deprecated in 30+
public fun systemBarsBehavior_transient() {
scenario.onActivity {
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
val decorView = scenario.withActivity { window.decorView }
val sysUiVis = decorView.systemUiVisibility
assertEquals(
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY,
sysUiVis and View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
assertEquals(0, sysUiVis and View.SYSTEM_UI_FLAG_IMMERSIVE)
}
@Test
public fun systemBarsBehavior_touch() {
scenario.onActivity {
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_BARS_BY_TOUCH
}
val decorView = scenario.withActivity { window.decorView }
val sysUiVis = decorView.systemUiVisibility
assertEquals(
0,
sysUiVis and (
View.SYSTEM_UI_FLAG_IMMERSIVE
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
)
)
}
private fun assumeNotCuttlefish() {
// TODO: remove this if b/159103848 is resolved
assumeFalse(
"Unable to test: Cuttlefish devices default to the virtual keyboard being disabled.",
Build.MODEL.contains("Cuttlefish", ignoreCase = true)
)
}
@After
fun cleanup() {
scenario.close()
}
@RequiresApi(23) // ViewCompat.getRootWindowInsets()
private fun View.assertInsetsVisibility(
type: Int,
expectedVisibility: Boolean
) {
val latch = CountDownLatch(1)
var loop = true
var lastVisibility: Boolean? = null
try {
thread {
while (loop) {
val rootWindowInsets = scenario.withActivity {
ViewCompat
.getRootWindowInsets(this@assertInsetsVisibility)!!
}
lastVisibility = rootWindowInsets.isVisible(type)
if (lastVisibility == expectedVisibility) {
latch.countDown()
}
Thread.sleep(300)
}
}
latch.await(5, TimeUnit.SECONDS)
} finally {
loop = false
assertThat(
"isVisible() should be <$expectedVisibility> but is <$lastVisibility>",
lastVisibility, `is`(expectedVisibility)
)
}
}
}
private fun View.doAndAwaitNextInsets(action: (View) -> Unit): WindowInsetsCompat {
val latch = CountDownLatch(1)
val received = AtomicReference<WindowInsetsCompat>()
// Set a listener to catch WindowInsets
ViewCompat.setOnApplyWindowInsetsListener(this) { _, insets: WindowInsetsCompat ->
received.set(insets)
latch.countDown()
WindowInsetsCompat.CONSUMED
}
try {
// Perform the action
action(this)
// Await an inset pass
if (!latch.await(5, TimeUnit.SECONDS)) {
fail("OnApplyWindowInsetsListener was not called")
}
} finally {
this.post {
ViewCompat.setOnApplyWindowInsetsListener(this, null)
}
}
return received.get()
}