Add border API to surface control
See go/sf-box-shadows-api for more details
Bug: b/367464660
Flag: com.android.window.flags.enable_border_settings
Test: atest SurfaceFlinger_test
Change-Id: I1190edb97693004d9f46058fd0165451470a65b3
diff --git a/libs/gui/Android.bp b/libs/gui/Android.bp
index 158c548..2117c98 100644
--- a/libs/gui/Android.bp
+++ b/libs/gui/Android.bp
@@ -93,6 +93,7 @@
"android/gui/StalledTransactionInfo.aidl",
"android/**/TouchOcclusionMode.aidl",
"android/gui/TrustedOverlay.aidl",
+ "android/gui/BorderSettings.aidl",
],
}
diff --git a/libs/gui/LayerState.cpp b/libs/gui/LayerState.cpp
index ad95d1a..86bc97e 100644
--- a/libs/gui/LayerState.cpp
+++ b/libs/gui/LayerState.cpp
@@ -180,6 +180,7 @@
SAFE_PARCEL(output.writeParcelableVector, listener.callbackIds);
}
SAFE_PARCEL(output.writeFloat, shadowRadius);
+ SAFE_PARCEL(output.writeParcelable, borderSettings);
SAFE_PARCEL(output.writeInt32, frameRateSelectionPriority);
SAFE_PARCEL(output.writeFloat, frameRate);
SAFE_PARCEL(output.writeByte, frameRateCompatibility);
@@ -328,6 +329,8 @@
listeners.emplace_back(listener, callbackIds);
}
SAFE_PARCEL(input.readFloat, &shadowRadius);
+ SAFE_PARCEL(input.readParcelable, &borderSettings);
+
SAFE_PARCEL(input.readInt32, &frameRateSelectionPriority);
SAFE_PARCEL(input.readFloat, &frameRate);
SAFE_PARCEL(input.readByte, &frameRateCompatibility);
@@ -727,6 +730,10 @@
what |= eShadowRadiusChanged;
shadowRadius = other.shadowRadius;
}
+ if (other.what & eBorderSettingsChanged) {
+ what |= eBorderSettingsChanged;
+ borderSettings = other.borderSettings;
+ }
if (other.what & eLutsChanged) {
what |= eLutsChanged;
luts = other.luts;
@@ -881,6 +888,7 @@
CHECK_DIFF2(diff, eBackgroundColorChanged, other, bgColor, bgColorDataspace);
if (other.what & eMetadataChanged) diff |= eMetadataChanged;
CHECK_DIFF(diff, eShadowRadiusChanged, other, shadowRadius);
+ CHECK_DIFF(diff, eBorderSettingsChanged, other, borderSettings);
CHECK_DIFF(diff, eDefaultFrameRateCompatibilityChanged, other, defaultFrameRateCompatibility);
CHECK_DIFF(diff, eFrameRateSelectionPriority, other, frameRateSelectionPriority);
CHECK_DIFF3(diff, eFrameRateChanged, other, frameRate, frameRateCompatibility,
diff --git a/libs/gui/SurfaceComposerClient.cpp b/libs/gui/SurfaceComposerClient.cpp
index 9854274..60a2bf7 100644
--- a/libs/gui/SurfaceComposerClient.cpp
+++ b/libs/gui/SurfaceComposerClient.cpp
@@ -2222,6 +2222,19 @@
return *this;
}
+SurfaceComposerClient::Transaction& SurfaceComposerClient::Transaction::setBorderSettings(
+ const sp<SurfaceControl>& sc, gui::BorderSettings settings) {
+ layer_state_t* s = getLayerState(sc);
+ if (!s) {
+ mStatus = BAD_INDEX;
+ return *this;
+ }
+
+ s->what |= layer_state_t::eBorderSettingsChanged;
+ s->borderSettings = settings;
+ return *this;
+}
+
SurfaceComposerClient::Transaction& SurfaceComposerClient::Transaction::setFrameRate(
const sp<SurfaceControl>& sc, float frameRate, int8_t compatibility,
int8_t changeFrameRateStrategy) {
diff --git a/libs/gui/android/gui/BorderSettings.aidl b/libs/gui/android/gui/BorderSettings.aidl
new file mode 100644
index 0000000..547f57f
--- /dev/null
+++ b/libs/gui/android/gui/BorderSettings.aidl
@@ -0,0 +1,24 @@
+/**
+ * Copyright (c) 2025, 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.gui;
+
+/** @hide */
+parcelable BorderSettings {
+ float strokeWidth;
+ // Space is sRGB, not premultiplied, bit pattern is 0xAARRGGBB.
+ int color;
+}
diff --git a/libs/gui/include/gui/LayerState.h b/libs/gui/include/gui/LayerState.h
index 369d3d1..e2d27ac 100644
--- a/libs/gui/include/gui/LayerState.h
+++ b/libs/gui/include/gui/LayerState.h
@@ -21,6 +21,7 @@
#include <sys/types.h>
#include <span>
+#include <android/gui/BorderSettings.h>
#include <android/gui/DisplayCaptureArgs.h>
#include <android/gui/IWindowInfosReportedListener.h>
#include <android/gui/LayerCaptureArgs.h>
@@ -250,6 +251,7 @@
ePictureProfileHandleChanged = 0x80000'00000000,
eAppContentPriorityChanged = 0x100000'00000000,
eClientDrawnCornerRadiusChanged = 0x200000'00000000,
+ eBorderSettingsChanged = 0x400000'00000000,
};
layer_state_t();
@@ -293,8 +295,8 @@
layer_state_t::eColorSpaceAgnosticChanged | layer_state_t::eColorTransformChanged |
layer_state_t::eCornerRadiusChanged | layer_state_t::eDimmingEnabledChanged |
layer_state_t::eHdrMetadataChanged | layer_state_t::eShadowRadiusChanged |
- layer_state_t::eStretchChanged |
- layer_state_t::ePictureProfileHandleChanged | layer_state_t::eAppContentPriorityChanged;
+ layer_state_t::eStretchChanged | layer_state_t::ePictureProfileHandleChanged |
+ layer_state_t::eAppContentPriorityChanged | layer_state_t::eBorderSettingsChanged;
// Changes which invalidates the layer's visible region in CE.
static constexpr uint64_t CONTENT_DIRTY = layer_state_t::CONTENT_CHANGES |
@@ -322,7 +324,8 @@
// Changes that force GPU composition.
static constexpr uint64_t COMPOSITION_EFFECTS = layer_state_t::eBackgroundBlurRadiusChanged |
layer_state_t::eBlurRegionsChanged | layer_state_t::eCornerRadiusChanged |
- layer_state_t::eShadowRadiusChanged | layer_state_t::eStretchChanged;
+ layer_state_t::eShadowRadiusChanged | layer_state_t::eStretchChanged |
+ layer_state_t::eBorderSettingsChanged;
bool hasValidBuffer() const;
void sanitize(int32_t permissions);
@@ -411,6 +414,9 @@
// Draws a shadow around the surface.
float shadowRadius;
+ // Draws an outline around the layer.
+ gui::BorderSettings borderSettings;
+
// Priority of the layer assigned by Window Manager.
int32_t frameRateSelectionPriority;
diff --git a/libs/gui/include/gui/SurfaceComposerClient.h b/libs/gui/include/gui/SurfaceComposerClient.h
index 4fda8de..1ad9f95 100644
--- a/libs/gui/include/gui/SurfaceComposerClient.h
+++ b/libs/gui/include/gui/SurfaceComposerClient.h
@@ -717,6 +717,8 @@
const Rect& source, const Rect& dst, int transform);
Transaction& setShadowRadius(const sp<SurfaceControl>& sc, float cornerRadius);
+ Transaction& setBorderSettings(const sp<SurfaceControl>& sc, gui::BorderSettings settings);
+
Transaction& setFrameRate(const sp<SurfaceControl>& sc, float frameRate,
int8_t compatibility, int8_t changeFrameRateStrategy);
diff --git a/libs/renderengine/Android.bp b/libs/renderengine/Android.bp
index f9b84fa..39182aa 100644
--- a/libs/renderengine/Android.bp
+++ b/libs/renderengine/Android.bp
@@ -52,6 +52,7 @@
"libtonemap",
"libsurfaceflinger_common",
"libsurfaceflingerflags",
+ "libgui_window_info_static",
],
local_include_dirs: ["include"],
export_include_dirs: ["include"],
@@ -122,7 +123,13 @@
"skia_renderengine_deps",
"libsurfaceflinger_common_deps",
],
- static_libs: ["libskia_renderengine"],
+ static_libs: [
+ "libgui_window_info_static",
+ "libskia_renderengine",
+ ],
+ shared_libs: [
+ "libbinder",
+ ],
}
// Note: if compilation fails when adding librenderengine as a dependency, try adding
diff --git a/libs/renderengine/include/renderengine/LayerSettings.h b/libs/renderengine/include/renderengine/LayerSettings.h
index ecb16b2..3523497 100644
--- a/libs/renderengine/include/renderengine/LayerSettings.h
+++ b/libs/renderengine/include/renderengine/LayerSettings.h
@@ -16,6 +16,7 @@
#pragma once
+#include <android/gui/BorderSettings.h>
#include <gui/DisplayLuts.h>
#include <math/mat4.h>
#include <math/vec3.h>
@@ -71,6 +72,10 @@
// Boundaries of the layer.
FloatRect boundaries = FloatRect();
+ // Boundaries of the layer before transparent region hint is subtracted.
+ // Effects like shadows and outline ignore the transparent region hint.
+ FloatRect originalBounds = FloatRect();
+
// Transform matrix to apply to mesh coordinates.
mat4 positionTransform = mat4();
@@ -127,6 +132,8 @@
ShadowSettings shadow;
+ gui::BorderSettings borderSettings;
+
int backgroundBlurRadius = 0;
std::vector<BlurRegion> blurRegions;
diff --git a/libs/renderengine/skia/SkiaRenderEngine.cpp b/libs/renderengine/skia/SkiaRenderEngine.cpp
index 9e1c226..5b6edb4 100644
--- a/libs/renderengine/skia/SkiaRenderEngine.cpp
+++ b/libs/renderengine/skia/SkiaRenderEngine.cpp
@@ -986,6 +986,30 @@
drawShadow(canvas, rrect, layer.shadow);
}
+ // Similar to shadows, do the rendering before the clip is applied because even when the
+ // layer is occluded it should have an outline.
+ if (layer.borderSettings.strokeWidth > 0) {
+ // TODO(b/367464660): Move this code to the parent scope and
+ // update shadow rendering above to use these bounds since they should be
+ // identical.
+ SkRRect originalBounds, originalClip;
+ std::tie(originalBounds, originalClip) =
+ getBoundsAndClip(layer.geometry.boundaries, layer.geometry.roundedCornersCrop,
+ layer.geometry.roundedCornersRadius);
+ const SkRRect& preferredOriginalBounds =
+ originalBounds.isRect() && !originalClip.isEmpty() ? originalClip
+ : originalBounds;
+
+ SkRRect outlineRect = preferredOriginalBounds;
+ outlineRect.outset(layer.borderSettings.strokeWidth, layer.borderSettings.strokeWidth);
+
+ SkPaint paint;
+ paint.setAntiAlias(true);
+ paint.setColor(layer.borderSettings.color);
+ paint.setStyle(SkPaint::kFill_Style);
+ canvas->drawDRRect(outlineRect, preferredOriginalBounds, paint);
+ }
+
const float layerDimmingRatio = layer.whitePointNits <= 0.f
? displayDimmingRatio
: (layer.whitePointNits / maxLayerWhitePoint) * displayDimmingRatio;
diff --git a/services/surfaceflinger/CompositionEngine/include/compositionengine/LayerFECompositionState.h b/services/surfaceflinger/CompositionEngine/include/compositionengine/LayerFECompositionState.h
index fb8fed0..34b0bb5 100644
--- a/services/surfaceflinger/CompositionEngine/include/compositionengine/LayerFECompositionState.h
+++ b/services/surfaceflinger/CompositionEngine/include/compositionengine/LayerFECompositionState.h
@@ -18,6 +18,7 @@
#include <cstdint>
+#include <android/gui/BorderSettings.h>
#include <android/gui/CachingHint.h>
#include <gui/DisplayLuts.h>
#include <gui/HdrMetadata.h>
@@ -141,6 +142,9 @@
ShadowSettings shadowSettings;
+ // The settings to configure the outline of a layer.
+ gui::BorderSettings borderSettings;
+
// List of regions that require blur
std::vector<BlurRegion> blurRegions;
diff --git a/services/surfaceflinger/CompositionEngine/src/LayerFECompositionState.cpp b/services/surfaceflinger/CompositionEngine/src/LayerFECompositionState.cpp
index 348111d..294b167 100644
--- a/services/surfaceflinger/CompositionEngine/src/LayerFECompositionState.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/LayerFECompositionState.cpp
@@ -70,6 +70,9 @@
out.append(" ");
dumpVal(out, "shadowLength", shadowSettings.length);
+ out.append(" ");
+ dumpVal(out, "borderSettings", borderSettings.toString());
+
out.append("\n ");
dumpVal(out, "blend", toString(blendMode), blendMode);
dumpVal(out, "alpha", alpha);
diff --git a/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp b/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp
index ea36011..e4793a4 100644
--- a/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp
+++ b/services/surfaceflinger/CompositionEngine/src/OutputLayer.cpp
@@ -237,6 +237,16 @@
geomLayerBounds.bottom += outset;
}
+ // Similar to above
+ if (layerState.forceClientComposition && layerState.borderSettings.strokeWidth > 0.0f) {
+ // Antialiasing should never add more than 2 pixels.
+ const auto outset = layerState.borderSettings.strokeWidth + 2;
+ geomLayerBounds.left -= outset;
+ geomLayerBounds.top -= outset;
+ geomLayerBounds.right += outset;
+ geomLayerBounds.bottom += outset;
+ }
+
geomLayerBounds = layerTransform.transform(geomLayerBounds);
FloatRect frame = reduce(geomLayerBounds, activeTransparentRegion);
frame = frame.intersect(outputState.layerStackSpace.getContent().toFloatRect());
diff --git a/services/surfaceflinger/CompositionEngine/tests/OutputLayerTest.cpp b/services/surfaceflinger/CompositionEngine/tests/OutputLayerTest.cpp
index ca262ee..2f531f1 100644
--- a/services/surfaceflinger/CompositionEngine/tests/OutputLayerTest.cpp
+++ b/services/surfaceflinger/CompositionEngine/tests/OutputLayerTest.cpp
@@ -355,6 +355,26 @@
EXPECT_THAT(calculateOutputDisplayFrame(), expected);
}
+TEST_F(OutputLayerDisplayFrameTest, outlineExpandsDisplayFrame) {
+ const int kStrokeWidth = 3;
+ mLayerFEState.borderSettings.strokeWidth = kStrokeWidth;
+ mLayerFEState.forceClientComposition = true;
+
+ mLayerFEState.geomLayerBounds = FloatRect{100.f, 100.f, 200.f, 200.f};
+ Rect expected{mLayerFEState.geomLayerBounds};
+ expected.inset(-kStrokeWidth - 2, -kStrokeWidth - 2, -kStrokeWidth - 2, -kStrokeWidth - 2);
+ EXPECT_THAT(calculateOutputDisplayFrame(), expected);
+}
+TEST_F(OutputLayerDisplayFrameTest, outlineExpandsDisplayFrame_onlyIfForcingClientComposition) {
+ const int kStrokeWidth = 3;
+ mLayerFEState.borderSettings.strokeWidth = kStrokeWidth;
+ mLayerFEState.forceClientComposition = false;
+
+ mLayerFEState.geomLayerBounds = FloatRect{100.f, 100.f, 200.f, 200.f};
+ Rect expected{mLayerFEState.geomLayerBounds};
+ EXPECT_THAT(calculateOutputDisplayFrame(), expected);
+}
+
/*
* OutputLayer::calculateOutputRelativeBufferTransform()
*/
diff --git a/services/surfaceflinger/FrontEnd/LayerSnapshot.cpp b/services/surfaceflinger/FrontEnd/LayerSnapshot.cpp
index 964a970..3aa2e98 100644
--- a/services/surfaceflinger/FrontEnd/LayerSnapshot.cpp
+++ b/services/surfaceflinger/FrontEnd/LayerSnapshot.cpp
@@ -179,8 +179,12 @@
return backgroundBlurRadius > 0 || blurRegions.size() > 0;
}
+bool LayerSnapshot::hasOutline() const {
+ return borderSettings.strokeWidth > 0;
+}
+
bool LayerSnapshot::hasEffect() const {
- return fillsColor() || drawShadows() || hasBlur();
+ return fillsColor() || drawShadows() || hasBlur() || hasOutline();
}
bool LayerSnapshot::hasSomethingToDraw() const {
@@ -253,6 +257,7 @@
reason << " buffer=" << externalTexture->getId() << " frame=" << frameNumber;
if (fillsColor() || color.a > 0.0f) reason << " color{" << color << "}";
if (drawShadows()) reason << " shadowSettings.length=" << shadowSettings.length;
+ if (hasOutline()) reason << "borderSettings=" << borderSettings.toString();
if (backgroundBlurRadius > 0) reason << " backgroundBlurRadius=" << backgroundBlurRadius;
if (blurRegions.size() > 0) reason << " blurRegions.size()=" << blurRegions.size();
if (contentDirty) reason << " contentDirty";
@@ -410,7 +415,9 @@
if (forceUpdate || requested.what & layer_state_t::eShadowRadiusChanged) {
shadowSettings.length = requested.shadowRadius;
}
-
+ if (forceUpdate || requested.what & layer_state_t::eBorderSettingsChanged) {
+ borderSettings = requested.borderSettings;
+ }
if (forceUpdate || requested.what & layer_state_t::eFrameRateSelectionPriority) {
frameRateSelectionPriority = requested.frameRateSelectionPriority;
}
@@ -508,9 +515,9 @@
(layer_state_t::eBufferChanged | layer_state_t::eDataspaceChanged |
layer_state_t::eApiChanged | layer_state_t::eShadowRadiusChanged |
layer_state_t::eBlurRegionsChanged | layer_state_t::eStretchChanged |
- layer_state_t::eEdgeExtensionChanged)) {
+ layer_state_t::eEdgeExtensionChanged | layer_state_t::eBorderSettingsChanged)) {
forceClientComposition = shadowSettings.length > 0 || stretchEffect.hasEffect() ||
- edgeExtensionEffect.hasEffect();
+ edgeExtensionEffect.hasEffect() || borderSettings.strokeWidth > 0;
}
if (forceUpdate ||
diff --git a/services/surfaceflinger/FrontEnd/LayerSnapshot.h b/services/surfaceflinger/FrontEnd/LayerSnapshot.h
index 69120bd..eca9718 100644
--- a/services/surfaceflinger/FrontEnd/LayerSnapshot.h
+++ b/services/surfaceflinger/FrontEnd/LayerSnapshot.h
@@ -149,6 +149,7 @@
bool hasBlur() const;
bool hasBufferOrSidebandStream() const;
bool hasEffect() const;
+ bool hasOutline() const;
bool hasSomethingToDraw() const;
bool isContentOpaque() const;
bool isHiddenByPolicy() const;
diff --git a/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp b/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp
index 28a6031..91b54af 100644
--- a/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp
+++ b/services/surfaceflinger/FrontEnd/LayerSnapshotBuilder.cpp
@@ -939,6 +939,18 @@
}
if (forceUpdate ||
+ snapshot.clientChanges &
+ (layer_state_t::eBorderSettingsChanged | layer_state_t::eAlphaChanged)) {
+ snapshot.borderSettings = requested.borderSettings;
+
+ // Multiply outline alpha by snapshot alpha.
+ uint32_t c = static_cast<uint32_t>(snapshot.borderSettings.color);
+ float alpha = snapshot.alpha * (c >> 24) / 255.0f;
+ uint32_t a = static_cast<uint32_t>(alpha * 255 + 0.5f);
+ snapshot.borderSettings.color = static_cast<int32_t>((c & ~0xff000000) | (a << 24));
+ }
+
+ if (forceUpdate ||
snapshot.changes.any(RequestedLayerState::Changes::Geometry |
RequestedLayerState::Changes::Input)) {
updateInput(snapshot, requested, parentSnapshot, path, args);
@@ -946,7 +958,9 @@
// computed snapshot properties
snapshot.forceClientComposition = snapshot.shadowSettings.length > 0 ||
- snapshot.stretchEffect.hasEffect() || snapshot.edgeExtensionEffect.hasEffect();
+ snapshot.stretchEffect.hasEffect() || snapshot.edgeExtensionEffect.hasEffect() ||
+ snapshot.borderSettings.strokeWidth > 0;
+
snapshot.contentOpaque = snapshot.isContentOpaque();
snapshot.isOpaque = snapshot.contentOpaque && !snapshot.roundedCorner.hasRoundedCorners() &&
snapshot.color.a == 1.f;
diff --git a/services/surfaceflinger/LayerFE.cpp b/services/surfaceflinger/LayerFE.cpp
index 5e076bd..3cd432c 100644
--- a/services/surfaceflinger/LayerFE.cpp
+++ b/services/surfaceflinger/LayerFE.cpp
@@ -113,6 +113,8 @@
// set the shadow for the layer if needed
prepareShadowClientComposition(*layerSettings, targetSettings.viewport);
+ layerSettings->borderSettings = mSnapshot->borderSettings;
+
return layerSettings;
}
@@ -120,6 +122,7 @@
compositionengine::LayerFE::ClientCompositionTargetSettings& targetSettings) const {
SFTRACE_CALL();
compositionengine::LayerFE::LayerSettings layerSettings;
+ layerSettings.geometry.originalBounds = mSnapshot->geomLayerBounds;
layerSettings.geometry.boundaries =
reduce(mSnapshot->geomLayerBounds, mSnapshot->transparentRegionHint);
layerSettings.geometry.positionTransform = mSnapshot->geomLayerTransform.asMatrix4();
@@ -205,7 +208,7 @@
if (targetSettings.realContentIsVisible && fillsColor()) {
// Set color for color fill settings.
layerSettings.source.solidColor = mSnapshot->color.rgb;
- } else if (hasBlur() || drawShadows()) {
+ } else if (hasBlur() || drawShadows() || hasOutline()) {
layerSettings.skipContentDraw = true;
}
}
@@ -392,6 +395,10 @@
return mSnapshot->backgroundBlurRadius > 0 || mSnapshot->blurRegions.size() > 0;
}
+bool LayerFE::hasOutline() const {
+ return mSnapshot->borderSettings.strokeWidth > 0;
+}
+
bool LayerFE::drawShadows() const {
return mSnapshot->shadowSettings.length > 0.f &&
(mSnapshot->shadowSettings.ambientColor.a > 0 ||
diff --git a/services/surfaceflinger/LayerFE.h b/services/surfaceflinger/LayerFE.h
index b89b6b4..b897a90 100644
--- a/services/surfaceflinger/LayerFE.h
+++ b/services/surfaceflinger/LayerFE.h
@@ -83,12 +83,13 @@
compositionengine::LayerFE::LayerSettings&,
compositionengine::LayerFE::ClientCompositionTargetSettings&) const;
- bool hasEffect() const { return fillsColor() || drawShadows() || hasBlur(); }
+ bool hasEffect() const { return fillsColor() || drawShadows() || hasBlur() || hasOutline(); }
bool hasBufferOrSidebandStream() const;
bool fillsColor() const;
bool hasBlur() const;
bool drawShadows() const;
+ bool hasOutline() const;
const sp<GraphicBuffer> getBuffer() const;
diff --git a/services/surfaceflinger/common/Android.bp b/services/surfaceflinger/common/Android.bp
index 13f6577..c68513e 100644
--- a/services/surfaceflinger/common/Android.bp
+++ b/services/surfaceflinger/common/Android.bp
@@ -22,6 +22,7 @@
],
static_libs: [
"librenderengine_includes",
+ "libgui_window_info_static",
],
srcs: [
"FlagManager.cpp",
diff --git a/services/surfaceflinger/tests/Android.bp b/services/surfaceflinger/tests/Android.bp
index b5f7a74..37f3aa7 100644
--- a/services/surfaceflinger/tests/Android.bp
+++ b/services/surfaceflinger/tests/Android.bp
@@ -63,7 +63,10 @@
"VirtualDisplay_test.cpp",
"WindowInfosListener_test.cpp",
],
- data: ["SurfaceFlinger_test.filter"],
+ data: [
+ "SurfaceFlinger_test.filter",
+ "testdata/*",
+ ],
static_libs: [
"android.hardware.graphics.composer@2.1",
"libsurfaceflinger_common",
@@ -76,6 +79,7 @@
"libcutils",
"libEGL",
"libGLESv2",
+ "libjnigraphics",
"libgui",
"liblog",
"libnativewindow",
@@ -83,6 +87,7 @@
"libui",
"libutils",
"server_configurable_flags",
+ "libc++",
],
header_libs: [
"libnativewindow_headers",
diff --git a/services/surfaceflinger/tests/AndroidTest.xml b/services/surfaceflinger/tests/AndroidTest.xml
index ad43cdc..b199ddb 100644
--- a/services/surfaceflinger/tests/AndroidTest.xml
+++ b/services/surfaceflinger/tests/AndroidTest.xml
@@ -14,6 +14,11 @@
limitations under the License.
-->
<configuration description="Config for SurfaceFlinger_test">
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="throw-if-cmd-fail" value="true" />
+ <option name="run-command" value="mkdir -p /data/local/tmp/SurfaceFlinger_test_screenshots" />
+ <option name="teardown-command" value="rm -fr /data/local/tmp/SurfaceFlinger_test_screenshots"/>
+ </target_preparer>
<target_preparer class="com.android.tradefed.targetprep.PushFilePreparer">
<option name="cleanup" value="true" />
<option name="push" value="SurfaceFlinger_test->/data/local/tmp/SurfaceFlinger_test" />
@@ -27,4 +32,8 @@
<option name="native-test-device-path" value="/data/local/tmp" />
<option name="module-name" value="SurfaceFlinger_test" />
</test>
-</configuration>
+ <metrics_collector class="com.android.tradefed.device.metric.FilePullerLogCollector">
+ <option name = "pull-pattern-keys" value = ".*png" />
+ <option name = "directory-keys" value = "/data/local/tmp/SurfaceFlinger_test_screenshots" />
+ </metrics_collector>
+</configuration>
\ No newline at end of file
diff --git a/services/surfaceflinger/tests/LayerTypeAndRenderTypeTransaction_test.cpp b/services/surfaceflinger/tests/LayerTypeAndRenderTypeTransaction_test.cpp
index 151611c..ada9862 100644
--- a/services/surfaceflinger/tests/LayerTypeAndRenderTypeTransaction_test.cpp
+++ b/services/surfaceflinger/tests/LayerTypeAndRenderTypeTransaction_test.cpp
@@ -662,6 +662,93 @@
}
}
+TEST_P(LayerTypeAndRenderTypeTransactionTest, SetBorderSettings) {
+ sp<SurfaceControl> parent;
+ sp<SurfaceControl> child;
+ const uint32_t size = 64;
+ const uint32_t parentSize = size * 3;
+ ASSERT_NO_FATAL_FAILURE(parent = createLayer("parent", parentSize, parentSize));
+ ASSERT_NO_FATAL_FAILURE(fillLayerColor(parent, Color::RED, parentSize, parentSize));
+ ASSERT_NO_FATAL_FAILURE(child = createLayer("child", size, size));
+ ASSERT_NO_FATAL_FAILURE(fillLayerColor(child, Color::GREEN, size, size));
+
+ gui::BorderSettings outline;
+ outline.strokeWidth = 3;
+ outline.color = 0xff0000ff;
+ Transaction()
+ .setCrop(parent, Rect(0, 0, parentSize, parentSize))
+ .reparent(child, parent)
+ .setPosition(child, size, size)
+ .setCornerRadius(child, 20.0f)
+ .setBorderSettings(child, outline)
+ .apply(true);
+
+ {
+ auto shot = getScreenCapture();
+
+ shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+ "testdata/SetBorderSettings_Opaque.png");
+ }
+
+ {
+ Transaction().setAlpha(child, 0.5f).apply(true);
+ auto shot = getScreenCapture();
+
+ shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+ "testdata/SetBorderSettings_HalfAlpha.png");
+ }
+
+ {
+ Transaction().setAlpha(child, 0.0f).apply(true);
+
+ auto shot = getScreenCapture();
+
+ shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+ "testdata/SetBorderSettings_ZeroAlpha.png");
+ }
+
+ {
+ Transaction()
+ .setAlpha(child, 1.0f)
+ .setCrop(parent, Rect(0, 0, parentSize / 2, parentSize))
+ .apply(true);
+
+ auto shot = getScreenCapture();
+
+ shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+ "testdata/SetBorderSettings_Cropped.png");
+ }
+
+ {
+ outline.color = 0xff0000ff;
+ outline.strokeWidth = 1;
+ Transaction()
+ .setCrop(parent, Rect(0, 0, parentSize, parentSize))
+ .setBorderSettings(child, outline)
+ .apply(true);
+
+ auto shot = getScreenCapture();
+
+ shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+ "testdata/SetBorderSettings_StrokeWidth1.png");
+ }
+
+ {
+ outline.color = 0x440000ff;
+ outline.strokeWidth = 3;
+ Transaction()
+ .setCrop(parent, Rect(0, 0, parentSize, parentSize))
+ .setBorderSettings(child, outline)
+ .apply(true);
+
+ auto shot = getScreenCapture();
+
+ shot->expectBufferMatchesImageFromFile(Rect(0, 0, parentSize, parentSize),
+ "testdata/"
+ "SetBorderSettings_StrokeColorWithAlpha.png");
+ }
+}
+
TEST_P(LayerTypeAndRenderTypeTransactionTest, SetBackgroundBlurRadiusSimple) {
if (!deviceSupportsBlurs()) GTEST_SKIP();
if (!deviceUsesSkiaRenderEngine()) GTEST_SKIP();
diff --git a/services/surfaceflinger/tests/common/LayerLifecycleManagerHelper.h b/services/surfaceflinger/tests/common/LayerLifecycleManagerHelper.h
index 82390ac..1bee27b 100644
--- a/services/surfaceflinger/tests/common/LayerLifecycleManagerHelper.h
+++ b/services/surfaceflinger/tests/common/LayerLifecycleManagerHelper.h
@@ -504,6 +504,17 @@
mLifecycleManager.applyTransactions(transactions);
}
+ void setBorderSettings(uint32_t id, gui::BorderSettings settings) {
+ std::vector<QueuedTransactionState> transactions;
+ transactions.emplace_back();
+ transactions.back().states.push_back({});
+
+ transactions.back().states.front().state.what = layer_state_t::eBorderSettingsChanged;
+ transactions.back().states.front().layerId = id;
+ transactions.back().states.front().state.borderSettings = settings;
+ mLifecycleManager.applyTransactions(transactions);
+ }
+
void setTrustedOverlay(uint32_t id, gui::TrustedOverlay trustedOverlay) {
std::vector<QueuedTransactionState> transactions;
transactions.emplace_back();
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_Cropped.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_Cropped.png
new file mode 100644
index 0000000..b52d517
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_Cropped.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_HalfAlpha.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_HalfAlpha.png
new file mode 100644
index 0000000..e1ab54b
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_HalfAlpha.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_Opaque.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_Opaque.png
new file mode 100644
index 0000000..bbaf0af
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_Opaque.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeColorWithAlpha.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeColorWithAlpha.png
new file mode 100644
index 0000000..0fe2ed8
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeColorWithAlpha.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeWidth1.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeWidth1.png
new file mode 100644
index 0000000..3ee5ac6
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_StrokeWidth1.png
Binary files differ
diff --git a/services/surfaceflinger/tests/testdata/SetBorderSettings_ZeroAlpha.png b/services/surfaceflinger/tests/testdata/SetBorderSettings_ZeroAlpha.png
new file mode 100644
index 0000000..e5e8850
--- /dev/null
+++ b/services/surfaceflinger/tests/testdata/SetBorderSettings_ZeroAlpha.png
Binary files differ
diff --git a/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp b/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp
index 07356b9..d045eb8 100644
--- a/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp
+++ b/services/surfaceflinger/tests/unittests/LayerSnapshotTest.cpp
@@ -1546,6 +1546,14 @@
EXPECT_EQ(getSnapshot(1)->shadowSettings.length, SHADOW_RADIUS);
}
+TEST_F(LayerSnapshotTest, setBorderSettings) {
+ gui::BorderSettings settings;
+ settings.strokeWidth = 5;
+ setBorderSettings(1, settings);
+ UPDATE_AND_VERIFY(mSnapshotBuilder, STARTING_ZORDER);
+ EXPECT_EQ(getSnapshot(1)->borderSettings.strokeWidth, settings.strokeWidth);
+}
+
TEST_F(LayerSnapshotTest, setTrustedOverlayForNonVisibleInput) {
hideLayer(1);
setTrustedOverlay(1, gui::TrustedOverlay::ENABLED);
diff --git a/services/surfaceflinger/tests/utils/ScreenshotUtils.h b/services/surfaceflinger/tests/utils/ScreenshotUtils.h
index 0bedcd1..02c3ecd 100644
--- a/services/surfaceflinger/tests/utils/ScreenshotUtils.h
+++ b/services/surfaceflinger/tests/utils/ScreenshotUtils.h
@@ -15,15 +15,23 @@
*/
#pragma once
+#include <android-base/file.h>
+#include <android/bitmap.h>
+#include <android/data_space.h>
+#include <android/imagedecoder.h>
#include <gui/AidlUtil.h>
#include <gui/SyncScreenCaptureListener.h>
#include <private/gui/ComposerServiceAIDL.h>
#include <ui/FenceResult.h>
+#include <ui/PixelFormat.h>
#include <ui/Rect.h>
#include <utils/String8.h>
#include <functional>
#include "TransactionUtils.h"
+#include <filesystem>
+#include <fstream>
+
namespace android {
using gui::aidl_utils::statusTFromBinderStatus;
@@ -174,6 +182,146 @@
}
}
+ static void writePng(const std::filesystem::path& path, const void* pixels, uint32_t width,
+ uint32_t height, uint32_t stride) {
+ AndroidBitmapInfo info{
+ .width = width,
+ .height = height,
+ .stride = stride,
+ .format = ANDROID_BITMAP_FORMAT_RGBA_8888,
+ .flags = ANDROID_BITMAP_FLAGS_ALPHA_OPAQUE,
+ };
+
+ std::ofstream file(path, std::ios::binary);
+ ASSERT_TRUE(file.is_open());
+
+ auto writeFunc = [](void* filePtr, const void* data, size_t size) -> bool {
+ auto file = reinterpret_cast<std::ofstream*>(filePtr);
+ file->write(reinterpret_cast<const char*>(data), size);
+ return file->good();
+ };
+
+ int compressResult = AndroidBitmap_compress(&info, ADATASPACE_SRGB, pixels,
+ ANDROID_BITMAP_COMPRESS_FORMAT_PNG,
+ /*(ignored) quality=*/100, &file, writeFunc);
+ ASSERT_EQ(compressResult, ANDROID_BITMAP_RESULT_SUCCESS);
+ file.close();
+ }
+
+ static void readImage(const std::filesystem::path& filename, std::vector<uint8_t>& outBytes,
+ int& outWidth, int& outHeight) {
+ std::ifstream file(filename, std::ios::binary | std::ios::ate);
+ ASSERT_TRUE(file.is_open()) << "Failed to open " << filename;
+
+ size_t fileSize = file.tellg();
+ file.seekg(0, std::ios::beg);
+ std::vector<char> fileData(fileSize);
+ file.read(fileData.data(), fileSize);
+ file.close();
+
+ AImageDecoder* decoder = nullptr;
+ int createResult = AImageDecoder_createFromBuffer(fileData.data(), fileSize, &decoder);
+
+ ASSERT_EQ(createResult, ANDROID_IMAGE_DECODER_SUCCESS);
+
+ const AImageDecoderHeaderInfo* headerInfo = AImageDecoder_getHeaderInfo(decoder);
+ outWidth = AImageDecoderHeaderInfo_getWidth(headerInfo);
+ outHeight = AImageDecoderHeaderInfo_getHeight(headerInfo);
+ int32_t format = AImageDecoderHeaderInfo_getAndroidBitmapFormat(headerInfo);
+ ASSERT_EQ(format, ANDROID_BITMAP_FORMAT_RGBA_8888);
+
+ size_t stride = outWidth * 4; // Assuming RGBA format
+ size_t bufferSize = stride * outHeight;
+
+ outBytes.resize(bufferSize);
+ int decodeResult = AImageDecoder_decodeImage(decoder, outBytes.data(), stride, bufferSize);
+ ASSERT_EQ(decodeResult, ANDROID_IMAGE_DECODER_SUCCESS);
+ AImageDecoder_delete(decoder);
+ }
+
+ static void writeGraphicBufferToPng(const std::string& path, const sp<GraphicBuffer>& buffer) {
+ base::unique_fd fd{open(path.c_str(), O_WRONLY | O_CREAT, S_IWUSR)};
+ ASSERT_GE(fd.get(), 0);
+
+ void* pixels = nullptr;
+ int32_t stride = 0;
+ auto lockStatus = buffer->lock(GRALLOC_USAGE_SW_READ_OFTEN, &pixels,
+ nullptr /*outBytesPerPixel*/, &stride);
+ ASSERT_GE(lockStatus, 0);
+
+ writePng(path, pixels, buffer->getWidth(), buffer->getHeight(), stride);
+
+ auto unlockStatus = buffer->unlock();
+ ASSERT_GE(unlockStatus, 0);
+ }
+
+ // Tries to read an image from executable directory
+ // If the test fails, the screenshot is written to $TMPDIR
+ void expectBufferMatchesImageFromFile(const Rect& rect,
+ const std::filesystem::path& pathRelativeToExeDir) {
+ ASSERT_NE(nullptr, mOutBuffer);
+ ASSERT_EQ(HAL_PIXEL_FORMAT_RGBA_8888, mOutBuffer->getPixelFormat());
+
+ int bufferWidth = int32_t(mOutBuffer->getWidth());
+ int bufferHeight = int32_t(mOutBuffer->getHeight());
+ int bufferStride = mOutBuffer->getStride() * 4;
+
+ std::vector<uint8_t> imagePixels;
+ int imageWidth;
+ int imageHeight;
+ readImage(android::base::GetExecutableDirectory() / pathRelativeToExeDir, imagePixels,
+ imageWidth, imageHeight);
+ int imageStride = 4 * imageWidth;
+
+ ASSERT_TRUE(rect.isValid());
+
+ ASSERT_GE(rect.left, 0);
+ ASSERT_GE(rect.bottom, 0);
+
+ ASSERT_LE(rect.right, bufferWidth);
+ ASSERT_LE(rect.bottom, bufferHeight);
+
+ ASSERT_LE(rect.right, imageWidth);
+ ASSERT_LE(rect.bottom, imageHeight);
+
+ int tolerance = 4; // arbitrary
+ for (int32_t y = rect.top; y < rect.bottom; y++) {
+ for (int32_t x = rect.left; x < rect.right; x++) {
+ const uint8_t* bufferPixel = mPixels + y * bufferStride + x * 4;
+ const uint8_t* imagePixel =
+ imagePixels.data() + (y - rect.top) * imageStride + (x - rect.left) * 4;
+
+ int dr = bufferPixel[0] - imagePixel[0];
+ int dg = bufferPixel[1] - imagePixel[1];
+ int db = bufferPixel[2] - imagePixel[2];
+ int da = bufferPixel[3] - imagePixel[3];
+ int dist = std::abs(dr) + std::abs(dg) + std::abs(db) + std::abs(da);
+
+ bool pixelMatches = dist < tolerance;
+
+ if (!pixelMatches) {
+ std::filesystem::path outFilename = pathRelativeToExeDir.filename();
+ outFilename.replace_extension();
+ outFilename += "_actual.png";
+ std::filesystem::path outPath = std::filesystem::temp_directory_path() /
+ "SurfaceFlinger_test_screenshots" / outFilename;
+ writeGraphicBufferToPng(outPath, mOutBuffer);
+
+ ASSERT_TRUE(pixelMatches)
+ << String8::format("pixel @ (%3d, %3d): "
+ "expected [%3d, %3d, %3d, %3d], got [%3d, %3d, %3d, "
+ "%3d], "
+ "wrote screenshot to '%s'",
+ x, y, imagePixel[0], imagePixel[1], imagePixel[2],
+ imagePixel[3], bufferPixel[0], bufferPixel[1],
+ bufferPixel[2], bufferPixel[3], outPath.c_str())
+ .c_str();
+ return;
+ }
+ }
+ }
+ }
+
Color getPixelColor(uint32_t x, uint32_t y) {
if (!mOutBuffer || mOutBuffer->getPixelFormat() != HAL_PIXEL_FORMAT_RGBA_8888) {
return {0, 0, 0, 0};