Fix IME being shifted when the app setRequestedOrientation

Fix an edge case happens on large-screen devices that
DisplayContent#updateImeParent may not be called to update IME surface
parent from the activity to the display area when the app actvitity is
letterboxed by
 1) activity handles configChange without being relaunched
    by window configuration change
 2) calling setRequestedOrientation to fix orientation as portrait

In this case, if the activity bounds size is not suitable to attach
IME surface, we need to recompute the IME surface parent in
WindowState#onConfigurationChanged to make IME surface can be placed on
the display area to avoid IME position being shifted by
letterboxed activity.

Fix: 249081451
Test: atest FlickerTests:OpenImeWindowToFixedPortraitAppTest
Merged-In: I7a66fd84e4094be249714c2597706ee25938adbe
Change-Id: I7a66fd84e4094be249714c2597706ee25938adbe
This commit is contained in:
Ming-Shin Lu
2023-01-13 08:19:01 +00:00
parent 8165fae2c8
commit 9053d728be
7 changed files with 186 additions and 16 deletions

View File

@@ -2395,7 +2395,11 @@ class WindowState extends WindowContainer<WindowState> implements WindowManagerP
// IME parent may failed to attach to the app during rotating the screen.
// See DisplayContent#shouldImeAttachedToApp, DisplayContent#isImeControlledByApp
if (windowConfigChanged) {
getDisplayContent().updateImeControlTarget();
// If the window was the IME layering target, updates the IME surface parent in case
// the IME surface may be wrongly positioned when the window configuration affects the
// IME surface association. (e.g. Attach IME surface on the display instead of the
// app when the app bounds being letterboxed.)
mDisplayContent.updateImeControlTarget(isImeLayeringTarget() /* updateImeParent */);
}
}

View File

@@ -1119,7 +1119,9 @@ public class WindowStateTests extends WindowTestsBase {
spyOn(app.getDisplayContent());
app.mActivityRecord.getRootTask().setWindowingMode(WINDOWING_MODE_FULLSCREEN);
verify(app.getDisplayContent()).updateImeControlTarget();
// Expect updateImeParent will be invoked when the configuration of the IME control
// target has changed.
verify(app.getDisplayContent()).updateImeControlTarget(eq(true) /* updateImeParent */);
assertEquals(mAppWindow, mDisplayContent.getImeTarget(IME_TARGET_CONTROL).getWindow());
}

View File

@@ -113,4 +113,18 @@ class ImeAppAutoFocusHelper @JvmOverloads constructor(
}
return false
}
fun toggleFixPortraitOrientation(wmHelper: WindowManagerStateHelper) {
val button = uiDevice.wait(Until.findObject(By.res(getPackage(),
"toggle_fixed_portrait_btn")), FIND_TIMEOUT)
require(button != null) {
"Button not found, this usually happens when the device " +
"was left in an unknown state (e.g. Screen turned off)"
}
button.click()
mInstrumentation.waitForIdleSync()
// Ensure app relaunching transition finish and the IME has shown
wmHelper.waitForAppTransitionIdle()
wmHelper.waitImeShown()
}
}

View File

@@ -0,0 +1,129 @@
/*
* Copyright (C) 2023 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 com.android.server.wm.flicker.ime
import android.app.Instrumentation
import android.platform.test.annotations.Postsubmit
import android.view.Surface
import android.view.WindowManagerPolicyConstants
import androidx.test.filters.RequiresDevice
import androidx.test.platform.app.InstrumentationRegistry
import com.android.server.wm.flicker.FlickerBuilderProvider
import com.android.server.wm.flicker.FlickerParametersRunnerFactory
import com.android.server.wm.flicker.FlickerTestParameter
import com.android.server.wm.flicker.FlickerTestParameterFactory
import com.android.server.wm.flicker.annotation.Group2
import com.android.server.wm.flicker.dsl.FlickerBuilder
import com.android.server.wm.flicker.helpers.ImeAppAutoFocusHelper
import com.android.server.wm.flicker.helpers.WindowUtils
import com.android.server.wm.flicker.traces.region.RegionSubject
import com.android.server.wm.traces.common.FlickerComponentName
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
/**
* Test IME window shown on the app with fixing portrait orientation.
* To run this test: `atest FlickerTests:OpenImeWindowToFixedPortraitAppTest`
*/
@RequiresDevice
@RunWith(Parameterized::class)
@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
@Group2
class OpenImeWindowToFixedPortraitAppTest (private val testSpec: FlickerTestParameter) {
private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
private val testApp = ImeAppAutoFocusHelper(instrumentation, testSpec.startRotation)
@FlickerBuilderProvider
fun buildFlicker(): FlickerBuilder {
return FlickerBuilder(instrumentation).apply {
setup {
eachRun {
testApp.launchViaIntent(wmHelper)
testApp.openIME(device, wmHelper)
// Enable letterbox when the app calls setRequestedOrientation
device.executeShellCommand("cmd window set-ignore-orientation-request true")
}
}
transitions {
testApp.toggleFixPortraitOrientation(wmHelper)
}
teardown {
eachRun {
testApp.exit()
device.executeShellCommand("cmd window set-ignore-orientation-request false")
}
}
}
}
@Postsubmit
@Test
fun imeLayerVisibleStart() {
testSpec.assertLayersStart {
this.isVisible(FlickerComponentName.IME)
}
}
@Postsubmit
@Test
fun imeLayerExistsEnd() {
testSpec.assertLayersEnd {
this.isVisible(FlickerComponentName.IME)
}
}
@Postsubmit
@Test
fun imeLayerVisibleRegionKeepsTheSame() {
var imeLayerVisibleRegionBeforeTransition: RegionSubject? = null
testSpec.assertLayersStart {
imeLayerVisibleRegionBeforeTransition = this.visibleRegion(FlickerComponentName.IME)
}
testSpec.assertLayersEnd {
this.visibleRegion(FlickerComponentName.IME)
.coversExactly(imeLayerVisibleRegionBeforeTransition!!.region)
}
}
@Postsubmit
@Test
fun appWindowWithLetterboxCoversExactlyOnScreen() {
val displayBounds = WindowUtils.getDisplayBounds(testSpec.startRotation)
testSpec.assertLayersEnd {
this.visibleRegion(testApp.component, FlickerComponentName.LETTERBOX)
.coversExactly(displayBounds)
}
}
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
fun getParams(): Collection<FlickerTestParameter> {
return FlickerTestParameterFactory.getInstance()
.getConfigNonRotationTests(
supportedRotations = listOf(Surface.ROTATION_90, Surface.ROTATION_270),
supportedNavigationModes = listOf(
WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON_OVERLAY,
WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL_OVERLAY
)
)
}
}
}

View File

@@ -45,7 +45,7 @@
android:theme="@style/CutoutShortEdges"
android:taskAffinity="com.android.server.wm.flicker.testapp.ImeActivityAutoFocus"
android:windowSoftInputMode="stateVisible"
android:configChanges="orientation|screenSize"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:label="ImeAppAutoFocus"
android:exported="true">
<intent-filter>

View File

@@ -26,14 +26,27 @@
android:layout_width="match_parent"
android:imeOptions="flagNoExtractUi"
android:inputType="text"/>
<Button
android:id="@+id/finish_activity_btn"
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Finish activity" />
<Button
android:id="@+id/start_dialog_themed_activity_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start dialog themed activity" />
android:layout_height="match_parent"
android:orientation="horizontal">
<Button
android:id="@+id/finish_activity_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Finish activity" />
<Button
android:id="@+id/start_dialog_themed_activity_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Dialog activity" />
<ToggleButton
android:id="@+id/toggle_fixed_portrait_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textOn="Portrait (On)"
android:textOff="Portrait (Off)"
/>
</LinearLayout>
</LinearLayout>

View File

@@ -16,21 +16,29 @@
package com.android.server.wm.flicker.testapp;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import android.content.Intent;
import android.widget.Button;
import android.widget.EditText;
import android.widget.ToggleButton;
public class ImeActivityAutoFocus extends ImeActivity {
@Override
protected void onStart() {
super.onStart();
EditText editTextField = findViewById(R.id.plain_text_input);
editTextField.requestFocus();
Button startThemedActivityButton = findViewById(R.id.start_dialog_themed_activity_btn);
startThemedActivityButton.setOnClickListener(
button -> startActivity(new Intent(this, DialogThemedActivity.class)));
ToggleButton toggleFixedPortraitButton = findViewById(R.id.toggle_fixed_portrait_btn);
toggleFixedPortraitButton.setOnCheckedChangeListener(
(button, isChecked) -> setRequestedOrientation(
isChecked ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_UNSPECIFIED));
EditText editTextField = findViewById(R.id.plain_text_input);
editTextField.requestFocus();
}
}