Merge "Support hide display cutout with display area"

This commit is contained in:
Shawn Lin
2020-11-12 04:49:35 +00:00
committed by Android (Google) Code Review
18 changed files with 849 additions and 31 deletions

View File

@@ -77,6 +77,12 @@ public class DisplayAreaOrganizer extends WindowOrganizer {
*/
public static final int FEATURE_FULLSCREEN_MAGNIFICATION = FEATURE_SYSTEM_FIRST + 5;
/**
* Display area for hiding display cutout feature
* @hide
*/
public static final int FEATURE_HIDE_DISPLAY_CUTOUT = FEATURE_SYSTEM_FIRST + 6;
/**
* The last boundary of display area for system features
*/

View File

@@ -4510,4 +4510,7 @@
activity-level letterboxing (size-compat mode). Therefore this override can control the
maximum screen area that can be occupied by the app in the letterbox mode. -->
<item name="config_taskLetterboxAspectRatio" format="float" type="dimen">0.0</item>
<!-- If true, hide the display cutout with display area -->
<bool name="config_hideDisplayCutoutWithDisplayArea">false</bool>
</resources>

View File

@@ -4095,4 +4095,6 @@
<java-symbol type="dimen" name="controls_thumbnail_image_max_width" />
<java-symbol type="dimen" name="config_taskLetterboxAspectRatio" />
<java-symbol type="bool" name="config_hideDisplayCutoutWithDisplayArea" />
</resources>

View File

@@ -16,8 +16,7 @@
package com.android.wm.shell;
import com.android.wm.shell.common.DisplayImeController;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.splitscreen.SplitScreen;
@@ -33,16 +32,19 @@ public class ShellDump {
private final Optional<SplitScreen> mSplitScreenOptional;
private final Optional<Pip> mPipOptional;
private final Optional<OneHanded> mOneHandedOptional;
private final Optional<HideDisplayCutout> mHideDisplayCutout;
private final ShellTaskOrganizer mShellTaskOrganizer;
public ShellDump(ShellTaskOrganizer shellTaskOrganizer,
Optional<SplitScreen> splitScreenOptional,
Optional<Pip> pipOptional,
Optional<OneHanded> oneHandedOptional) {
Optional<OneHanded> oneHandedOptional,
Optional<HideDisplayCutout> hideDisplayCutout) {
mShellTaskOrganizer = shellTaskOrganizer;
mSplitScreenOptional = splitScreenOptional;
mPipOptional = pipOptional;
mOneHandedOptional = oneHandedOptional;
mHideDisplayCutout = hideDisplayCutout;
}
public void dump(PrintWriter pw) {
@@ -52,5 +54,6 @@ public class ShellDump {
mPipOptional.ifPresent(pip -> pip.dump(pw));
mSplitScreenOptional.ifPresent(splitScreen -> splitScreen.dump(pw));
mOneHandedOptional.ifPresent(oneHanded -> oneHanded.dump(pw));
mHideDisplayCutout.ifPresent(hideDisplayCutout -> hideDisplayCutout.dump(pw));
}
}

View File

@@ -0,0 +1,39 @@
/*
* 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 com.android.wm.shell.hidedisplaycutout;
import android.content.res.Configuration;
import androidx.annotation.NonNull;
import java.io.PrintWriter;
/**
* Interface to engage hide display cutout feature.
*/
public interface HideDisplayCutout {
/**
* Notifies {@link Configuration} changed.
* @param newConfig
*/
void onConfigurationChanged(Configuration newConfig);
/**
* Dumps hide display cutout status.
*/
void dump(@NonNull PrintWriter pw);
}

View File

@@ -0,0 +1,101 @@
/*
* 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 com.android.wm.shell.hidedisplaycutout;
import android.content.Context;
import android.content.res.Configuration;
import android.os.SystemProperties;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.android.wm.shell.common.DisplayController;
import java.io.PrintWriter;
/**
* Manages the hide display cutout status.
*/
public class HideDisplayCutoutController implements HideDisplayCutout {
private static final String TAG = "HideDisplayCutoutController";
private final Context mContext;
private final HideDisplayCutoutOrganizer mOrganizer;
@VisibleForTesting
boolean mEnabled;
HideDisplayCutoutController(Context context, HideDisplayCutoutOrganizer organizer) {
mContext = context;
mOrganizer = organizer;
updateStatus();
}
/**
* Creates {@link HideDisplayCutoutController}, returns {@code null} if the feature is not
* supported.
*/
@Nullable
public static HideDisplayCutoutController create(
Context context, DisplayController displayController) {
// The SystemProperty is set for devices that support this feature and is used to control
// whether to create the HideDisplayCutout instance.
// It's defined in the device.mk (e.g. device/google/crosshatch/device.mk).
if (!SystemProperties.getBoolean("ro.support_hide_display_cutout", false)) {
return null;
}
HideDisplayCutoutOrganizer organizer =
new HideDisplayCutoutOrganizer(context, displayController);
return new HideDisplayCutoutController(context, organizer);
}
@VisibleForTesting
void updateStatus() {
// The config value is used for controlling enabling/disabling status of the feature and is
// defined in the config.xml in a "Hide Display Cutout" overlay package (e.g. device/google/
// crosshatch/crosshatch/overlay/packages/apps/overlays/NoCutoutOverlay).
final boolean enabled = mContext.getResources().getBoolean(
com.android.internal.R.bool.config_hideDisplayCutoutWithDisplayArea);
if (enabled == mEnabled) {
return;
}
mEnabled = enabled;
if (enabled) {
mOrganizer.enableHideDisplayCutout();
} else {
mOrganizer.disableHideDisplayCutout();
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
updateStatus();
}
@Override
public void dump(@NonNull PrintWriter pw) {
final String prefix = " ";
pw.print(TAG);
pw.println(" states: ");
pw.print(prefix);
pw.print("mEnabled=");
pw.println(mEnabled);
mOrganizer.dump(pw);
}
}

View File

@@ -0,0 +1,330 @@
/*
* 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 com.android.wm.shell.hidedisplaycutout;
import static android.view.Display.DEFAULT_DISPLAY;
import android.content.Context;
import android.graphics.Insets;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.ArrayMap;
import android.util.Log;
import android.util.RotationUtils;
import android.view.Display;
import android.view.DisplayCutout;
import android.view.Surface;
import android.view.SurfaceControl;
import android.window.DisplayAreaAppearedInfo;
import android.window.DisplayAreaInfo;
import android.window.DisplayAreaOrganizer;
import android.window.WindowContainerToken;
import android.window.WindowContainerTransaction;
import androidx.annotation.GuardedBy;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import com.android.internal.R;
import com.android.wm.shell.common.DisplayChangeController;
import com.android.wm.shell.common.DisplayController;
import java.io.PrintWriter;
import java.util.List;
/**
* Manages the display areas of hide display cutout feature.
*/
class HideDisplayCutoutOrganizer extends DisplayAreaOrganizer {
private static final String TAG = "HideDisplayCutoutOrganizer";
private final Context mContext;
private final DisplayController mDisplayController;
@VisibleForTesting
@GuardedBy("this")
ArrayMap<WindowContainerToken, SurfaceControl> mDisplayAreaMap = new ArrayMap();
// The default display bound in natural orientation.
private final Rect mDefaultDisplayBounds = new Rect();
@VisibleForTesting
final Rect mCurrentDisplayBounds = new Rect();
// The default display cutout in natural orientation.
private Insets mDefaultCutoutInsets;
private Insets mCurrentCutoutInsets;
private boolean mIsDefaultPortrait;
private int mStatusBarHeight;
@VisibleForTesting
int mOffsetX;
@VisibleForTesting
int mOffsetY;
@VisibleForTesting
int mRotation;
/**
* Handles rotation based on OnDisplayChangingListener callback.
*/
private final DisplayChangeController.OnDisplayChangingListener mRotationController =
(display, fromRotation, toRotation, wct) -> {
mRotation = toRotation;
updateBoundsAndOffsets(true /* enable */);
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
applyAllBoundsAndOffsets(wct, t);
// Only apply t here since the server will do the wct.apply when the method
// finishes.
t.apply();
};
HideDisplayCutoutOrganizer(Context context, DisplayController displayController) {
mContext = context;
mDisplayController = displayController;
}
@Override
public void onDisplayAreaAppeared(@NonNull DisplayAreaInfo displayAreaInfo,
@NonNull SurfaceControl leash) {
if (!addDisplayAreaInfoAndLeashToMap(displayAreaInfo, leash)) {
return;
}
final WindowContainerTransaction wct = new WindowContainerTransaction();
final SurfaceControl.Transaction tx = new SurfaceControl.Transaction();
applyBoundsAndOffsets(displayAreaInfo.token, leash, wct, tx);
applyTransaction(wct, tx);
}
@Override
public void onDisplayAreaVanished(@NonNull DisplayAreaInfo displayAreaInfo) {
synchronized (this) {
if (!mDisplayAreaMap.containsKey(displayAreaInfo.token)) {
Log.w(TAG, "Unrecognized token: " + displayAreaInfo.token);
return;
}
final WindowContainerTransaction wct = new WindowContainerTransaction();
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
applyBoundsAndOffsets(
displayAreaInfo.token, mDisplayAreaMap.get(displayAreaInfo.token), wct, t);
applyTransaction(wct, t);
mDisplayAreaMap.remove(displayAreaInfo.token);
}
}
private void updateDisplayAreaMap(List<DisplayAreaAppearedInfo> displayAreaInfos) {
for (int i = 0; i < displayAreaInfos.size(); i++) {
final DisplayAreaInfo info = displayAreaInfos.get(i).getDisplayAreaInfo();
final SurfaceControl leash = displayAreaInfos.get(i).getLeash();
addDisplayAreaInfoAndLeashToMap(info, leash);
}
}
@VisibleForTesting
boolean addDisplayAreaInfoAndLeashToMap(@NonNull DisplayAreaInfo displayAreaInfo,
@NonNull SurfaceControl leash) {
synchronized (this) {
if (displayAreaInfo.displayId != DEFAULT_DISPLAY) {
return false;
}
if (mDisplayAreaMap.containsKey(displayAreaInfo.token)) {
Log.w(TAG, "Already appeared token: " + displayAreaInfo.token);
return false;
}
mDisplayAreaMap.put(displayAreaInfo.token, leash);
return true;
}
}
/**
* Enables hide display cutout.
*/
void enableHideDisplayCutout() {
mDisplayController.addDisplayChangingController(mRotationController);
final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY);
if (display != null) {
mRotation = display.getRotation();
}
final List<DisplayAreaAppearedInfo> displayAreaInfos =
registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT);
updateDisplayAreaMap(displayAreaInfos);
updateBoundsAndOffsets(true /* enabled */);
final WindowContainerTransaction wct = new WindowContainerTransaction();
final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
applyAllBoundsAndOffsets(wct, t);
applyTransaction(wct, t);
}
/**
* Disables hide display cutout.
*/
void disableHideDisplayCutout() {
updateBoundsAndOffsets(false /* enabled */);
mDisplayController.removeDisplayChangingController(mRotationController);
unregisterOrganizer();
}
@VisibleForTesting
Insets getDisplayCutoutInsetsOfNaturalOrientation() {
final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY);
if (display == null) {
return Insets.NONE;
}
DisplayCutout cutout = display.getCutout();
Insets insets = cutout != null ? Insets.of(cutout.getSafeInsets()) : Insets.NONE;
return mRotation != Surface.ROTATION_0
? RotationUtils.rotateInsets(insets, 4 /* total number of rotation */ - mRotation)
: insets;
}
@VisibleForTesting
Rect getDisplayBoundsOfNaturalOrientation() {
Point realSize = new Point(0, 0);
final Display display = mDisplayController.getDisplay(DEFAULT_DISPLAY);
if (display != null) {
display.getRealSize(realSize);
}
final boolean isDisplaySizeFlipped = isDisplaySizeFlipped();
return new Rect(
0,
0,
isDisplaySizeFlipped ? realSize.y : realSize.x,
isDisplaySizeFlipped ? realSize.x : realSize.y);
}
private boolean isDisplaySizeFlipped() {
return mRotation == Surface.ROTATION_90 || mRotation == Surface.ROTATION_270;
}
/**
* Updates bounds and offsets according to current state.
*
* @param enabled whether the hide display cutout feature is enabled.
*/
@VisibleForTesting
void updateBoundsAndOffsets(boolean enabled) {
if (!enabled) {
resetBoundsAndOffsets();
} else {
initDefaultValuesIfNeeded();
// Reset to default values.
mCurrentDisplayBounds.set(mDefaultDisplayBounds);
mOffsetX = 0;
mOffsetY = 0;
// Update bounds and insets according to the rotation.
mCurrentCutoutInsets = RotationUtils.rotateInsets(mDefaultCutoutInsets, mRotation);
if (isDisplaySizeFlipped()) {
mCurrentDisplayBounds.set(
mCurrentDisplayBounds.top,
mCurrentDisplayBounds.left,
mCurrentDisplayBounds.bottom,
mCurrentDisplayBounds.right);
}
mCurrentDisplayBounds.inset(mCurrentCutoutInsets);
// Replace the top bound with the max(status bar height, cutout height) if there is
// cutout on the top side.
mStatusBarHeight = getStatusBarHeight();
if (mCurrentCutoutInsets.top != 0) {
mCurrentDisplayBounds.top = Math.max(mStatusBarHeight, mCurrentCutoutInsets.top);
}
mOffsetX = mCurrentDisplayBounds.left;
mOffsetY = mCurrentDisplayBounds.top;
}
}
private void resetBoundsAndOffsets() {
mCurrentDisplayBounds.setEmpty();
mOffsetX = 0;
mOffsetY = 0;
}
private void initDefaultValuesIfNeeded() {
if (!mDefaultDisplayBounds.isEmpty()) {
return;
}
mDefaultDisplayBounds.set(getDisplayBoundsOfNaturalOrientation());
mDefaultCutoutInsets = getDisplayCutoutInsetsOfNaturalOrientation();
mIsDefaultPortrait = mDefaultDisplayBounds.width() < mDefaultDisplayBounds.height();
}
private void applyAllBoundsAndOffsets(
WindowContainerTransaction wct, SurfaceControl.Transaction t) {
synchronized (this) {
mDisplayAreaMap.forEach((token, leash) -> {
applyBoundsAndOffsets(token, leash, wct, t);
});
}
}
@VisibleForTesting
void applyBoundsAndOffsets(WindowContainerToken token, SurfaceControl leash,
WindowContainerTransaction wct, SurfaceControl.Transaction t) {
wct.setBounds(token, mCurrentDisplayBounds.isEmpty() ? null : mCurrentDisplayBounds);
t.setPosition(leash, mOffsetX, mOffsetY);
}
@VisibleForTesting
void applyTransaction(WindowContainerTransaction wct, SurfaceControl.Transaction t) {
applyTransaction(wct);
t.apply();
}
private int getStatusBarHeight() {
final boolean isLandscape =
mIsDefaultPortrait ? isDisplaySizeFlipped() : !isDisplaySizeFlipped();
return mContext.getResources().getDimensionPixelSize(
isLandscape ? R.dimen.status_bar_height_landscape
: R.dimen.status_bar_height_portrait);
}
void dump(@NonNull PrintWriter pw) {
final String prefix = " ";
pw.print(TAG);
pw.println(" states: ");
synchronized (this) {
pw.print(prefix);
pw.print("mDisplayAreaMap=");
pw.println(mDisplayAreaMap);
}
pw.print(prefix);
pw.print("getDisplayBoundsOfNaturalOrientation()=");
pw.println(getDisplayBoundsOfNaturalOrientation());
pw.print(prefix);
pw.print("mDefaultDisplayBounds=");
pw.println(mDefaultDisplayBounds);
pw.print(prefix);
pw.print("mCurrentDisplayBounds=");
pw.println(mCurrentDisplayBounds);
pw.print(prefix);
pw.print("mDefaultCutoutInsets=");
pw.println(mDefaultCutoutInsets);
pw.print(prefix);
pw.print("mCurrentCutoutInsets=");
pw.println(mCurrentCutoutInsets);
pw.print(prefix);
pw.print("mRotation=");
pw.println(mRotation);
pw.print(prefix);
pw.print("mStatusBarHeight=");
pw.println(mStatusBarHeight);
pw.print(prefix);
pw.print("mOffsetX=");
pw.println(mOffsetX);
pw.print(prefix);
pw.print("mOffsetY=");
pw.println(mOffsetY);
}
}

View File

@@ -30,6 +30,7 @@ android_test {
"mockito-target-extended-minus-junit4",
"truth-prebuilt",
"testables",
"platform-test-annotations",
],
libs: [

View File

@@ -0,0 +1,73 @@
/*
* 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 com.android.wm.shell.hidedisplaycutout;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import android.platform.test.annotations.Presubmit;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@Presubmit
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class HideDisplayCutoutControllerTest {
private TestableContext mContext = new TestableContext(
InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
private HideDisplayCutoutController mHideDisplayCutoutController;
@Mock
private HideDisplayCutoutOrganizer mMockDisplayAreaOrganizer;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
mHideDisplayCutoutController = new HideDisplayCutoutController(
mContext, mMockDisplayAreaOrganizer);
}
@Test
public void testToggleHideDisplayCutout_On() {
mHideDisplayCutoutController.mEnabled = false;
mContext.getOrCreateTestableResources().addOverride(
com.android.internal.R.bool.config_hideDisplayCutoutWithDisplayArea, true);
reset(mMockDisplayAreaOrganizer);
mHideDisplayCutoutController.updateStatus();
verify(mMockDisplayAreaOrganizer).enableHideDisplayCutout();
}
@Test
public void testToggleHideDisplayCutout_Off() {
mHideDisplayCutoutController.mEnabled = true;
mContext.getOrCreateTestableResources().addOverride(
com.android.internal.R.bool.config_hideDisplayCutoutWithDisplayArea, false);
mHideDisplayCutoutController.updateStatus();
verify(mMockDisplayAreaOrganizer).disableHideDisplayCutout();
}
}

View File

@@ -0,0 +1,218 @@
/*
* 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 com.android.wm.shell.hidedisplaycutout;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.window.DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.doNothing;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.res.Configuration;
import android.graphics.Insets;
import android.graphics.Rect;
import android.os.Binder;
import android.platform.test.annotations.Presubmit;
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
import android.view.Display;
import android.view.Surface;
import android.view.SurfaceControl;
import android.window.DisplayAreaAppearedInfo;
import android.window.DisplayAreaInfo;
import android.window.DisplayAreaOrganizer;
import android.window.IWindowContainerToken;
import android.window.WindowContainerToken;
import androidx.test.InstrumentationRegistry;
import androidx.test.filters.SmallTest;
import com.android.internal.R;
import com.android.wm.shell.common.DisplayController;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import java.util.ArrayList;
@Presubmit
@SmallTest
@RunWith(AndroidTestingRunner.class)
@TestableLooper.RunWithLooper
public class HideDisplayCutoutOrganizerTest {
private TestableContext mContext = new TestableContext(
InstrumentationRegistry.getInstrumentation().getTargetContext(), null);
@Mock
private DisplayController mMockDisplayController;
private HideDisplayCutoutOrganizer mOrganizer;
private DisplayAreaInfo mDisplayAreaInfo;
private SurfaceControl mLeash;
@Mock
private Display mDisplay;
@Mock
private IWindowContainerToken mMockRealToken;
private WindowContainerToken mToken;
private final Rect mFakeDefaultBounds = new Rect(0, 0, 100, 200);
private final Insets mFakeDefaultCutoutInsets = Insets.of(0, 10, 0, 0);
private final int mFakeStatusBarHeightPortrait = 15;
private final int mFakeStatusBarHeightLandscape = 10;
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
when(mMockDisplayController.getDisplay(anyInt())).thenReturn(mDisplay);
HideDisplayCutoutOrganizer organizer = new HideDisplayCutoutOrganizer(
mContext, mMockDisplayController);
mOrganizer = Mockito.spy(organizer);
doNothing().when(mOrganizer).unregisterOrganizer();
doNothing().when(mOrganizer).applyBoundsAndOffsets(any(), any(), any(), any());
doNothing().when(mOrganizer).applyTransaction(any(), any());
// It will be called when mDisplayAreaMap.containKey(token) is called.
Binder binder = new Binder();
doReturn(binder).when(mMockRealToken).asBinder();
mToken = new WindowContainerToken(mMockRealToken);
mLeash = new SurfaceControl();
mDisplayAreaInfo = new DisplayAreaInfo(
mToken, DEFAULT_DISPLAY, FEATURE_HIDE_DISPLAY_CUTOUT);
mDisplayAreaInfo.configuration.orientation = Configuration.ORIENTATION_PORTRAIT;
DisplayAreaAppearedInfo info = new DisplayAreaAppearedInfo(mDisplayAreaInfo, mLeash);
ArrayList<DisplayAreaAppearedInfo> infoList = new ArrayList<>();
infoList.add(info);
doReturn(infoList).when(mOrganizer).registerOrganizer(
DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT);
}
@Test
public void testEnableHideDisplayCutout() {
mOrganizer.enableHideDisplayCutout();
verify(mOrganizer).registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT);
verify(mOrganizer).addDisplayAreaInfoAndLeashToMap(mDisplayAreaInfo, mLeash);
verify(mOrganizer).updateBoundsAndOffsets(true);
assertThat(mOrganizer.mDisplayAreaMap.containsKey(mDisplayAreaInfo.token)).isTrue();
assertThat(mOrganizer.mDisplayAreaMap.containsValue(mLeash)).isTrue();
}
@Test
public void testOnDisplayAreaAppeared() {
mOrganizer.onDisplayAreaAppeared(mDisplayAreaInfo, mLeash);
assertThat(mOrganizer.mDisplayAreaMap.containsKey(mToken)).isTrue();
assertThat(mOrganizer.mDisplayAreaMap.containsValue(mLeash)).isTrue();
}
@Test
public void testOnDisplayAreaVanished() {
mOrganizer.mDisplayAreaMap.put(mDisplayAreaInfo.token, mLeash);
mOrganizer.onDisplayAreaVanished(mDisplayAreaInfo);
assertThat(mOrganizer.mDisplayAreaMap.containsKey(mDisplayAreaInfo.token)).isFalse();
}
@Test
public void testToggleHideDisplayCutout_enable_rot0() {
doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation();
doReturn(mFakeDefaultCutoutInsets).when(mOrganizer)
.getDisplayCutoutInsetsOfNaturalOrientation();
mContext.getOrCreateTestableResources().addOverride(
R.dimen.status_bar_height_portrait, mFakeStatusBarHeightPortrait);
doReturn(Surface.ROTATION_0).when(mDisplay).getRotation();
mOrganizer.enableHideDisplayCutout();
verify(mOrganizer).registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT);
verify(mOrganizer).addDisplayAreaInfoAndLeashToMap(mDisplayAreaInfo, mLeash);
verify(mOrganizer).updateBoundsAndOffsets(true);
assertThat(mOrganizer.mCurrentDisplayBounds).isEqualTo(new Rect(0, 15, 100, 200));
assertThat(mOrganizer.mOffsetX).isEqualTo(0);
assertThat(mOrganizer.mOffsetY).isEqualTo(15);
assertThat(mOrganizer.mRotation).isEqualTo(Surface.ROTATION_0);
}
@Test
public void testToggleHideDisplayCutout_enable_rot90() {
doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation();
doReturn(mFakeDefaultCutoutInsets).when(mOrganizer)
.getDisplayCutoutInsetsOfNaturalOrientation();
mContext.getOrCreateTestableResources().addOverride(
R.dimen.status_bar_height_landscape, mFakeStatusBarHeightLandscape);
doReturn(Surface.ROTATION_90).when(mDisplay).getRotation();
mOrganizer.enableHideDisplayCutout();
verify(mOrganizer).registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT);
verify(mOrganizer).addDisplayAreaInfoAndLeashToMap(mDisplayAreaInfo, mLeash);
verify(mOrganizer).updateBoundsAndOffsets(true);
assertThat(mOrganizer.mCurrentDisplayBounds).isEqualTo(new Rect(10, 0, 200, 100));
assertThat(mOrganizer.mOffsetX).isEqualTo(10);
assertThat(mOrganizer.mOffsetY).isEqualTo(0);
assertThat(mOrganizer.mRotation).isEqualTo(Surface.ROTATION_90);
}
@Test
public void testToggleHideDisplayCutout_enable_rot270() {
doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation();
doReturn(mFakeDefaultCutoutInsets).when(mOrganizer)
.getDisplayCutoutInsetsOfNaturalOrientation();
mContext.getOrCreateTestableResources().addOverride(
R.dimen.status_bar_height_landscape, mFakeStatusBarHeightLandscape);
doReturn(Surface.ROTATION_270).when(mDisplay).getRotation();
mOrganizer.enableHideDisplayCutout();
verify(mOrganizer).registerOrganizer(DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT);
verify(mOrganizer).addDisplayAreaInfoAndLeashToMap(mDisplayAreaInfo, mLeash);
verify(mOrganizer).updateBoundsAndOffsets(true);
assertThat(mOrganizer.mCurrentDisplayBounds).isEqualTo(new Rect(0, 0, 190, 100));
assertThat(mOrganizer.mOffsetX).isEqualTo(0);
assertThat(mOrganizer.mOffsetY).isEqualTo(0);
assertThat(mOrganizer.mRotation).isEqualTo(Surface.ROTATION_270);
}
@Test
public void testToggleHideDisplayCutout_disable() {
doReturn(mFakeDefaultBounds).when(mOrganizer).getDisplayBoundsOfNaturalOrientation();
doReturn(mFakeDefaultCutoutInsets).when(mOrganizer)
.getDisplayCutoutInsetsOfNaturalOrientation();
mContext.getOrCreateTestableResources().addOverride(
R.dimen.status_bar_height_portrait, mFakeStatusBarHeightPortrait);
mOrganizer.enableHideDisplayCutout();
// disable hide display cutout
mOrganizer.disableHideDisplayCutout();
verify(mOrganizer).updateBoundsAndOffsets(false);
verify(mOrganizer).unregisterOrganizer();
assertThat(mOrganizer.mCurrentDisplayBounds).isEqualTo(new Rect(0, 0, 0, 0));
assertThat(mOrganizer.mOffsetX).isEqualTo(0);
assertThat(mOrganizer.mOffsetY).isEqualTo(0);
}
}

View File

@@ -109,6 +109,7 @@ public class SystemUIFactory {
.setSplitScreen(mWMComponent.getSplitScreen())
.setOneHanded(mWMComponent.getOneHanded())
.setBubbles(mWMComponent.getBubbles())
.setHideDisplayCutout(mWMComponent.getHideDisplayCutout())
.setShellDump(mWMComponent.getShellDump());
} else {
// TODO: Call on prepareSysUIComponentBuilder but not with real components.
@@ -116,6 +117,7 @@ public class SystemUIFactory {
.setSplitScreen(Optional.ofNullable(null))
.setOneHanded(Optional.ofNullable(null))
.setBubbles(Optional.ofNullable(null))
.setHideDisplayCutout(Optional.ofNullable(null))
.setShellDump(Optional.ofNullable(null));
}
mSysUIComponent = builder.build();

View File

@@ -26,6 +26,7 @@ import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.util.InjectionInflationController;
import com.android.wm.shell.ShellDump;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.splitscreen.SplitScreen;
@@ -65,6 +66,9 @@ public interface SysUIComponent {
@BindsInstance
Builder setBubbles(Optional<Bubbles> b);
@BindsInstance
Builder setHideDisplayCutout(Optional<HideDisplayCutout> h);
@BindsInstance
Builder setShellDump(Optional<ShellDump> shellDump);

View File

@@ -20,6 +20,7 @@ import com.android.systemui.wmshell.WMShellModule;
import com.android.wm.shell.ShellDump;
import com.android.wm.shell.ShellInit;
import com.android.wm.shell.bubbles.Bubbles;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.pip.Pip;
import com.android.wm.shell.splitscreen.SplitScreen;
@@ -71,4 +72,7 @@ public interface WMComponent {
@WMSingleton
Optional<Bubbles> getBubbles();
@WMSingleton
Optional<HideDisplayCutout> getHideDisplayCutout();
}

View File

@@ -16,8 +16,6 @@
package com.android.systemui.wmshell;
import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.view.Display.DEFAULT_DISPLAY;
import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_3BUTTON;
@@ -29,21 +27,14 @@ import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_O
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_QUICK_SETTINGS_EXPANDED;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING;
import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED;
import static com.android.systemui.shared.system.WindowManagerWrapper.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY;
import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.ActivityTaskManager.RootTaskInfo;
import android.content.ComponentName;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.inputmethodservice.InputMethodService;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.util.Log;
import android.util.Pair;
import android.view.KeyEvent;
import com.android.internal.annotations.VisibleForTesting;
@@ -55,8 +46,6 @@ import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.shared.tracing.ProtoTraceable;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.ConfigurationController;
@@ -64,6 +53,7 @@ import com.android.systemui.statusbar.policy.UserInfoController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.systemui.tracing.nano.SystemUiTraceProto;
import com.android.wm.shell.ShellDump;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.nano.WmShellTraceProto;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedEvents;
@@ -105,6 +95,7 @@ public final class WMShell extends SystemUI
private final Optional<Pip> mPipOptional;
private final Optional<SplitScreen> mSplitScreenOptional;
private final Optional<OneHanded> mOneHandedOptional;
private final Optional<HideDisplayCutout> mHideDisplayCutoutOptional;
private final ProtoTracer mProtoTracer;
private final Optional<ShellDump> mShellDump;
@@ -123,6 +114,7 @@ public final class WMShell extends SystemUI
Optional<Pip> pipOptional,
Optional<SplitScreen> splitScreenOptional,
Optional<OneHanded> oneHandedOptional,
Optional<HideDisplayCutout> hideDisplayCutoutOptional,
ProtoTracer protoTracer,
Optional<ShellDump> shellDump) {
super(context);
@@ -135,6 +127,7 @@ public final class WMShell extends SystemUI
mPipOptional = pipOptional;
mSplitScreenOptional = splitScreenOptional;
mOneHandedOptional = oneHandedOptional;
mHideDisplayCutoutOptional = hideDisplayCutoutOptional;
mProtoTracer = protoTracer;
mProtoTracer.add(this);
mShellDump = shellDump;
@@ -146,6 +139,7 @@ public final class WMShell extends SystemUI
mPipOptional.ifPresent(this::initPip);
mSplitScreenOptional.ifPresent(this::initSplitScreen);
mOneHandedOptional.ifPresent(this::initOneHanded);
mHideDisplayCutoutOptional.ifPresent(this::initHideDisplayCutout);
}
@VisibleForTesting
@@ -284,6 +278,16 @@ public final class WMShell extends SystemUI
});
}
@VisibleForTesting
void initHideDisplayCutout(HideDisplayCutout hideDisplayCutout) {
mConfigurationController.addCallback(new ConfigurationController.ConfigurationListener() {
@Override
public void onConfigChanged(Configuration newConfig) {
hideDisplayCutout.onConfigurationChanged(newConfig);
}
});
}
@Override
public void writeToProto(SystemUiTraceProto proto) {
if (proto.wmShell == null) {

View File

@@ -28,7 +28,6 @@ import com.android.internal.logging.UiEventLogger;
import com.android.internal.statusbar.IStatusBarService;
import com.android.systemui.dagger.WMSingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.shared.system.InputConsumerController;
import com.android.wm.shell.ShellDump;
import com.android.wm.shell.ShellInit;
import com.android.wm.shell.ShellTaskOrganizer;
@@ -46,6 +45,8 @@ import com.android.wm.shell.common.SystemWindows;
import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.common.TransactionPool;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutoutController;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.pip.Pip;
@@ -90,9 +91,10 @@ public abstract class WMShellBaseModule {
static Optional<ShellDump> provideShellDump(ShellTaskOrganizer shellTaskOrganizer,
Optional<SplitScreen> splitScreenOptional,
Optional<Pip> pipOptional,
Optional<OneHanded> oneHandedOptional) {
Optional<OneHanded> oneHandedOptional,
Optional<HideDisplayCutout> hideDisplayCutout) {
return Optional.of(new ShellDump(shellTaskOrganizer, splitScreenOptional, pipOptional,
oneHandedOptional));
oneHandedOptional, hideDisplayCutout));
}
@WMSingleton
@@ -215,4 +217,10 @@ public abstract class WMShellBaseModule {
return new HandlerExecutor(handler);
}
@WMSingleton
@Provides
static Optional<HideDisplayCutout> provideHideDisplayCutoutController(Context context,
DisplayController displayController) {
return Optional.ofNullable(HideDisplayCutoutController.create(context, displayController));
}
}

View File

@@ -20,7 +20,6 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.content.pm.PackageManager;
import android.test.suitebuilder.annotation.SmallTest;
import androidx.test.runner.AndroidJUnit4;
@@ -31,13 +30,11 @@ import com.android.systemui.SysuiTestCase;
import com.android.systemui.keyguard.ScreenLifecycle;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.shared.system.InputConsumerController;
import com.android.systemui.shared.system.TaskStackChangeListener;
import com.android.systemui.shared.system.TaskStackChangeListeners;
import com.android.systemui.statusbar.CommandQueue;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.tracing.ProtoTracer;
import com.android.wm.shell.ShellDump;
import com.android.wm.shell.hidedisplaycutout.HideDisplayCutout;
import com.android.wm.shell.onehanded.OneHanded;
import com.android.wm.shell.onehanded.OneHandedGestureHandler;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
@@ -56,13 +53,11 @@ import java.util.Optional;
@SmallTest
@RunWith(AndroidJUnit4.class)
public class WMShellTest extends SysuiTestCase {
InputConsumerController mInputConsumerController;
WMShell mWMShell;
@Mock CommandQueue mCommandQueue;
@Mock ConfigurationController mConfigurationController;
@Mock KeyguardUpdateMonitor mKeyguardUpdateMonitor;
@Mock InputConsumerController mMockInputConsumerController;
@Mock NavigationModeController mNavigationModeController;
@Mock ScreenLifecycle mScreenLifecycle;
@Mock SysUiState mSysUiState;
@@ -70,6 +65,7 @@ public class WMShellTest extends SysuiTestCase {
@Mock PipTouchHandler mPipTouchHandler;
@Mock SplitScreen mSplitScreen;
@Mock OneHanded mOneHanded;
@Mock HideDisplayCutout mHideDisplayCutout;
@Mock ProtoTracer mProtoTracer;
@Mock ShellDump mShellDump;
@@ -80,7 +76,8 @@ public class WMShellTest extends SysuiTestCase {
mWMShell = new WMShell(mContext, mCommandQueue, mConfigurationController,
mKeyguardUpdateMonitor, mNavigationModeController,
mScreenLifecycle, mSysUiState, Optional.of(mPip), Optional.of(mSplitScreen),
Optional.of(mOneHanded), mProtoTracer, Optional.of(mShellDump));
Optional.of(mOneHanded), Optional.of(mHideDisplayCutout), mProtoTracer,
Optional.of(mShellDump));
when(mPip.getPipTouchHandler()).thenReturn(mPipTouchHandler);
}
@@ -113,4 +110,12 @@ public class WMShellTest extends SysuiTestCase {
OneHandedGestureHandler.OneHandedGestureEventCallback.class));
verify(mOneHanded).registerTransitionCallback(any(OneHandedTransitionCallback.class));
}
@Test
public void initHideDisplayCutout_registersCallbacks() {
mWMShell.initHideDisplayCutout(mHideDisplayCutout);
verify(mConfigurationController).addCallback(
any(ConfigurationController.ConfigurationListener.class));
}
}

View File

@@ -22,8 +22,11 @@ import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
import static android.view.WindowManager.LayoutParams.TYPE_MAGNIFICATION_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR;
import static android.view.WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL;
import static android.view.WindowManager.LayoutParams.TYPE_NOTIFICATION_SHADE;
import static android.view.WindowManager.LayoutParams.TYPE_STATUS_BAR;
import static android.window.DisplayAreaOrganizer.FEATURE_DEFAULT_TASK_CONTAINER;
import static android.window.DisplayAreaOrganizer.FEATURE_FULLSCREEN_MAGNIFICATION;
import static android.window.DisplayAreaOrganizer.FEATURE_HIDE_DISPLAY_CUTOUT;
import static android.window.DisplayAreaOrganizer.FEATURE_ONE_HANDED;
import static android.window.DisplayAreaOrganizer.FEATURE_WINDOWED_MAGNIFICATION;
@@ -99,6 +102,12 @@ public abstract class DisplayAreaPolicy {
// layer
.setNewDisplayAreaSupplier(DisplayArea.Dimmable::new)
.build())
.addFeature(new Feature.Builder(wmService.mPolicy, "HideDisplayCutout",
FEATURE_HIDE_DISPLAY_CUTOUT)
.all()
.except(TYPE_NAVIGATION_BAR, TYPE_NAVIGATION_BAR_PANEL, TYPE_STATUS_BAR,
TYPE_NOTIFICATION_SHADE)
.build())
.addFeature(new Feature.Builder(wmService.mPolicy, "OneHanded",
FEATURE_ONE_HANDED)
.all()

View File

@@ -2969,10 +2969,6 @@ public class DisplayPolicy {
mDisplayContent.getInsetsPolicy().updateBarControlTarget(win);
final int fullscreenAppearance = updateLightStatusBarLw(0 /* appearance */,
mTopFullscreenOpaqueWindowState, mTopFullscreenOpaqueOrDimmingWindowState);
final int dockedAppearance = updateLightStatusBarLw(0 /* appearance */,
mTopDockedOpaqueWindowState, mTopDockedOpaqueOrDimmingWindowState);
final boolean inSplitScreen =
mService.mRoot.getDefaultTaskDisplayArea().isSplitScreenModeActivated();
if (inSplitScreen) {
@@ -2984,6 +2980,12 @@ public class DisplayPolicy {
mService.getStackBounds(inSplitScreen ? WINDOWING_MODE_SPLIT_SCREEN_SECONDARY
: WINDOWING_MODE_FULLSCREEN,
ACTIVITY_TYPE_UNDEFINED, mNonDockedStackBounds);
final int fullscreenAppearance = updateLightStatusBarLw(0 /* appearance */,
mTopFullscreenOpaqueWindowState, mTopFullscreenOpaqueOrDimmingWindowState,
mNonDockedStackBounds);
final int dockedAppearance = updateLightStatusBarLw(0 /* appearance */,
mTopDockedOpaqueWindowState, mTopDockedOpaqueOrDimmingWindowState,
mDockedStackBounds);
final int disableFlags = win.getSystemUiVisibility() & StatusBarManager.DISABLE_MASK;
final int opaqueAppearance = updateSystemBarsLw(win, disableFlags);
final WindowState navColorWin = chooseNavigationColorWindowLw(
@@ -3049,10 +3051,14 @@ public class DisplayPolicy {
}
private int updateLightStatusBarLw(@Appearance int appearance, WindowState opaque,
WindowState opaqueOrDimming) {
WindowState opaqueOrDimming, Rect stackBounds) {
final DisplayRotation displayRotation = mDisplayContent.getDisplayRotation();
final int statusBarHeight = mStatusBarHeightForRotation[displayRotation.getRotation()];
final boolean stackBoundsContainStatusBar =
stackBounds.isEmpty() ? false : stackBounds.top < statusBarHeight;
final boolean onKeyguard = isKeyguardShowing() && !isKeyguardOccluded();
final WindowState statusColorWin = onKeyguard ? mNotificationShade : opaqueOrDimming;
if (statusColorWin != null) {
if (stackBoundsContainStatusBar && statusColorWin != null) {
if (statusColorWin == opaque || onKeyguard) {
// If the top fullscreen-or-dimming window is also the top fullscreen, respect
// its light flag.