blob: 1939c72a75c64856ef5e0ec0206d2836418bf5e3 [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 android.server.wm;
import static android.server.wm.WindowManagerTestBase.startActivity;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeTrue;
import android.app.WindowConfiguration;
import android.content.ComponentCallbacks;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.Binder;
import android.os.IBinder;
import android.platform.test.annotations.AppModeFull;
import android.platform.test.annotations.Presubmit;
import android.server.wm.WindowContextTests.TestWindowService.TestToken;
import android.view.View;
import android.view.WindowManager;
import android.window.WindowProviderService;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.rule.ServiceTestRule;
import org.junit.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Tests that verify the behavior of window context
*
* Build/Install/Run:
* atest CtsWindowManagerDeviceTestCases:WindowContextTests
*/
@Presubmit
public class WindowContextTests extends WindowContextTestBase {
@Test
@AppModeFull
public void testWindowContextConfigChanges() {
createAllowSystemAlertWindowAppOpSession();
final WindowManagerState.DisplayContent display = createManagedVirtualDisplaySession()
.setSimulateDisplay(true).createDisplay();
final Context windowContext = createWindowContext(display.mId);
mInstrumentation.runOnMainSync(() -> {
final View view = new View(windowContext);
WindowManager wm = windowContext.getSystemService(WindowManager.class);
wm.addView(view, new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY));
});
final DisplayMetricsSession displayMetricsSession =
createManagedDisplayMetricsSession(display.mId);
mWmState.computeState();
Rect bounds = windowContext.getSystemService(WindowManager.class).getCurrentWindowMetrics()
.getBounds();
assertBoundsEquals(displayMetricsSession.getDisplayMetrics(), bounds);
displayMetricsSession.changeDisplayMetrics(1.2 /* sizeRatio */, 1.1 /* densityRatio */);
mWmState.computeState();
bounds = windowContext.getSystemService(WindowManager.class).getCurrentWindowMetrics()
.getBounds();
assertBoundsEquals(displayMetricsSession.getDisplayMetrics(), bounds);
}
private void assertBoundsEquals(ReportedDisplayMetrics expectedMetrics,
Rect bounds) {
assertEquals(expectedMetrics.getSize().getWidth(), bounds.width());
assertEquals(expectedMetrics.getSize().getHeight(), bounds.height());
}
@Test
@AppModeFull
public void testWindowContextBindService() {
createAllowSystemAlertWindowAppOpSession();
final Context windowContext = createWindowContext(DEFAULT_DISPLAY);
final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {}
@Override
public void onServiceDisconnected(ComponentName name) {}
};
try {
assertTrue("WindowContext must bind service successfully.",
windowContext.bindService(new Intent(windowContext, TestLogService.class),
serviceConnection, Context.BIND_AUTO_CREATE));
} finally {
windowContext.unbindService(serviceConnection);
}
}
/**
* Verify if the {@link ComponentCallbacks#onConfigurationChanged(Configuration)} callback
* is received when the window context configuration changes.
*/
@Test
public void testWindowContextRegisterComponentCallbacks() {
final TestComponentCallbacks callbacks = new TestComponentCallbacks();
final WindowManagerState.DisplayContent display = createManagedVirtualDisplaySession()
.setSimulateDisplay(true).createDisplay();
final Context windowContext = createWindowContext(display.mId);
final DisplayMetricsSession displayMetricsSession =
createManagedDisplayMetricsSession(display.mId);
windowContext.registerComponentCallbacks(callbacks);
callbacks.mLatch = new CountDownLatch(1);
displayMetricsSession.changeDisplayMetrics(1.2 /* sizeRatio */, 1.1 /* densityRatio */);
// verify if there is a callback from the window context configuration change.
try {
assertTrue(callbacks.mLatch.await(4, TimeUnit.SECONDS));
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
Rect bounds = callbacks.mConfiguration.windowConfiguration.getBounds();
assertBoundsEquals(displayMetricsSession.getDisplayMetrics(), bounds);
windowContext.unregisterComponentCallbacks(callbacks);
}
/**
* Verifies if window context on the secondary display receives global configuration changes.
*/
@Test
public void testWindowContextGlobalConfigChanges() {
final TestComponentCallbacks callbacks = new TestComponentCallbacks();
final WindowManagerState.DisplayContent display = createManagedVirtualDisplaySession()
.setPublicDisplay(true).createDisplay();
final FontScaleSession fontScaleSession = createManagedFontScaleSession();
final Context windowContext = createWindowContext(display.mId);
windowContext.registerComponentCallbacks(callbacks);
final float expectedFontScale = fontScaleSession.get() + 0.3f;
fontScaleSession.set(expectedFontScale);
// Wait for TestComponentCallbacks#mConfiguration to be assigned.
callbacks.waitForConfigChanged();
// We don't rely on latch to verify the result because we may receive two configuration
// changes. One may from that WindowContext attaches to a DisplayArea although it is before
// ComponentCallback registration), the other is from font the scale change, which is what
// we want to verify.
waitForOrFail("font scale to match " + expectedFontScale, () ->
expectedFontScale == callbacks.mConfiguration.fontScale);
windowContext.unregisterComponentCallbacks(callbacks);
}
/**
* Verify the {@link WindowProviderService} lifecycle:
* <ul>
* <li>In {@link WindowProviderService#onCreate()}, register to the DisplayArea with
* given value from {@link WindowProviderService#getWindowType()} and
* {@link WindowProviderService#getWindowContextOptions()}} and receive a
* {@link Configuration} update which matches DisplayArea's metrics.</li>
* <li>After {@link WindowProviderService#attachToWindowToken(IBinder)}, the
* {@link WindowProviderService} must be switched to register to the Window Token and
* receive a configuration update which matches Window Token's metrics.</li>
* </ul>
*/
@Test
public void testWindowProviderServiceLifecycle() {
assumeTrue(supportsSplitScreenMultiWindow());
// Start an activity for WindowProviderService to attach
final TestActivity activity = startActivity(TestActivity.class);
final ComponentName activityName = activity.getComponentName();
// If the device supports multi-window, make this Activity to multi-window mode.
// In this way, we can verify if the WindowProviderService's metrics matches
// the split-screen Activity's metrics, which is different from TaskDisplayArea's metrics.
mWmState.computeState(activityName);
putActivityInPrimarySplit(activityName);
activity.waitAndAssertConfigurationChanged();
// Obtain the TestWindowService instance.
final TestWindowService service = createManagedWindowServiceSession().getService();
// Compute state to obtain associated TaskActivityArea information.
mWmState.computeState(activityName);
final WindowManagerState.DisplayArea da = mWmState.getTaskDisplayArea(activityName);
final Rect daBounds = da.mFullConfiguration.windowConfiguration.getBounds();
final Rect maxDaBounds = da.mFullConfiguration.windowConfiguration.getMaxBounds();
assertBoundsMatches(service, daBounds, maxDaBounds,
"WindowProviderService bounds must match DisplayArea bounds.");
// Obtain the Activity's token and attach it to TestWindowService.
final IBinder windowToken = activity.getWindow().getAttributes().token;
service.attachToWindowToken(windowToken);
final WindowManager wm = activity.getWindowManager();
final Rect currentBounds = wm.getCurrentWindowMetrics().getBounds();
final Rect maxBounds = wm.getMaximumWindowMetrics().getBounds();
service.waitAndAssertConfigurationChanged();
// After TestWindowService attaches the Activity's token, which is also a WindowToken,
// it is expected to receive a config update which matches the WindowMetrics of
// the Activity.
assertBoundsMatches(service, currentBounds, maxBounds,
"WindowProviderService bounds must match WindowToken bounds.");
}
/**
* Verifies if:
* <ul>
* <li>{@link android.view.WindowMetrics} bounds matches provided bounds.</li>
* <li>Bounds from {@link WindowProviderService#onConfigurationChanged(Configuration)}
* callback matches provided bounds.</li>
* </ul>
*/
private void assertBoundsMatches(TestWindowService service, Rect currentBounds,
Rect maxBounds, String message) {
final WindowConfiguration winConfig = service.mConfiguration.windowConfiguration;
assertWithMessage(message + " WindowConfiguration#getBounds not matched.")
.that(winConfig.getBounds()).isEqualTo(currentBounds);
assertWithMessage(message + " WindowConfiguration#getMaxBounds not "
+ "matched.").that(winConfig.getMaxBounds()).isEqualTo(maxBounds);
final WindowManager wm = service.getSystemService(WindowManager.class);
final Rect currentWindowBounds = wm.getCurrentWindowMetrics().getBounds();
final Rect maxWindowBounds = wm.getMaximumWindowMetrics().getBounds();
assertWithMessage(message + " Current WindowMetrics bounds not matched.")
.that(currentWindowBounds).isEqualTo(currentBounds);
assertWithMessage(message + " Maximum WindowMetrics bounds not matched.")
.that(maxWindowBounds).isEqualTo(maxBounds);
}
@Test
public void testWidowProviderServiceGlobalConfigChanges() {
final TestWindowService service = createManagedWindowServiceSession().getService();
// Obtain the original config
final Configuration originalConfiguration =
new Configuration(service.getResources().getConfiguration());
final FontScaleSession fontScaleSession = createManagedFontScaleSession();
final float expectedFontScale = fontScaleSession.get() + 0.3f;
fontScaleSession.set(expectedFontScale);
service.waitAndAssertConfigurationChanged();
assertThat(service.mConfiguration.fontScale).isEqualTo(expectedFontScale);
// Also check Configuration obtained from WindowProviderService's Resources
assertWithMessage("Configuration update must contains font scale change.")
.that(originalConfiguration.diff(service.mConfiguration)
& ActivityInfo.CONFIG_FONT_SCALE).isNotEqualTo(0);
assertWithMessage("Font scale must be updated to WindowProviderService Resources.")
.that(service.getResources().getConfiguration().fontScale)
.isEqualTo(expectedFontScale);
}
@Test
public void testWindowProviderServiceCallWmBeforeOnCreateNotCrash() {
final TestWindowService service =
createManagedWindowServiceSession(true /* verifyWmInOnCreate */).getService();
if (service.mThrowableFromOnCreate != null) {
throw new AssertionError("Calling WindowManager APIs before"
+ " WindowProviderService#onCreate must not throw Throwable, but did.",
service.mThrowableFromOnCreate);
}
}
public static class TestActivity extends WindowManagerTestBase.FocusableActivity {
final CountDownLatch mLatch = new CountDownLatch(1);
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mLatch.countDown();
}
private void waitAndAssertConfigurationChanged() {
try {
assertThat(mLatch.await(4, TimeUnit.SECONDS)).isTrue();
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
}
}
private TestWindowServiceSession createManagedWindowServiceSession() {
return mObjectTracker.manage(new TestWindowServiceSession(false /* verifyWmInOnCreate */));
}
private TestWindowServiceSession createManagedWindowServiceSession(boolean verifyWmInOnCreate) {
return mObjectTracker.manage(new TestWindowServiceSession(verifyWmInOnCreate));
}
private static class TestWindowServiceSession implements AutoCloseable {
private final ServiceTestRule mServiceRule = new ServiceTestRule();
private final TestWindowService mService;
private static boolean sVerifyWmInOnCreate = false;
private TestWindowServiceSession(boolean verifyWmInOnCreate) {
final Context context = ApplicationProvider.getApplicationContext();
final Intent intent = new Intent(context, TestWindowService.class);
sVerifyWmInOnCreate = verifyWmInOnCreate;
try {
final TestToken token = (TestToken) mServiceRule.bindService(intent);
mService = token.getService();
} catch (TimeoutException e) {
throw new RuntimeException(e);
}
}
private TestWindowService getService() {
return mService;
}
@Override
public void close() {
mServiceRule.unbindService();
}
}
public static class TestWindowService extends WindowProviderService {
private final IBinder mToken = new TestToken();
private CountDownLatch mLatch = new CountDownLatch(1);
private Configuration mConfiguration;
private Throwable mThrowableFromOnCreate;
@Override
public int getWindowType() {
return TYPE_APPLICATION;
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mToken;
}
@Override
public void onCreate() {
// Verify if call WindowManager before WindowProviderService#onCreate throws Exception.
if (TestWindowServiceSession.sVerifyWmInOnCreate) {
try {
getSystemService(WindowManager.class).getCurrentWindowMetrics();
} catch (Throwable t) {
mThrowableFromOnCreate = t;
}
}
super.onCreate();
mConfiguration = getResources().getConfiguration();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mConfiguration = newConfig;
mLatch.countDown();
}
private void resetLatch() {
mLatch = new CountDownLatch(1);
}
private void waitAndAssertConfigurationChanged() {
try {
assertThat(mLatch.await(4, TimeUnit.SECONDS)).isTrue();
} catch(InterruptedException e) {
throw new RuntimeException(e);
}
}
public class TestToken extends Binder {
private TestWindowService getService() {
return TestWindowService.this;
}
}
}
private static class TestComponentCallbacks implements ComponentCallbacks {
private Configuration mConfiguration;
private CountDownLatch mLatch = new CountDownLatch(1);
private void waitForConfigChanged() {
try {
assertThat(mLatch.await(4, TimeUnit.SECONDS)).isTrue();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
mConfiguration = newConfig;
mLatch.countDown();
}
@Override
public void onLowMemory() {}
}
}