Merge "Don't pilfer pointers with 3-button-nav predictive back" into main
diff --git a/core/api/current.txt b/core/api/current.txt
index 88180e5..1b42977 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -20809,6 +20809,7 @@
method public int describeContents();
method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @FloatRange(from=0.0f, to=1.0f) public float getDefaultBrightness();
method public int getDensityDpi();
+ method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @FloatRange(from=0.0f, to=1.0f) public float getDimBrightness();
method @NonNull public java.util.Set<java.lang.String> getDisplayCategories();
method public int getFlags();
method public int getHeight();
@@ -20830,6 +20831,7 @@
method @NonNull public android.hardware.display.VirtualDisplayConfig build();
method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setBrightnessListener(@NonNull java.util.concurrent.Executor, @NonNull android.hardware.display.VirtualDisplayConfig.BrightnessListener);
method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDefaultBrightness(@FloatRange(from=0.0f, to=1.0f) float);
+ method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDimBrightness(@FloatRange(from=0.0f, to=1.0f) float);
method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setDisplayCategories(@NonNull java.util.Set<java.lang.String>);
method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setFlags(int);
method @NonNull public android.hardware.display.VirtualDisplayConfig.Builder setRequestedRefreshRate(@FloatRange(from=0.0f) float);
diff --git a/core/java/android/hardware/display/VirtualDisplayConfig.java b/core/java/android/hardware/display/VirtualDisplayConfig.java
index eceaa8f..7257055 100644
--- a/core/java/android/hardware/display/VirtualDisplayConfig.java
+++ b/core/java/android/hardware/display/VirtualDisplayConfig.java
@@ -66,6 +66,7 @@
private final DisplayCutout mDisplayCutout;
private final boolean mIgnoreActivitySizeRestrictions;
private final float mDefaultBrightness;
+ private final float mDimBrightness;
private final IBrightnessListener mBrightnessListener;
private VirtualDisplayConfig(
@@ -84,6 +85,7 @@
@Nullable DisplayCutout displayCutout,
boolean ignoreActivitySizeRestrictions,
@FloatRange(from = 0.0f, to = 1.0f) float defaultBrightness,
+ @FloatRange(from = 0.0f, to = 1.0f) float dimBrightness,
IBrightnessListener brightnessListener) {
mName = name;
mWidth = width;
@@ -100,6 +102,7 @@
mDisplayCutout = displayCutout;
mIgnoreActivitySizeRestrictions = ignoreActivitySizeRestrictions;
mDefaultBrightness = defaultBrightness;
+ mDimBrightness = dimBrightness;
mBrightnessListener = brightnessListener;
}
@@ -180,6 +183,19 @@
}
/**
+ * Returns the dim brightness of the display.
+ *
+ * <p>Value of {@code 0.0} indicates the minimum supported brightness and value of {@code 1.0}
+ * indicates the maximum supported brightness.</p>
+ *
+ * @see Builder#setDimBrightness(float)
+ */
+ @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ public @FloatRange(from = 0.0f, to = 1.0f) float getDimBrightness() {
+ return mDimBrightness;
+ }
+
+ /**
* Returns the listener to get notified about changes in the display brightness.
* @hide
*/
@@ -278,6 +294,7 @@
DisplayCutout.ParcelableWrapper.writeCutoutToParcel(mDisplayCutout, dest, flags);
dest.writeBoolean(mIgnoreActivitySizeRestrictions);
dest.writeFloat(mDefaultBrightness);
+ dest.writeFloat(mDimBrightness);
dest.writeStrongBinder(mBrightnessListener != null ? mBrightnessListener.asBinder() : null);
}
@@ -308,8 +325,8 @@
&& mIgnoreActivitySizeRestrictions == that.mIgnoreActivitySizeRestrictions
&& Objects.equals(mDisplayCutout, that.mDisplayCutout)
&& mDefaultBrightness == that.mDefaultBrightness
+ && mDimBrightness == that.mDimBrightness
&& Objects.equals(mBrightnessListener, that.mBrightnessListener);
-
}
@Override
@@ -318,7 +335,8 @@
mName, mWidth, mHeight, mDensityDpi, mFlags, mSurface, mUniqueId,
mDisplayIdToMirror, mWindowManagerMirroringEnabled, mDisplayCategories,
mRequestedRefreshRate, mIsHomeSupported, mDisplayCutout,
- mIgnoreActivitySizeRestrictions, mDefaultBrightness, mBrightnessListener);
+ mIgnoreActivitySizeRestrictions, mDefaultBrightness, mDimBrightness,
+ mBrightnessListener);
return hashCode;
}
@@ -341,6 +359,7 @@
+ " mDisplayCutout=" + mDisplayCutout
+ " mIgnoreActivitySizeRestrictions=" + mIgnoreActivitySizeRestrictions
+ " mDefaultBrightness=" + mDefaultBrightness
+ + " mDimBrightness=" + mDimBrightness
+ ")";
}
@@ -360,8 +379,8 @@
mDisplayCutout = DisplayCutout.ParcelableWrapper.readCutoutFromParcel(in);
mIgnoreActivitySizeRestrictions = in.readBoolean();
mDefaultBrightness = in.readFloat();
+ mDimBrightness = in.readFloat();
mBrightnessListener = IBrightnessListener.Stub.asInterface(in.readStrongBinder());
-
}
/**
@@ -432,6 +451,7 @@
private DisplayCutout mDisplayCutout = null;
private boolean mIgnoreActivitySizeRestrictions = false;
private float mDefaultBrightness = 0.0f;
+ private float mDimBrightness = PowerManager.BRIGHTNESS_INVALID;
private IBrightnessListener mBrightnessListener = null;
/**
@@ -635,14 +655,14 @@
*
* <p>If unset, defaults to {@code 0.0}</p>
*
+ * @throws IllegalArgumentException if the brightness is outside the valid range [0.0, 1.0]
* @see android.view.View#setKeepScreenOn(boolean)
* @see #setBrightnessListener(Executor, BrightnessListener)
*/
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
@NonNull
public Builder setDefaultBrightness(@FloatRange(from = 0.0f, to = 1.0f) float brightness) {
- if (brightness < PowerManager.BRIGHTNESS_MIN
- || brightness > PowerManager.BRIGHTNESS_MAX) {
+ if (!isValidBrightness(brightness)) {
throw new IllegalArgumentException(
"Virtual display default brightness must be in range [0.0, 1.0]");
}
@@ -651,6 +671,33 @@
}
/**
+ * Sets the dim brightness of the display.
+ *
+ * <p>The system will use this brightness value whenever the display should be dim, i.e.
+ * it is powered on and dimmed due to user activity or app activity.</p>
+ *
+ * <p>Value of {@code 0.0} indicates the minimum supported brightness and value of
+ * {@code 1.0} indicates the maximum supported brightness.</p>
+ *
+ * <p>If set, the default brightness must also be set to a value greater or equal to the
+ * dim brightness. If unset, defaults to the system default.</p>
+ *
+ * @throws IllegalArgumentException if the brightness is outside the valid range [0.0, 1.0]
+ * @see Builder#setDefaultBrightness(float)
+ * @see #setBrightnessListener(Executor, BrightnessListener)
+ */
+ @FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
+ @NonNull
+ public Builder setDimBrightness(@FloatRange(from = 0.0f, to = 1.0f) float brightness) {
+ if (!isValidBrightness(brightness)) {
+ throw new IllegalArgumentException(
+ "Virtual display dim brightness must be in range [0.0, 1.0]");
+ }
+ mDimBrightness = brightness;
+ return this;
+ }
+
+ /**
* Sets the listener to get notified about changes in the display brightness.
*
* @param executor The executor where the callback is executed on.
@@ -666,11 +713,23 @@
return this;
}
+ private boolean isValidBrightness(float brightness) {
+ return !Float.isNaN(brightness) && PowerManager.BRIGHTNESS_MIN <= brightness
+ && brightness <= PowerManager.BRIGHTNESS_MAX;
+ }
+
/**
* Builds the {@link VirtualDisplayConfig} instance.
+ *
+ * @throws IllegalArgumentException if the dim brightness is set to a value greater than
+ * the default brightness.
*/
@NonNull
public VirtualDisplayConfig build() {
+ if (isValidBrightness(mDimBrightness) && mDimBrightness > mDefaultBrightness) {
+ throw new IllegalArgumentException(
+ "The dim brightness must not be greater than the default brightness");
+ }
return new VirtualDisplayConfig(
mName,
mWidth,
@@ -687,6 +746,7 @@
mDisplayCutout,
mIgnoreActivitySizeRestrictions,
mDefaultBrightness,
+ mDimBrightness,
mBrightnessListener);
}
}
diff --git a/core/java/android/os/IPowerManager.aidl b/core/java/android/os/IPowerManager.aidl
index e85e580..4cac4dee 100644
--- a/core/java/android/os/IPowerManager.aidl
+++ b/core/java/android/os/IPowerManager.aidl
@@ -55,7 +55,7 @@
void goToSleepWithDisplayId(int displayId, long time, int reason, int flags);
@UnsupportedAppUsage(maxTargetSdk = 28)
void nap(long time);
- float getBrightnessConstraint(int constraint);
+ float getBrightnessConstraint(int displayId, int constraint);
@UnsupportedAppUsage
boolean isInteractive();
boolean isDisplayInteractive(int displayId);
diff --git a/core/java/android/os/PowerManager.java b/core/java/android/os/PowerManager.java
index 9e7bf47..cd48f08 100644
--- a/core/java/android/os/PowerManager.java
+++ b/core/java/android/os/PowerManager.java
@@ -1266,9 +1266,17 @@
* @hide
*/
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public float getBrightnessConstraint(int constraint) {
+ public float getBrightnessConstraint(@BrightnessConstraint int constraint) {
+ return getBrightnessConstraint(Display.DEFAULT_DISPLAY, constraint);
+ }
+
+ /**
+ * Gets a float screen brightness setting for a specific display.
+ * @hide
+ */
+ public float getBrightnessConstraint(int displayId, @BrightnessConstraint int constraint) {
try {
- return mService.getBrightnessConstraint(constraint);
+ return mService.getBrightnessConstraint(displayId, constraint);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java
index 8b6458a..4307884 100644
--- a/core/java/android/view/DisplayInfo.java
+++ b/core/java/android/view/DisplayInfo.java
@@ -361,6 +361,12 @@
public float brightnessDefault;
/**
+ * The current dim brightness of the display. Value between 0.0 and 1.0,
+ * derived from the configuration of the display device of this logical display.
+ */
+ public float brightnessDim;
+
+ /**
* The {@link RoundedCorners} if present, otherwise {@code null}.
*/
@Nullable
@@ -479,6 +485,7 @@
&& brightnessMinimum == other.brightnessMinimum
&& brightnessMaximum == other.brightnessMaximum
&& brightnessDefault == other.brightnessDefault
+ && brightnessDim == other.brightnessDim
&& Objects.equals(roundedCorners, other.roundedCorners)
&& installOrientation == other.installOrientation
&& Objects.equals(displayShape, other.displayShape)
@@ -546,6 +553,7 @@
brightnessMinimum = other.brightnessMinimum;
brightnessMaximum = other.brightnessMaximum;
brightnessDefault = other.brightnessDefault;
+ brightnessDim = other.brightnessDim;
roundedCorners = other.roundedCorners;
installOrientation = other.installOrientation;
displayShape = other.displayShape;
@@ -620,6 +628,7 @@
brightnessMinimum = source.readFloat();
brightnessMaximum = source.readFloat();
brightnessDefault = source.readFloat();
+ brightnessDim = source.readFloat();
roundedCorners = source.readTypedObject(RoundedCorners.CREATOR);
int numUserDisabledFormats = source.readInt();
userDisabledHdrTypes = new int[numUserDisabledFormats];
@@ -696,6 +705,7 @@
dest.writeFloat(brightnessMinimum);
dest.writeFloat(brightnessMaximum);
dest.writeFloat(brightnessDefault);
+ dest.writeFloat(brightnessDim);
dest.writeTypedObject(roundedCorners, flags);
dest.writeInt(userDisabledHdrTypes.length);
for (int i = 0; i < userDisabledHdrTypes.length; i++) {
@@ -994,6 +1004,8 @@
sb.append(brightnessMaximum);
sb.append(", brightnessDefault ");
sb.append(brightnessDefault);
+ sb.append(", brightnessDim ");
+ sb.append(brightnessDim);
sb.append(", installOrientation ");
sb.append(Surface.rotationToString(installOrientation));
sb.append(", layoutLimitedRefreshRate ");
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 02c7901..049189f 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -28277,7 +28277,7 @@
mPrivateFlags |= PFLAG_FORCE_LAYOUT;
mPrivateFlags |= PFLAG_INVALIDATED;
- if (mParent != null) {
+ if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index 81734a3..d0d4af6 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -116,3 +116,14 @@
description: "Relax the assumption of non-match parent activity"
bug: "356277166"
}
+
+flag {
+ namespace: "windowing_sdk"
+ name: "allow_multiple_adjacent_task_fragments"
+ description: "Refactor to allow more than 2 adjacent TaskFragments"
+ bug: "373709676"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java
new file mode 100644
index 0000000..a090c7a
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedBasicEnvelopeEffect.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright (C) 2024 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.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_DURATION_MS;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INITIAL_SHARPNESS;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INTENSITY;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_SHARPNESS;
+import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_CONTROL_POINT;
+
+import android.annotation.NonNull;
+import android.os.VibrationEffect;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Serialized representation of a basic envelope effect created via
+ * {@link VibrationEffect.BasicEnvelopeBuilder}.
+ *
+ * @hide
+ */
+final class SerializedBasicEnvelopeEffect implements SerializedComposedEffect.SerializedSegment {
+ private final BasicControlPoint[] mControlPoints;
+ private final float mInitialSharpness;
+
+ SerializedBasicEnvelopeEffect(BasicControlPoint[] controlPoints, float initialSharpness) {
+ mControlPoints = controlPoints;
+ mInitialSharpness = initialSharpness;
+ }
+
+ @Override
+ public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+ serializer.startTag(NAMESPACE, TAG_BASIC_ENVELOPE_EFFECT);
+
+ if (!Float.isNaN(mInitialSharpness)) {
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INITIAL_SHARPNESS, mInitialSharpness);
+ }
+
+ for (BasicControlPoint point : mControlPoints) {
+ serializer.startTag(NAMESPACE, TAG_CONTROL_POINT);
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INTENSITY, point.mIntensity);
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_SHARPNESS, point.mSharpness);
+ serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, point.mDurationMs);
+ serializer.endTag(NAMESPACE, TAG_CONTROL_POINT);
+ }
+
+ serializer.endTag(NAMESPACE, TAG_BASIC_ENVELOPE_EFFECT);
+ }
+
+ @Override
+ public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+ VibrationEffect.BasicEnvelopeBuilder builder = new VibrationEffect.BasicEnvelopeBuilder();
+
+ if (!Float.isNaN(mInitialSharpness)) {
+ builder.setInitialSharpness(mInitialSharpness);
+ }
+
+ for (BasicControlPoint point : mControlPoints) {
+ builder.addControlPoint(point.mIntensity, point.mSharpness, point.mDurationMs);
+ }
+ composition.addEffect(builder.build());
+ }
+
+ @Override
+ public String toString() {
+ return "SerializedBasicEnvelopeEffect{"
+ + "initialSharpness=" + (Float.isNaN(mInitialSharpness) ? "" : mInitialSharpness)
+ + ", controlPoints=" + Arrays.toString(mControlPoints)
+ + '}';
+ }
+
+ static final class Builder {
+ private final List<BasicControlPoint> mControlPoints;
+ private float mInitialSharpness = Float.NaN;
+
+ Builder() {
+ mControlPoints = new ArrayList<>();
+ }
+
+ void setInitialSharpness(float sharpness) {
+ mInitialSharpness = sharpness;
+ }
+
+ void addControlPoint(float intensity, float sharpness, long durationMs) {
+ mControlPoints.add(new BasicControlPoint(intensity, sharpness, durationMs));
+ }
+
+ SerializedBasicEnvelopeEffect build() {
+ return new SerializedBasicEnvelopeEffect(
+ mControlPoints.toArray(new BasicControlPoint[0]), mInitialSharpness);
+ }
+ }
+
+ /** Parser implementation for {@link SerializedBasicEnvelopeEffect}. */
+ static final class Parser {
+
+ @NonNull
+ static SerializedBasicEnvelopeEffect parseNext(@NonNull TypedXmlPullParser parser,
+ @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+ XmlValidator.checkStartTag(parser, TAG_BASIC_ENVELOPE_EFFECT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_INITIAL_SHARPNESS);
+
+ Builder builder = new Builder();
+ builder.setInitialSharpness(
+ XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_INITIAL_SHARPNESS, 0f, 1f,
+ Float.NaN));
+
+ int outerDepth = parser.getDepth();
+
+ // Read all nested tags
+ while (XmlReader.readNextTagWithin(parser, outerDepth)) {
+ parseControlPoint(parser, builder);
+ // Consume tag
+ XmlReader.readEndTag(parser);
+ }
+
+ // Check schema assertions about <basic-envelope-effect>
+ XmlValidator.checkParserCondition(!builder.mControlPoints.isEmpty(),
+ "Expected tag %s to have at least one control point",
+ TAG_BASIC_ENVELOPE_EFFECT);
+ XmlValidator.checkParserCondition(builder.mControlPoints.getLast().mIntensity == 0,
+ "Basic envelope effects must end at a zero intensity control point");
+
+ return builder.build();
+ }
+
+ private static void parseControlPoint(TypedXmlPullParser parser, Builder builder)
+ throws XmlParserException {
+ XmlValidator.checkStartTag(parser, TAG_CONTROL_POINT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(
+ parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_INTENSITY,
+ ATTRIBUTE_SHARPNESS);
+ float intensity = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_INTENSITY, 0,
+ 1);
+ float sharpness = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_SHARPNESS, 0,
+ 1);
+ long durationMs = XmlReader.readAttributePositiveLong(parser, ATTRIBUTE_DURATION_MS);
+
+ builder.addControlPoint(intensity, sharpness, durationMs);
+ }
+ }
+
+ private static final class BasicControlPoint {
+ private final float mIntensity;
+ private final float mSharpness;
+ private final long mDurationMs;
+
+ BasicControlPoint(float intensity, float sharpness, long durationMs) {
+ mIntensity = intensity;
+ mSharpness = sharpness;
+ mDurationMs = durationMs;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ROOT, "(%.2f, %.2f, %dms)", mIntensity, mSharpness,
+ mDurationMs);
+ }
+ }
+}
+
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java
new file mode 100644
index 0000000..6a89343
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedWaveformEnvelopeEffect.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2024 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.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_AMPLITUDE;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_DURATION_MS;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_FREQUENCY_HZ;
+import static com.android.internal.vibrator.persistence.XmlConstants.ATTRIBUTE_INITIAL_FREQUENCY_HZ;
+import static com.android.internal.vibrator.persistence.XmlConstants.NAMESPACE;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_CONTROL_POINT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;
+
+import android.annotation.NonNull;
+import android.os.VibrationEffect;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Serialized representation of a waveform envelope effect created via
+ * {@link VibrationEffect.WaveformEnvelopeBuilder}.
+ *
+ * @hide
+ */
+final class SerializedWaveformEnvelopeEffect implements SerializedComposedEffect.SerializedSegment {
+
+ private final WaveformControlPoint[] mControlPoints;
+ private final float mInitialFrequency;
+
+ SerializedWaveformEnvelopeEffect(WaveformControlPoint[] controlPoints, float initialFrequency) {
+ mControlPoints = controlPoints;
+ mInitialFrequency = initialFrequency;
+ }
+
+ @Override
+ public void write(@NonNull TypedXmlSerializer serializer) throws IOException {
+ serializer.startTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT);
+
+ if (!Float.isNaN(mInitialFrequency)) {
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_INITIAL_FREQUENCY_HZ, mInitialFrequency);
+ }
+
+ for (WaveformControlPoint point : mControlPoints) {
+ serializer.startTag(NAMESPACE, TAG_CONTROL_POINT);
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_AMPLITUDE, point.mAmplitude);
+ serializer.attributeFloat(NAMESPACE, ATTRIBUTE_FREQUENCY_HZ, point.mFrequency);
+ serializer.attributeLong(NAMESPACE, ATTRIBUTE_DURATION_MS, point.mDurationMs);
+ serializer.endTag(NAMESPACE, TAG_CONTROL_POINT);
+ }
+
+ serializer.endTag(NAMESPACE, TAG_WAVEFORM_ENVELOPE_EFFECT);
+ }
+
+ @Override
+ public void deserializeIntoComposition(@NonNull VibrationEffect.Composition composition) {
+ VibrationEffect.WaveformEnvelopeBuilder builder =
+ new VibrationEffect.WaveformEnvelopeBuilder();
+
+ if (!Float.isNaN(mInitialFrequency)) {
+ builder.setInitialFrequencyHz(mInitialFrequency);
+ }
+
+ for (WaveformControlPoint point : mControlPoints) {
+ builder.addControlPoint(point.mAmplitude, point.mFrequency, point.mDurationMs);
+ }
+ composition.addEffect(builder.build());
+ }
+
+ @Override
+ public String toString() {
+ return "SerializedWaveformEnvelopeEffect{"
+ + "InitialFrequency=" + (Float.isNaN(mInitialFrequency) ? "" : mInitialFrequency)
+ + ", controlPoints=" + Arrays.toString(mControlPoints)
+ + '}';
+ }
+
+ static final class Builder {
+ private final List<WaveformControlPoint> mControlPoints;
+ private float mInitialFrequencyHz = Float.NaN;
+
+ Builder() {
+ mControlPoints = new ArrayList<>();
+ }
+
+ void setInitialFrequencyHz(float frequencyHz) {
+ mInitialFrequencyHz = frequencyHz;
+ }
+
+ void addControlPoint(float amplitude, float frequencyHz, long durationMs) {
+ mControlPoints.add(new WaveformControlPoint(amplitude, frequencyHz, durationMs));
+ }
+
+ SerializedWaveformEnvelopeEffect build() {
+ return new SerializedWaveformEnvelopeEffect(
+ mControlPoints.toArray(new WaveformControlPoint[0]), mInitialFrequencyHz);
+ }
+ }
+
+ /** Parser implementation for {@link SerializedWaveformEnvelopeEffect}. */
+ static final class Parser {
+
+ @NonNull
+ static SerializedWaveformEnvelopeEffect parseNext(@NonNull TypedXmlPullParser parser,
+ @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+ XmlValidator.checkStartTag(parser, TAG_WAVEFORM_ENVELOPE_EFFECT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ);
+
+ Builder builder = new Builder();
+ builder.setInitialFrequencyHz(
+ XmlReader.readAttributePositiveFloat(parser, ATTRIBUTE_INITIAL_FREQUENCY_HZ,
+ Float.NaN));
+
+ int outerDepth = parser.getDepth();
+
+ while (XmlReader.readNextTagWithin(parser, outerDepth)) {
+ parseControlPoint(parser, builder);
+ // Consume tag
+ XmlReader.readEndTag(parser);
+ }
+
+ // Check schema assertions about <waveform-envelope-effect>
+ XmlValidator.checkParserCondition(!builder.mControlPoints.isEmpty(),
+ "Expected tag %s to have at least one control point",
+ TAG_WAVEFORM_ENVELOPE_EFFECT);
+
+ return builder.build();
+ }
+
+ private static void parseControlPoint(TypedXmlPullParser parser, Builder builder)
+ throws XmlParserException {
+ XmlValidator.checkStartTag(parser, TAG_CONTROL_POINT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(
+ parser, ATTRIBUTE_DURATION_MS, ATTRIBUTE_AMPLITUDE,
+ ATTRIBUTE_FREQUENCY_HZ);
+ float amplitude = XmlReader.readAttributeFloatInRange(parser, ATTRIBUTE_AMPLITUDE, 0,
+ 1);
+ float frequencyHz = XmlReader.readAttributePositiveFloat(parser,
+ ATTRIBUTE_FREQUENCY_HZ);
+ long durationMs = XmlReader.readAttributePositiveLong(parser, ATTRIBUTE_DURATION_MS);
+
+ builder.addControlPoint(amplitude, frequencyHz, durationMs);
+ }
+ }
+
+ private static final class WaveformControlPoint {
+ private final float mAmplitude;
+ private final float mFrequency;
+ private final long mDurationMs;
+
+ WaveformControlPoint(float amplitude, float frequency, long durationMs) {
+ mAmplitude = amplitude;
+ mFrequency = frequency;
+ mDurationMs = durationMs;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.ROOT, "(%.2f, %.2f, %dms)", mAmplitude, mFrequency,
+ mDurationMs);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
index a9fbcaf..314bfe4 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
@@ -16,11 +16,13 @@
package com.android.internal.vibrator.persistence;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_BASIC_ENVELOPE_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PRIMITIVE_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VIBRATION_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_ENVELOPE_EFFECT;
import android.annotation.NonNull;
import android.os.VibrationEffect;
@@ -92,6 +94,32 @@
* }
* </pre>
*
+ * * Waveform Envelope effects
+ *
+ * <pre>
+ * {@code
+ * <vibration-effect>
+ * <waveform-envelope-effect initialFrequencyHz="20.0">
+ * <control-point amplitude="0.2" frequencyHz="80.0" durationMs="50" />
+ * <control-point amplitude="0.5" frequencyHz="150.0" durationMs="50" />
+ * </envelope-effect>
+ * </vibration-effect>
+ * }
+ * </pre>
+ *
+ * * Basic Envelope effects
+ *
+ * <pre>
+ * {@code
+ * <vibration-effect>
+ * <basic-envelope-effect initialSharpness="0.3">
+ * <control-point intensity="0.2" sharpness="0.5" durationMs="50" />
+ * <control-point intensity="0.0" sharpness="1.0" durationMs="50" />
+ * </envelope-effect>
+ * </vibration-effect>
+ * }
+ * </pre>
+ *
* @hide
*/
public class VibrationEffectXmlParser {
@@ -151,6 +179,18 @@
serializedVibration = new SerializedComposedEffect(
SerializedAmplitudeStepWaveform.Parser.parseNext(parser));
break;
+ case TAG_WAVEFORM_ENVELOPE_EFFECT:
+ if (Flags.normalizedPwleEffects()) {
+ serializedVibration = new SerializedComposedEffect(
+ SerializedWaveformEnvelopeEffect.Parser.parseNext(parser, flags));
+ break;
+ } // else fall through
+ case TAG_BASIC_ENVELOPE_EFFECT:
+ if (Flags.normalizedPwleEffects()) {
+ serializedVibration = new SerializedComposedEffect(
+ SerializedBasicEnvelopeEffect.Parser.parseNext(parser, flags));
+ break;
+ } // else fall through
default:
throw new XmlParserException("Unexpected tag " + parser.getName()
+ " in vibration tag " + vibrationTagName);
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
index cb834a5..ebe3434 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
@@ -19,9 +19,11 @@
import android.annotation.NonNull;
import android.os.PersistableBundle;
import android.os.VibrationEffect;
+import android.os.vibrator.BasicPwleSegment;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
+import android.os.vibrator.PwleSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
@@ -45,6 +47,8 @@
* <li>A composition created exclusively via
* {@link VibrationEffect.Composition#addPrimitive(int, float, int)}
* <li>{@link VibrationEffect#createVendorEffect(PersistableBundle)}
+ * <li>{@link VibrationEffect.WaveformEnvelopeBuilder}
+ * <li>{@link VibrationEffect.BasicEnvelopeBuilder}
* </ul>
*
* @hide
@@ -77,6 +81,12 @@
if (firstSegment instanceof PrimitiveSegment) {
return serializePrimitiveEffect(composed);
}
+ if (Flags.normalizedPwleEffects() && firstSegment instanceof PwleSegment) {
+ return serializeWaveformEnvelopeEffect(composed);
+ }
+ if (Flags.normalizedPwleEffects() && firstSegment instanceof BasicPwleSegment) {
+ return serializeBasicEnvelopeEffect(composed);
+ }
return serializeWaveformEffect(composed);
}
@@ -110,6 +120,53 @@
return new SerializedComposedEffect(primitives);
}
+ private static SerializedComposedEffect serializeWaveformEnvelopeEffect(
+ VibrationEffect.Composed effect) throws XmlSerializerException {
+ SerializedWaveformEnvelopeEffect.Builder builder =
+ new SerializedWaveformEnvelopeEffect.Builder();
+ List<VibrationEffectSegment> segments = effect.getSegments();
+ XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
+ "Unsupported repeating waveform envelope effect %s", effect);
+ for (int i = 0; i < segments.size(); i++) {
+ XmlValidator.checkSerializerCondition(segments.get(i) instanceof PwleSegment,
+ "Unsupported segment for waveform envelope effect %s", segments.get(i));
+ PwleSegment segment = (PwleSegment) segments.get(i);
+
+ if (i == 0 && segment.getStartFrequencyHz() != segment.getEndFrequencyHz()) {
+ // Initial frequency explicitly defined.
+ builder.setInitialFrequencyHz(segment.getStartFrequencyHz());
+ }
+
+ builder.addControlPoint(segment.getEndAmplitude(), segment.getEndFrequencyHz(),
+ segment.getDuration());
+ }
+
+ return new SerializedComposedEffect(builder.build());
+ }
+
+ private static SerializedComposedEffect serializeBasicEnvelopeEffect(
+ VibrationEffect.Composed effect) throws XmlSerializerException {
+ SerializedBasicEnvelopeEffect.Builder builder = new SerializedBasicEnvelopeEffect.Builder();
+ List<VibrationEffectSegment> segments = effect.getSegments();
+ XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
+ "Unsupported repeating basic envelope effect %s", effect);
+ for (int i = 0; i < segments.size(); i++) {
+ XmlValidator.checkSerializerCondition(segments.get(i) instanceof BasicPwleSegment,
+ "Unsupported segment for basic envelope effect %s", segments.get(i));
+ BasicPwleSegment segment = (BasicPwleSegment) segments.get(i);
+
+ if (i == 0 && segment.getStartSharpness() != segment.getEndSharpness()) {
+ // Initial sharpness explicitly defined.
+ builder.setInitialSharpness(segment.getStartSharpness());
+ }
+
+ builder.addControlPoint(segment.getEndIntensity(), segment.getEndSharpness(),
+ segment.getDuration());
+ }
+
+ return new SerializedComposedEffect(builder.build());
+ }
+
private static SerializedComposedEffect serializeWaveformEffect(
VibrationEffect.Composed effect) throws XmlSerializerException {
SerializedAmplitudeStepWaveform.Builder serializedWaveformBuilder =
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
index 4122215..df262cf 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
@@ -42,14 +42,22 @@
public static final String TAG_PREDEFINED_EFFECT = "predefined-effect";
public static final String TAG_PRIMITIVE_EFFECT = "primitive-effect";
public static final String TAG_VENDOR_EFFECT = "vendor-effect";
+ public static final String TAG_WAVEFORM_ENVELOPE_EFFECT = "waveform-envelope-effect";
+ public static final String TAG_BASIC_ENVELOPE_EFFECT = "basic-envelope-effect";
public static final String TAG_WAVEFORM_EFFECT = "waveform-effect";
public static final String TAG_WAVEFORM_ENTRY = "waveform-entry";
public static final String TAG_REPEATING = "repeating";
+ public static final String TAG_CONTROL_POINT = "control-point";
public static final String ATTRIBUTE_NAME = "name";
public static final String ATTRIBUTE_FALLBACK = "fallback";
public static final String ATTRIBUTE_DURATION_MS = "durationMs";
public static final String ATTRIBUTE_AMPLITUDE = "amplitude";
+ public static final String ATTRIBUTE_FREQUENCY_HZ = "frequencyHz";
+ public static final String ATTRIBUTE_INITIAL_FREQUENCY_HZ = "initialFrequencyHz";
+ public static final String ATTRIBUTE_INTENSITY = "intensity";
+ public static final String ATTRIBUTE_SHARPNESS = "sharpness";
+ public static final String ATTRIBUTE_INITIAL_SHARPNESS = "initialSharpness";
public static final String ATTRIBUTE_SCALE = "scale";
public static final String ATTRIBUTE_DELAY_MS = "delayMs";
public static final String ATTRIBUTE_DELAY_TYPE = "delayType";
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlReader.java b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
index 0ac6fef..1c4a783 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlReader.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
@@ -221,12 +221,63 @@
if (parser.getAttributeIndex(NAMESPACE, attrName) < 0) {
return defaultValue;
}
+
+ return readAttributeFloatInRange(parser, attrName, lowerInclusive, upperInclusive);
+ }
+
+ /**
+ * Read attribute from current tag as a float within given inclusive range.
+ */
+ public static float readAttributeFloatInRange(
+ TypedXmlPullParser parser, String attrName, float lowerInclusive,
+ float upperInclusive) throws XmlParserException {
String tagName = parser.getName();
float value = readAttributeFloat(parser, attrName);
XmlValidator.checkParserCondition(value >= lowerInclusive && value <= upperInclusive,
- "Unexpected %s = %f in tag %s, expected %s in [%f, %f]",
- attrName, value, tagName, attrName, lowerInclusive, upperInclusive);
+ "Unexpected %s = %f in tag %s, expected %s in [%f, %f]", attrName, value, tagName,
+ attrName, lowerInclusive, upperInclusive);
+ return value;
+ }
+
+ /**
+ * Read attribute from current tag as a positive float, returning default value if attribute
+ * is missing.
+ */
+ public static float readAttributePositiveFloat(TypedXmlPullParser parser, String attrName,
+ float defaultValue) throws XmlParserException {
+ if (parser.getAttributeIndex(NAMESPACE, attrName) < 0) {
+ return defaultValue;
+ }
+
+ return readAttributePositiveFloat(parser, attrName);
+ }
+
+ /**
+ * Read attribute from current tag as a positive float.
+ */
+ public static float readAttributePositiveFloat(TypedXmlPullParser parser, String attrName)
+ throws XmlParserException {
+ String tagName = parser.getName();
+ float value = readAttributeFloat(parser, attrName);
+
+ XmlValidator.checkParserCondition(value > 0,
+ "Unexpected %s = %d in tag %s, expected %s > 0", attrName, value, tagName,
+ attrName);
+ return value;
+ }
+
+ /**
+ * Read attribute from current tag as a positive long.
+ */
+ public static long readAttributePositiveLong(TypedXmlPullParser parser, String attrName)
+ throws XmlParserException {
+ String tagName = parser.getName();
+ long value = readAttributeLong(parser, attrName);
+
+ XmlValidator.checkParserCondition(value > 0,
+ "Unexpected %s = %d in tag %s, expected %s > 0", attrName, value, tagName,
+ attrName);
return value;
}
@@ -251,4 +302,15 @@
throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e);
}
}
+
+ private static long readAttributeLong(TypedXmlPullParser parser, String attrName)
+ throws XmlParserException {
+ String tagName = parser.getName();
+ try {
+ return parser.getAttributeLong(NAMESPACE, attrName);
+ } catch (XmlPullParserException e) {
+ String rawValue = parser.getAttributeValue(NAMESPACE, attrName);
+ throw XmlParserException.createFromPullParserException(tagName, attrName, rawValue, e);
+ }
+ }
}
diff --git a/core/tests/coretests/src/android/view/ViewGroupTest.java b/core/tests/coretests/src/android/view/ViewGroupTest.java
index 43c404e..ae3ad36 100644
--- a/core/tests/coretests/src/android/view/ViewGroupTest.java
+++ b/core/tests/coretests/src/android/view/ViewGroupTest.java
@@ -213,35 +213,6 @@
assertTrue(autofillableViews.containsAll(Arrays.asList(viewA, viewC)));
}
- @Test
- public void testMeasureCache() {
- final int spec1 = View.MeasureSpec.makeMeasureSpec(100, View.MeasureSpec.AT_MOST);
- final int spec2 = View.MeasureSpec.makeMeasureSpec(50, View.MeasureSpec.AT_MOST);
- final Context context = getInstrumentation().getContext();
- final View child = new View(context);
- final TestView parent = new TestView(context, 0);
- parent.addView(child);
-
- child.setPadding(1, 2, 3, 4);
- parent.measure(spec1, spec1);
- assertEquals(4, parent.getMeasuredWidth());
- assertEquals(6, parent.getMeasuredHeight());
-
- child.setPadding(5, 6, 7, 8);
- parent.measure(spec2, spec2);
- assertEquals(12, parent.getMeasuredWidth());
- assertEquals(14, parent.getMeasuredHeight());
-
- // This ends the state of forceLayout.
- parent.layout(0, 0, 50, 50);
-
- // The cached values should be cleared after the new setPadding is called. And the measured
- // width and height should be up-to-date.
- parent.measure(spec1, spec1);
- assertEquals(12, parent.getMeasuredWidth());
- assertEquals(14, parent.getMeasuredHeight());
- }
-
private static void getUnobscuredTouchableRegion(Region outRegion, View view) {
outRegion.set(view.getLeft(), view.getTop(), view.getRight(), view.getBottom());
final ViewParent parent = view.getParent();
@@ -269,19 +240,6 @@
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// We don't layout this view.
}
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int measuredWidth = 0;
- int measuredHeight = 0;
- final int count = getChildCount();
- for (int i = 0; i < count; i++) {
- final View child = getChildAt(i);
- measuredWidth += child.getPaddingLeft() + child.getPaddingRight();
- measuredHeight += child.getPaddingTop() + child.getPaddingBottom();
- }
- setMeasuredDimension(measuredWidth, measuredHeight);
- }
}
public static class AutofillableTestView extends TestView {
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index 4071057..5f25e93 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -439,6 +439,498 @@
}
@Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_allSucceed() throws Exception {
+ VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
+ .addControlPoint(0.2f, 80f, 10)
+ .addControlPoint(0.5f, 150f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ <control-point amplitude="0.5" frequencyHz="150.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, xml);
+ assertPublicApisRoundTrip(effect);
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, xml);
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffectWithInitialFrequency_allSucceed() throws Exception {
+ VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
+ .setInitialFrequencyHz(20)
+ .addControlPoint(0.2f, 80f, 10)
+ .addControlPoint(0.5f, 150f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0">
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ <control-point amplitude="0.5" frequencyHz="150.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, xml);
+ assertPublicApisRoundTrip(effect);
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, xml);
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_badXml_throwsException() throws IOException {
+ // Incomplete XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10">
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ </waveform-envelope-effect>
+ """);
+
+ // Bad vibration XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """);
+
+ // "waveform-envelope-effect" tag with invalid attributes
+ assertParseElementFails("""
+ <vibration-effect>
+ <waveform-envelope-effect init_freq="20.0">
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_noControlPoints_allFail() throws IOException {
+ String xml = "<vibration-effect><waveform-envelope-effect/></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = "<vibration-effect><waveform-envelope-effect> \n "
+ + "</waveform-envelope-effect></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = "<vibration-effect><waveform-envelope-effect>invalid</waveform-envelope-effect"
+ + "></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0" />
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0"> \n </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0">
+ invalid
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0">
+ <control-point />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_badControlPointData_allFail() throws IOException {
+ String xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="-1" frequencyHz="80.0" durationMs="100" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="0" durationMs="100" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="0">
+ <control-point amplitude="0.2" frequencyHz="30" durationMs="100" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="0" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <waveform-envelope-effect>
+ <control-point amplitude="0.2" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testWaveformEnvelopeEffect_featureFlagDisabled_allFail() throws Exception {
+ VibrationEffect effect = new VibrationEffect.WaveformEnvelopeBuilder()
+ .setInitialFrequencyHz(20)
+ .addControlPoint(0.2f, 80f, 10)
+ .addControlPoint(0.5f, 150f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <waveform-envelope-effect initialFrequencyHz="20.0">
+ <control-point amplitude="0.2" frequencyHz="80.0" durationMs="10" />
+ <control-point amplitude="0.5" frequencyHz="150.0" durationMs="10" />
+ </waveform-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserFails(xml);
+ assertPublicApisSerializerFails(effect);
+ assertHiddenApisParserFails(xml);
+ assertHiddenApisSerializerFails(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_allSucceed() throws Exception {
+ VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+ .addControlPoint(0.2f, 0.5f, 10)
+ .addControlPoint(0.0f, 1f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.5" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, xml);
+ assertPublicApisRoundTrip(effect);
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, xml);
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffectWithInitialSharpness_allSucceed() throws Exception {
+ VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+ .setInitialSharpness(0.3f)
+ .addControlPoint(0.2f, 0.5f, 10)
+ .addControlPoint(0.0f, 1f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.3">
+ <control-point intensity="0.2" sharpness="0.5" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, xml);
+ assertPublicApisRoundTrip(effect);
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, xml);
+ assertHiddenApisRoundTrip(effect);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_badXml_throwsException() throws IOException {
+ // Incomplete XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10">
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """);
+ assertParseElementFails("""
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ """);
+
+ // Bad vibration XML
+ assertParseElementFails("""
+ <vibration-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """);
+
+ // "basic-envelope-effect" tag with invalid attributes
+ assertParseElementFails("""
+ <vibration-effect>
+ <basic-envelope-effect init_sharp="20.0">
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_noControlPoints_allFail() throws IOException {
+ String xml = "<vibration-effect><basic-envelope-effect/></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = "<vibration-effect><basic-envelope-effect> \n "
+ + "</basic-envelope-effect></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = "<vibration-effect><basic-envelope-effect>invalid</basic-envelope-effect"
+ + "></vibration-effect>";
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.2" />
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.2"> \n </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.2">
+ invalid
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.2">
+ <control-point />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_badControlPointData_allFail() throws IOException {
+ String xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="-1" sharpness="0.8" durationMs="100" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="-1" durationMs="100" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="-1.0">
+ <control-point intensity="0.2" sharpness="0.8" durationMs="0" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="2.0">
+ <control-point intensity="0.2" sharpness="0.8" durationMs="0" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" sharpness="0.8" durationMs="10" />
+ <control-point intensity="0.5" sharpness="0.8" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+
+ xml = """
+ <vibration-effect>
+ <basic-envelope-effect>
+ <control-point intensity="0.2" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+ assertPublicApisParserFails(xml);
+ assertHiddenApisParserFails(xml);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_NORMALIZED_PWLE_EFFECTS)
+ public void testBasicEnvelopeEffect_featureFlagDisabled_allFail() throws Exception {
+ VibrationEffect effect = new VibrationEffect.BasicEnvelopeBuilder()
+ .setInitialSharpness(0.3f)
+ .addControlPoint(0.2f, 0.5f, 10)
+ .addControlPoint(0.0f, 1f, 10)
+ .build();
+
+ String xml = """
+ <vibration-effect>
+ <basic-envelope-effect initialSharpness="0.3">
+ <control-point intensity="0.2" sharpness="0.5" durationMs="10" />
+ <control-point intensity="0.0" sharpness="1.0" durationMs="10" />
+ </basic-envelope-effect>
+ </vibration-effect>
+ """;
+
+ assertPublicApisParserFails(xml);
+ assertPublicApisSerializerFails(effect);
+
+ assertHiddenApisParserFails(xml);
+ assertHiddenApisSerializerFails(effect);
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
public void testVendorEffect_allSucceed() throws Exception {
PersistableBundle vendorData = new PersistableBundle();
diff --git a/core/xsd/vibrator/vibration/schema/current.txt b/core/xsd/vibrator/vibration/schema/current.txt
index b4148d6..29f8d19 100644
--- a/core/xsd/vibrator/vibration/schema/current.txt
+++ b/core/xsd/vibrator/vibration/schema/current.txt
@@ -1,6 +1,23 @@
// Signature format: 2.0
package com.android.internal.vibrator.persistence {
+ public class BasicControlPoint {
+ ctor public BasicControlPoint();
+ method public long getDurationMs();
+ method public float getIntensity();
+ method public float getSharpness();
+ method public void setDurationMs(long);
+ method public void setIntensity(float);
+ method public void setSharpness(float);
+ }
+
+ public class BasicEnvelopeEffect {
+ ctor public BasicEnvelopeEffect();
+ method public java.util.List<com.android.internal.vibrator.persistence.BasicControlPoint> getControlPoint();
+ method public float getInitialSharpness();
+ method public void setInitialSharpness(float);
+ }
+
public class PredefinedEffect {
ctor public PredefinedEffect();
method public com.android.internal.vibrator.persistence.PredefinedEffectName getName();
@@ -47,14 +64,18 @@
public class VibrationEffect {
ctor public VibrationEffect();
+ method public com.android.internal.vibrator.persistence.BasicEnvelopeEffect getBasicEnvelopeEffect_optional();
method public com.android.internal.vibrator.persistence.PredefinedEffect getPredefinedEffect_optional();
method public com.android.internal.vibrator.persistence.PrimitiveEffect getPrimitiveEffect_optional();
method public byte[] getVendorEffect_optional();
method public com.android.internal.vibrator.persistence.WaveformEffect getWaveformEffect_optional();
+ method public com.android.internal.vibrator.persistence.WaveformEnvelopeEffect getWaveformEnvelopeEffect_optional();
+ method public void setBasicEnvelopeEffect_optional(com.android.internal.vibrator.persistence.BasicEnvelopeEffect);
method public void setPredefinedEffect_optional(com.android.internal.vibrator.persistence.PredefinedEffect);
method public void setPrimitiveEffect_optional(com.android.internal.vibrator.persistence.PrimitiveEffect);
method public void setVendorEffect_optional(byte[]);
method public void setWaveformEffect_optional(com.android.internal.vibrator.persistence.WaveformEffect);
+ method public void setWaveformEnvelopeEffect_optional(com.android.internal.vibrator.persistence.WaveformEnvelopeEffect);
}
public class VibrationSelect {
@@ -67,6 +88,16 @@
enum_constant public static final com.android.internal.vibrator.persistence.WaveformAmplitudeDefault _default;
}
+ public class WaveformControlPoint {
+ ctor public WaveformControlPoint();
+ method public float getAmplitude();
+ method public long getDurationMs();
+ method public float getFrequencyHz();
+ method public void setAmplitude(float);
+ method public void setDurationMs(long);
+ method public void setFrequencyHz(float);
+ }
+
public class WaveformEffect {
ctor public WaveformEffect();
method public com.android.internal.vibrator.persistence.WaveformEffect.Repeating getRepeating();
@@ -87,6 +118,13 @@
method public void setDurationMs(java.math.BigInteger);
}
+ public class WaveformEnvelopeEffect {
+ ctor public WaveformEnvelopeEffect();
+ method public java.util.List<com.android.internal.vibrator.persistence.WaveformControlPoint> getControlPoint();
+ method public float getInitialFrequencyHz();
+ method public void setInitialFrequencyHz(float);
+ }
+
public class XmlParser {
ctor public XmlParser();
method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
index 910a9b7..b4df2d1 100644
--- a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
+++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
@@ -54,6 +54,12 @@
<xs:element name="primitive-effect" type="PrimitiveEffect"/>
</xs:sequence>
+ <!-- Waveform envelope effect -->
+ <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect"/>
+
+ <!-- Basic envelope effect -->
+ <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
+
</xs:choice>
</xs:complexType>
@@ -180,4 +186,54 @@
</xs:restriction>
</xs:simpleType>
+ <!-- Definition of a waveform envelope effect -->
+ <xs:complexType name="WaveformEnvelopeEffect">
+ <xs:sequence>
+ <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+ type="WaveformControlPoint" />
+ </xs:sequence>
+ <xs:attribute name="initialFrequencyHz" type="ControlPointFrequency" />
+ </xs:complexType>
+
+ <!-- Definition of a basic envelope effect -->
+ <xs:complexType name="BasicEnvelopeEffect">
+ <xs:sequence>
+ <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+ type="BasicControlPoint" />
+ </xs:sequence>
+ <xs:attribute name="initialSharpness" type="NormalizedControlPointUnit" />
+ </xs:complexType>
+
+ <xs:complexType name="WaveformControlPoint">
+ <xs:attribute name="amplitude" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="frequencyHz" type="ControlPointFrequency" use="required"/>
+ <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="BasicControlPoint">
+ <xs:attribute name="intensity" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="sharpness" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+ </xs:complexType>
+
+ <xs:simpleType name="ControlPointFrequency">
+ <xs:restriction base="xs:float">
+ <xs:minExclusive value="0"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:simpleType name="PositiveLong">
+ <xs:restriction base="xs:long">
+ <xs:minExclusive value="0"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <!-- Normalized control point unit float in [0,1] -->
+ <xs:simpleType name="NormalizedControlPointUnit">
+ <xs:restriction base="xs:float">
+ <xs:minInclusive value="0"/>
+ <xs:maxInclusive value="1"/>
+ </xs:restriction>
+ </xs:simpleType>
+
</xs:schema>
diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd
index 3c8e016..fba966f 100644
--- a/core/xsd/vibrator/vibration/vibration.xsd
+++ b/core/xsd/vibrator/vibration/vibration.xsd
@@ -52,6 +52,12 @@
<xs:element name="primitive-effect" type="PrimitiveEffect"/>
</xs:sequence>
+ <!-- Waveform envelope effect -->
+ <xs:element name="waveform-envelope-effect" type="WaveformEnvelopeEffect"/>
+
+ <!-- Basic envelope effect -->
+ <xs:element name="basic-envelope-effect" type="BasicEnvelopeEffect"/>
+
</xs:choice>
</xs:complexType>
@@ -157,4 +163,54 @@
</xs:restriction>
</xs:simpleType>
+ <!-- Definition of a waveform envelope effect -->
+ <xs:complexType name="WaveformEnvelopeEffect">
+ <xs:sequence>
+ <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+ type="WaveformControlPoint" />
+ </xs:sequence>
+ <xs:attribute name="initialFrequencyHz" type="ControlPointFrequency" />
+ </xs:complexType>
+
+ <!-- Definition of a basic envelope effect -->
+ <xs:complexType name="BasicEnvelopeEffect">
+ <xs:sequence>
+ <xs:element name="control-point" maxOccurs="unbounded" minOccurs="1"
+ type="BasicControlPoint" />
+ </xs:sequence>
+ <xs:attribute name="initialSharpness" type="NormalizedControlPointUnit" />
+ </xs:complexType>
+
+ <xs:complexType name="WaveformControlPoint">
+ <xs:attribute name="amplitude" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="frequencyHz" type="ControlPointFrequency" use="required"/>
+ <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+ </xs:complexType>
+
+ <xs:complexType name="BasicControlPoint">
+ <xs:attribute name="intensity" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="sharpness" type="NormalizedControlPointUnit" use="required"/>
+ <xs:attribute name="durationMs" type="PositiveLong" use="required"/>
+ </xs:complexType>
+
+ <xs:simpleType name="ControlPointFrequency">
+ <xs:restriction base="xs:float">
+ <xs:minExclusive value="0"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <xs:simpleType name="PositiveLong">
+ <xs:restriction base="xs:long">
+ <xs:minExclusive value="0"/>
+ </xs:restriction>
+ </xs:simpleType>
+
+ <!-- Normalized control point unit float in [0,1] -->
+ <xs:simpleType name="NormalizedControlPointUnit">
+ <xs:restriction base="xs:float">
+ <xs:minInclusive value="0"/>
+ <xs:maxInclusive value="1"/>
+ </xs:restriction>
+ </xs:simpleType>
+
</xs:schema>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerStrategy.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerStrategy.kt
new file mode 100644
index 0000000..9e3edf6
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerStrategy.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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.compatui.letterbox
+
+import com.android.wm.shell.dagger.WMSingleton
+import javax.inject.Inject
+
+/**
+ * Encapsulate the logic related to the use of a single or multiple surfaces when
+ * implementing letterbox in shell.
+ */
+@WMSingleton
+class LetterboxControllerStrategy @Inject constructor() {
+
+ // Different letterbox implementation modes.
+ enum class LetterboxMode { SINGLE_SURFACE, MULTIPLE_SURFACES }
+
+ @Volatile
+ private var currentMode: LetterboxMode = LetterboxMode.SINGLE_SURFACE
+
+ fun configureLetterboxMode() {
+ // TODO(b/377875146): Define criteria for switching between [LetterboxMode]s.
+ currentMode = if (android.os.SystemProperties.getInt(
+ "multi_interface",
+ 0
+ ) == 0
+ ) {
+ LetterboxMode.SINGLE_SURFACE
+ } else {
+ LetterboxMode.MULTIPLE_SURFACES
+ }
+ }
+
+ /**
+ * @return The specific mode to use for implementing letterboxing for the given [request].
+ */
+ fun getLetterboxImplementationMode(): LetterboxMode = currentMode
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt
index a48dc8b..4718071 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt
@@ -35,7 +35,8 @@
shellInit: ShellInit,
private val transitions: Transitions,
private val letterboxController: LetterboxController,
- private val transitionStateHolder: TransitionStateHolder
+ private val transitionStateHolder: TransitionStateHolder,
+ private val letterboxModeStrategy: LetterboxControllerStrategy
) : Transitions.TransitionObserver {
companion object {
@@ -63,7 +64,6 @@
// We recognise the operation to execute and delegate to the LetterboxController
// the related operation.
// TODO(b/377875151): Identify Desktop Windowing Transactions.
- // TODO(b/377857898): Handling multiple surfaces
// TODO(b/371500295): Handle input events detection.
for (change in info.changes) {
change.taskInfo?.let { ti ->
@@ -83,6 +83,7 @@
} else {
val isTopActivityLetterboxed = ti.appCompatTaskInfo.isTopActivityLetterboxed
if (isTopActivityLetterboxed) {
+ letterboxModeStrategy.configureLetterboxMode()
createLetterboxSurface(
key,
startTransaction,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxUtils.kt
new file mode 100644
index 0000000..ef964f4
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxUtils.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2024 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.compatui.letterbox
+
+import android.graphics.Rect
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+
+/**
+ * Creates a [LetterboxController] which is the composition of other two [LetterboxController].
+ * It basically invokes the method on both of them.
+ */
+infix fun LetterboxController.append(other: LetterboxController) = object : LetterboxController {
+ override fun createLetterboxSurface(
+ key: LetterboxKey,
+ transaction: Transaction,
+ parentLeash: SurfaceControl
+ ) {
+ this@append.createLetterboxSurface(key, transaction, parentLeash)
+ other.createLetterboxSurface(key, transaction, parentLeash)
+ }
+
+ override fun destroyLetterboxSurface(
+ key: LetterboxKey,
+ transaction: Transaction
+ ) {
+ this@append.destroyLetterboxSurface(key, transaction)
+ other.destroyLetterboxSurface(key, transaction)
+ }
+
+ override fun updateLetterboxSurfaceVisibility(
+ key: LetterboxKey,
+ transaction: Transaction,
+ visible: Boolean
+ ) {
+ this@append.updateLetterboxSurfaceVisibility(key, transaction, visible)
+ other.updateLetterboxSurfaceVisibility(key, transaction, visible)
+ }
+
+ override fun updateLetterboxSurfaceBounds(
+ key: LetterboxKey,
+ transaction: Transaction,
+ taskBounds: Rect,
+ activityBounds: Rect
+ ) {
+ this@append.updateLetterboxSurfaceBounds(key, transaction, taskBounds, activityBounds)
+ other.updateLetterboxSurfaceBounds(key, transaction, taskBounds, activityBounds)
+ }
+
+ override fun dump() {
+ this@append.dump()
+ other.dump()
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxController.kt
new file mode 100644
index 0000000..8d06570
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxController.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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.compatui.letterbox
+
+import android.view.SurfaceControl
+import android.view.SurfaceControl.Transaction
+import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy.LetterboxMode.MULTIPLE_SURFACES
+import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy.LetterboxMode.SINGLE_SURFACE
+import com.android.wm.shell.dagger.WMSingleton
+import javax.inject.Inject
+
+/**
+ * [LetterboxController] implementation working as coordinator of other [LetterboxController]
+ * implementations.
+ */
+@WMSingleton
+class MixedLetterboxController @Inject constructor(
+ private val singleSurfaceController: SingleSurfaceLetterboxController,
+ private val multipleSurfaceController: MultiSurfaceLetterboxController,
+ private val controllerStrategy: LetterboxControllerStrategy
+) : LetterboxController by singleSurfaceController append multipleSurfaceController {
+
+ override fun createLetterboxSurface(
+ key: LetterboxKey,
+ transaction: Transaction,
+ parentLeash: SurfaceControl
+ ) {
+ when (controllerStrategy.getLetterboxImplementationMode()) {
+ SINGLE_SURFACE -> {
+ multipleSurfaceController.destroyLetterboxSurface(key, transaction)
+ singleSurfaceController.createLetterboxSurface(key, transaction, parentLeash)
+ }
+
+ MULTIPLE_SURFACES -> {
+ singleSurfaceController.destroyLetterboxSurface(key, transaction)
+ multipleSurfaceController.createLetterboxSurface(key, transaction, parentLeash)
+ }
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index df2b849..806be8c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -74,8 +74,9 @@
import com.android.wm.shell.common.transition.TransitionStateHolder;
import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler;
import com.android.wm.shell.compatui.letterbox.LetterboxController;
+import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy;
import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver;
-import com.android.wm.shell.compatui.letterbox.SingleSurfaceLetterboxController;
+import com.android.wm.shell.compatui.letterbox.MixedLetterboxController;
import com.android.wm.shell.dagger.back.ShellBackAnimationModule;
import com.android.wm.shell.dagger.pip.PipModule;
import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler;
@@ -1338,14 +1339,15 @@
@NonNull ShellInit shellInit,
@NonNull Transitions transitions,
@NonNull LetterboxController letterboxController,
- @NonNull TransitionStateHolder transitionStateHolder
+ @NonNull TransitionStateHolder transitionStateHolder,
+ @NonNull LetterboxControllerStrategy letterboxControllerStrategy
) {
return new LetterboxTransitionObserver(shellInit, transitions, letterboxController,
- transitionStateHolder);
+ transitionStateHolder, letterboxControllerStrategy);
}
@WMSingleton
@Binds
abstract LetterboxController bindsLetterboxController(
- SingleSurfaceLetterboxController letterboxController);
+ MixedLetterboxController letterboxController);
}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt
index f9f01bc..95a0c82 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt
@@ -98,6 +98,10 @@
activityBounds = activityBounds
)
+ fun invokeDump() {
+ letterboxController.dump()
+ }
+
fun checkSurfaceBuilderInvoked(times: Int = 1, name: String = "", callSite: String = "") {
verify(surfaceBuilder, times(times)).createSurface(
eq(transaction),
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt
index 8825bb4..78bb721 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt
@@ -96,6 +96,7 @@
validateOutput {
r.creationEventDetected(expected = false)
+ r.configureStrategyInvoked(expected = false)
r.visibilityEventDetected(expected = false)
r.destroyEventDetected(expected = false)
r.updateSurfaceBoundsEventDetected(expected = false)
@@ -123,6 +124,7 @@
validateOutput {
r.creationEventDetected(expected = true)
+ r.configureStrategyInvoked(expected = true)
r.visibilityEventDetected(expected = true, visible = true)
r.destroyEventDetected(expected = false)
r.updateSurfaceBoundsEventDetected(
@@ -217,6 +219,7 @@
private val letterboxController: LetterboxController
private val letterboxObserver: LetterboxTransitionObserver
private val transitionStateHolder: TransitionStateHolder
+ private val letterboxStrategy: LetterboxControllerStrategy
val observerFactory: () -> LetterboxTransitionObserver
@@ -225,6 +228,7 @@
shellInit = ShellInit(executor)
transitions = mock<Transitions>()
letterboxController = mock<LetterboxController>()
+ letterboxStrategy = mock<LetterboxControllerStrategy>()
transitionStateHolder =
TransitionStateHolder(shellInit, mock<RecentsTransitionHandler>())
spyOn(transitionStateHolder)
@@ -233,7 +237,8 @@
shellInit,
transitions,
letterboxController,
- transitionStateHolder
+ transitionStateHolder,
+ letterboxStrategy
)
observerFactory = { letterboxObserver }
}
@@ -302,6 +307,9 @@
eq(activityBounds)
)
+ fun configureStrategyInvoked(expected: Boolean) =
+ verify(letterboxStrategy, expected.asMode()).configureLetterboxMode()
+
fun createTopActivityChange(
inputBuilder: TransitionObserverInputBuilder,
isLetterboxed: Boolean = true,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxUtilsTest.kt
new file mode 100644
index 0000000..06b8052
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxUtilsTest.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2024 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.compatui.letterbox
+
+import android.content.Context
+import android.graphics.Rect
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import java.util.function.Consumer
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [LetterboxUtils].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:LetterboxUtilsTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class LetterboxUtilsTest : ShellTestCase() {
+
+ val firstLetterboxController = mock<LetterboxController>()
+ val secondLetterboxController = mock<LetterboxController>()
+ val thirdLetterboxController = mock<LetterboxController>()
+
+ private val letterboxControllerBuilder: (LetterboxSurfaceBuilder) -> LetterboxController =
+ { _ ->
+ firstLetterboxController.append(secondLetterboxController)
+ .append(thirdLetterboxController)
+ }
+
+ @Test
+ fun `Appended LetterboxController invoked creation on all the controllers`() {
+ runTestScenario { r ->
+ r.sendCreateSurfaceRequest()
+
+ r.verifyCreateSurfaceInvokedWithRequest(target = firstLetterboxController)
+ r.verifyCreateSurfaceInvokedWithRequest(target = secondLetterboxController)
+ r.verifyCreateSurfaceInvokedWithRequest(target = thirdLetterboxController)
+ }
+ }
+
+ @Test
+ fun `Appended LetterboxController invoked destroy on all the controllers`() {
+ runTestScenario { r ->
+ r.sendDestroySurfaceRequest()
+ r.verifyDestroySurfaceInvokedWithRequest(target = firstLetterboxController)
+ r.verifyDestroySurfaceInvokedWithRequest(target = secondLetterboxController)
+ r.verifyDestroySurfaceInvokedWithRequest(target = thirdLetterboxController)
+ }
+ }
+
+ @Test
+ fun `Appended LetterboxController invoked update visibility on all the controllers`() {
+ runTestScenario { r ->
+ r.sendUpdateSurfaceVisibilityRequest(visible = true)
+ r.verifyUpdateVisibilitySurfaceInvokedWithRequest(target = firstLetterboxController)
+ r.verifyUpdateVisibilitySurfaceInvokedWithRequest(target = secondLetterboxController)
+ r.verifyUpdateVisibilitySurfaceInvokedWithRequest(target = thirdLetterboxController)
+ }
+ }
+
+ @Test
+ fun `Appended LetterboxController invoked update bounds on all the controllers`() {
+ runTestScenario { r ->
+ r.sendUpdateSurfaceBoundsRequest(taskBounds = Rect(), activityBounds = Rect())
+ r.verifyUpdateSurfaceBoundsInvokedWithRequest(target = firstLetterboxController)
+ r.verifyUpdateSurfaceBoundsInvokedWithRequest(target = secondLetterboxController)
+ r.verifyUpdateSurfaceBoundsInvokedWithRequest(target = thirdLetterboxController)
+ }
+ }
+
+ @Test
+ fun `Appended LetterboxController invoked update dump on all the controllers`() {
+ runTestScenario { r ->
+ r.invokeDump()
+ r.verifyDumpInvoked(target = firstLetterboxController)
+ r.verifyDumpInvoked(target = secondLetterboxController)
+ r.verifyDumpInvoked(target = thirdLetterboxController)
+ }
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ fun runTestScenario(consumer: Consumer<AppendLetterboxControllerRobotTest>) {
+ val robot = AppendLetterboxControllerRobotTest(mContext, letterboxControllerBuilder)
+ consumer.accept(robot)
+ }
+
+ class AppendLetterboxControllerRobotTest(
+ ctx: Context,
+ builder: (LetterboxSurfaceBuilder) -> LetterboxController
+ ) : LetterboxControllerRobotTest(ctx, builder) {
+
+ fun verifyCreateSurfaceInvokedWithRequest(
+ target: LetterboxController,
+ times: Int = 1
+ ) {
+ verify(target, times(times)).createLetterboxSurface(any(), any(), any())
+ }
+
+ fun verifyDestroySurfaceInvokedWithRequest(
+ target: LetterboxController,
+ times: Int = 1
+ ) {
+ verify(target, times(times)).destroyLetterboxSurface(any(), any())
+ }
+
+ fun verifyUpdateVisibilitySurfaceInvokedWithRequest(
+ target: LetterboxController,
+ times: Int = 1
+ ) {
+ verify(target, times(times)).updateLetterboxSurfaceVisibility(any(), any(), any())
+ }
+
+ fun verifyUpdateSurfaceBoundsInvokedWithRequest(
+ target: LetterboxController,
+ times: Int = 1
+ ) {
+ verify(target, times(times)).updateLetterboxSurfaceBounds(any(), any(), any(), any())
+ }
+
+ fun verifyDumpInvoked(
+ target: LetterboxController,
+ times: Int = 1
+ ) {
+ verify(target, times(times)).dump()
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxControllerTest.kt
new file mode 100644
index 0000000..e6bff4c
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/MixedLetterboxControllerTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright 2024 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.compatui.letterbox
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import androidx.test.filters.SmallTest
+import com.android.wm.shell.ShellTestCase
+import com.android.wm.shell.compatui.letterbox.LetterboxControllerStrategy.LetterboxMode
+import java.util.function.Consumer
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+/**
+ * Tests for [MixedLetterboxController].
+ *
+ * Build/Install/Run:
+ * atest WMShellUnitTests:MixedLetterboxControllerTest
+ */
+@RunWith(AndroidTestingRunner::class)
+@SmallTest
+class MixedLetterboxControllerTest : ShellTestCase() {
+
+ @Test
+ fun `When strategy is SINGLE_SURFACE and a create request is sent multi are destroyed`() {
+ runTestScenario { r ->
+ r.configureStrategyFor(LetterboxMode.SINGLE_SURFACE)
+ r.sendCreateSurfaceRequest()
+ r.checkCreateInvokedOnSingleController()
+ r.checkDestroyInvokedOnMultiController()
+ }
+ }
+
+ @Test
+ fun `When strategy is MULTIPLE_SURFACES and a create request is sent single is destroyed`() {
+ runTestScenario { r ->
+ r.configureStrategyFor(LetterboxMode.MULTIPLE_SURFACES)
+ r.sendCreateSurfaceRequest()
+ r.checkDestroyInvokedOnSingleController()
+ r.checkCreateInvokedOnMultiController()
+ }
+ }
+
+ /**
+ * Runs a test scenario providing a Robot.
+ */
+ fun runTestScenario(consumer: Consumer<MixedLetterboxControllerRobotTest>) {
+ val robot = MixedLetterboxControllerRobotTest(mContext, ObjectToTestHolder())
+ consumer.accept(robot)
+ }
+
+ class MixedLetterboxControllerRobotTest(
+ ctx: Context,
+ private val objectToTestHolder: ObjectToTestHolder
+ ) : LetterboxControllerRobotTest(ctx, objectToTestHolder.controllerBuilder) {
+
+ fun configureStrategyFor(letterboxMode: LetterboxMode) {
+ doReturn(letterboxMode).`when`(objectToTestHolder.controllerStrategy)
+ .getLetterboxImplementationMode()
+ }
+
+ fun checkCreateInvokedOnSingleController(times: Int = 1) {
+ verify(
+ objectToTestHolder.singleLetterboxController,
+ times(times)
+ ).createLetterboxSurface(any(), any(), any())
+ }
+
+ fun checkCreateInvokedOnMultiController(times: Int = 1) {
+ verify(
+ objectToTestHolder.multipleLetterboxController,
+ times(times)
+ ).createLetterboxSurface(any(), any(), any())
+ }
+
+ fun checkDestroyInvokedOnSingleController(times: Int = 1) {
+ verify(
+ objectToTestHolder.singleLetterboxController,
+ times(times)
+ ).destroyLetterboxSurface(any(), any())
+ }
+
+ fun checkDestroyInvokedOnMultiController(times: Int = 1) {
+ verify(
+ objectToTestHolder.multipleLetterboxController,
+ times(times)
+ ).destroyLetterboxSurface(any(), any())
+ }
+ }
+
+ data class ObjectToTestHolder(
+ val singleLetterboxController: SingleSurfaceLetterboxController =
+ mock<SingleSurfaceLetterboxController>(),
+ val multipleLetterboxController: MultiSurfaceLetterboxController =
+ mock<MultiSurfaceLetterboxController>(),
+ val controllerStrategy: LetterboxControllerStrategy = mock<LetterboxControllerStrategy>()
+ ) {
+
+ private val mixedController =
+ MixedLetterboxController(
+ singleLetterboxController,
+ multipleLetterboxController,
+ controllerStrategy
+ )
+
+ val controllerBuilder: (LetterboxSurfaceBuilder) -> LetterboxController =
+ { _ -> mixedController }
+ }
+}
diff --git a/libs/input/MouseCursorController.cpp b/libs/input/MouseCursorController.cpp
index d993b87..28d96e3 100644
--- a/libs/input/MouseCursorController.cpp
+++ b/libs/input/MouseCursorController.cpp
@@ -28,12 +28,14 @@
#define INDENT " "
#define INDENT2 " "
+namespace android {
+
namespace {
+
// Time to spend fading out the pointer completely.
const nsecs_t POINTER_FADE_DURATION = 500 * 1000000LL; // 500 ms
-} // namespace
-namespace android {
+} // namespace
// --- MouseCursorController ---
@@ -64,17 +66,23 @@
mLocked.pointerSprite.clear();
}
-void MouseCursorController::move(float deltaX, float deltaY) {
+FloatPoint MouseCursorController::move(float deltaX, float deltaY) {
#if DEBUG_MOUSE_CURSOR_UPDATES
ALOGD("Move pointer by deltaX=%0.3f, deltaY=%0.3f", deltaX, deltaY);
#endif
if (deltaX == 0.0f && deltaY == 0.0f) {
- return;
+ return {0, 0};
}
+ // When transition occurs, the MouseCursorController object may or may not be deleted, depending
+ // if there's another display on the other side of the transition. At this point we still need
+ // to move the cursor to the boundary.
std::scoped_lock lock(mLock);
-
- setPositionLocked(mLocked.pointerX + deltaX, mLocked.pointerY + deltaY);
+ const FloatPoint position{mLocked.pointerX + deltaX, mLocked.pointerY + deltaY};
+ setPositionLocked(position.x, position.y);
+ // The amount of the delta that was not consumed as a result of the cursor
+ // hitting the edge of the display.
+ return {position.x - mLocked.pointerX, position.y - mLocked.pointerY};
}
void MouseCursorController::setPosition(float x, float y) {
@@ -85,19 +93,23 @@
setPositionLocked(x, y);
}
-void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) {
- const auto& v = mLocked.viewport;
- if (!v.isValid()) return;
-
+FloatRect MouseCursorController::getBoundsLocked() REQUIRES(mLock) {
// The valid bounds for a mouse cursor. Since the right and bottom edges are considered outside
// the display, clip the bounds by one pixel instead of letting the cursor get arbitrarily
// close to the outside edge.
- const FloatRect bounds{
+ return FloatRect{
static_cast<float>(mLocked.viewport.logicalLeft),
static_cast<float>(mLocked.viewport.logicalTop),
static_cast<float>(mLocked.viewport.logicalRight - 1),
static_cast<float>(mLocked.viewport.logicalBottom - 1),
};
+}
+
+void MouseCursorController::setPositionLocked(float x, float y) REQUIRES(mLock) {
+ const auto& v = mLocked.viewport;
+ if (!v.isValid()) return;
+
+ const FloatRect bounds = getBoundsLocked();
mLocked.pointerX = std::max(bounds.left, std::min(bounds.right, x));
mLocked.pointerY = std::max(bounds.top, std::min(bounds.bottom, y));
diff --git a/libs/input/MouseCursorController.h b/libs/input/MouseCursorController.h
index 12b31a8..e14a55d 100644
--- a/libs/input/MouseCursorController.h
+++ b/libs/input/MouseCursorController.h
@@ -20,9 +20,6 @@
#include <gui/DisplayEventReceiver.h>
#include <input/DisplayViewport.h>
#include <input/Input.h>
-#include <utils/BitSet.h>
-#include <utils/Looper.h>
-#include <utils/RefBase.h>
#include <functional>
#include <map>
@@ -43,7 +40,8 @@
MouseCursorController(PointerControllerContext& context);
~MouseCursorController();
- void move(float deltaX, float deltaY);
+ // Move the pointer and return unconsumed delta
+ FloatPoint move(float deltaX, float deltaY);
void setPosition(float x, float y);
FloatPoint getPosition() const;
ui::LogicalDisplayId getDisplayId() const;
@@ -113,6 +111,7 @@
bool doFadingAnimationLocked(nsecs_t timestamp);
void startAnimationLocked();
+ FloatRect getBoundsLocked();
};
} // namespace android
diff --git a/libs/input/PointerController.cpp b/libs/input/PointerController.cpp
index 78d7d3a..a713f1d 100644
--- a/libs/input/PointerController.cpp
+++ b/libs/input/PointerController.cpp
@@ -138,15 +138,19 @@
return mDisplayInfoListener->mLock;
}
-void PointerController::move(float deltaX, float deltaY) {
+FloatPoint PointerController::move(float deltaX, float deltaY) {
const ui::LogicalDisplayId displayId = mCursorController.getDisplayId();
- vec2 transformed;
+ ui::Transform transform;
{
std::scoped_lock lock(getLock());
- const auto& transform = getTransformForDisplayLocked(displayId);
- transformed = transformWithoutTranslation(transform, {deltaX, deltaY});
+ transform = getTransformForDisplayLocked(displayId);
}
- mCursorController.move(transformed.x, transformed.y);
+
+ const vec2 transformed = transformWithoutTranslation(transform, {deltaX, deltaY});
+
+ const FloatPoint unconsumedDelta = mCursorController.move(transformed.x, transformed.y);
+ return FloatPoint(transformWithoutTranslation(transform.inverse(),
+ {unconsumedDelta.x, unconsumedDelta.y}));
}
void PointerController::setPosition(float x, float y) {
@@ -295,6 +299,11 @@
mCursorController.setSkipScreenshot(false);
}
+ui::Transform PointerController::getDisplayTransform() const {
+ std::scoped_lock lock(getLock());
+ return getTransformForDisplayLocked(mLocked.pointerDisplayId);
+}
+
void PointerController::doInactivityTimeout() {
fade(Transition::GRADUAL);
}
diff --git a/libs/input/PointerController.h b/libs/input/PointerController.h
index ee8d121..8b33190 100644
--- a/libs/input/PointerController.h
+++ b/libs/input/PointerController.h
@@ -51,7 +51,7 @@
~PointerController() override;
- void move(float deltaX, float deltaY) override;
+ FloatPoint move(float deltaX, float deltaY) override;
void setPosition(float x, float y) override;
FloatPoint getPosition() const override;
ui::LogicalDisplayId getDisplayId() const override;
@@ -67,6 +67,7 @@
void setCustomPointerIcon(const SpriteIcon& icon) override;
void setSkipScreenshotFlagForDisplay(ui::LogicalDisplayId displayId) override;
void clearSkipScreenshotFlags() override;
+ ui::Transform getDisplayTransform() const override;
virtual void setInactivityTimeout(InactivityTimeout inactivityTimeout);
void doInactivityTimeout();
@@ -165,7 +166,7 @@
~TouchPointerController() override;
- void move(float, float) override {
+ FloatPoint move(float, float) override {
LOG_ALWAYS_FATAL("Should not be called");
}
void setPosition(float, float) override {
diff --git a/libs/input/tests/PointerController_test.cpp b/libs/input/tests/PointerController_test.cpp
index 5b00fca..80c934a 100644
--- a/libs/input/tests/PointerController_test.cpp
+++ b/libs/input/tests/PointerController_test.cpp
@@ -40,6 +40,8 @@
CURSOR_TYPE_CUSTOM = -1,
};
+static constexpr float EPSILON = MotionEvent::ROUNDING_PRECISION;
+
using ::testing::AllOf;
using ::testing::Field;
using ::testing::NiceMock;
@@ -399,6 +401,135 @@
testing::Values(PointerControllerInterface::ControllerType::MOUSE,
PointerControllerInterface::ControllerType::STYLUS));
+class MousePointerControllerTest : public PointerControllerTest {
+protected:
+ MousePointerControllerTest() {
+ sp<MockSprite> testPointerSprite(new NiceMock<MockSprite>);
+ EXPECT_CALL(*mSpriteController, createSprite).WillOnce(Return(testPointerSprite));
+
+ // create a mouse pointer controller
+ mPointerController =
+ PointerController::create(mPolicy, mLooper, *mSpriteController,
+ PointerControllerInterface::ControllerType::MOUSE);
+
+ // set display viewport
+ DisplayViewport viewport;
+ viewport.displayId = ui::LogicalDisplayId::DEFAULT;
+ viewport.logicalRight = 5;
+ viewport.logicalBottom = 5;
+ viewport.physicalRight = 5;
+ viewport.physicalBottom = 5;
+ viewport.deviceWidth = 5;
+ viewport.deviceHeight = 5;
+ mPointerController->setDisplayViewport(viewport);
+ }
+};
+
+struct MousePointerControllerTestParam {
+ vec2 startPosition;
+ vec2 delta;
+ vec2 endPosition;
+ vec2 unconsumedDelta;
+};
+
+class PointerControllerViewportTransitionTest
+ : public MousePointerControllerTest,
+ public testing::WithParamInterface<MousePointerControllerTestParam> {};
+
+TEST_P(PointerControllerViewportTransitionTest, testPointerViewportTransition) {
+ const auto& params = GetParam();
+
+ mPointerController->setPosition(params.startPosition.x, params.startPosition.y);
+ auto unconsumedDelta = mPointerController->move(params.delta.x, params.delta.y);
+
+ auto position = mPointerController->getPosition();
+ EXPECT_NEAR(position.x, params.endPosition.x, EPSILON);
+ EXPECT_NEAR(position.y, params.endPosition.y, EPSILON);
+ EXPECT_NEAR(unconsumedDelta.x, params.unconsumedDelta.x, EPSILON);
+ EXPECT_NEAR(unconsumedDelta.y, params.unconsumedDelta.y, EPSILON);
+}
+
+INSTANTIATE_TEST_SUITE_P(PointerControllerViewportTransitionTest,
+ PointerControllerViewportTransitionTest,
+ testing::Values(
+ // no transition
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {2.0f, 2.0f},
+ {4.0f, 4.0f},
+ {0.0f, 0.0f}},
+ // right boundary
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {3.0f, 0.0f},
+ {4.0f, 2.0f},
+ {1.0f, 0.0f}},
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {3.0f, -1.0f},
+ {4.0f, 1.0f},
+ {1.0f, 0.0f}},
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {3.0f, 1.0f},
+ {4.0f, 3.0f},
+ {1.0f, 0.0f}},
+ // left boundary
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {-3.0f, 0.0f},
+ {0.0f, 2.0f},
+ {-1.0f, 0.0f}},
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {-3.0f, -1.0f},
+ {0.0f, 1.0f},
+ {-1.0f, 0.0f}},
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {-3.0f, 1.0f},
+ {0.0f, 3.0f},
+ {-1.0f, 0.0f}},
+ // bottom boundary
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {0.0f, 3.0f},
+ {2.0f, 4.0f},
+ {0.0f, 1.0f}},
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {-1.0f, 3.0f},
+ {1.0f, 4.0f},
+ {0.0f, 1.0f}},
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {1.0f, 3.0f},
+ {3.0f, 4.0f},
+ {0.0f, 1.0f}},
+ // top boundary
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {0.0f, -3.0f},
+ {2.0f, 0.0f},
+ {0.0f, -1.0f}},
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {-1.0f, -3.0f},
+ {1.0f, 0.0f},
+ {0.0f, -1.0f}},
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {1.0f, -3.0f},
+ {3.0f, 0.0f},
+ {0.0f, -1.0f}},
+ // top-left corner
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {-3.0f, -3.0f},
+ {0.0f, 0.0f},
+ {-1.0f, -1.0f}},
+ // top-right corner
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {3.0f, -3.0f},
+ {4.0f, 0.0f},
+ {1.0f, -1.0f}},
+ // bottom-right corner
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {3.0f, 3.0f},
+ {4.0f, 4.0f},
+ {1.0f, 1.0f}},
+ // bottom-left corner
+ MousePointerControllerTestParam{{2.0f, 2.0f},
+ {-3.0f, 3.0f},
+ {0.0f, 4.0f},
+ {-1.0f, 1.0f}}));
+
class PointerControllerWindowInfoListenerTest : public Test {};
TEST_F(PointerControllerWindowInfoListenerTest,
diff --git a/media/java/android/media/projection/MediaProjection.java b/media/java/android/media/projection/MediaProjection.java
index f7f10df..4f7132a 100644
--- a/media/java/android/media/projection/MediaProjection.java
+++ b/media/java/android/media/projection/MediaProjection.java
@@ -324,6 +324,19 @@
}
/**
+ * Stops projection.
+ * @hide
+ */
+ public void stop(@StopReason int stopReason) {
+ try {
+ Log.d(TAG, "Content Recording: stopping projection");
+ mImpl.stop(stopReason);
+ } catch (RemoteException e) {
+ Log.e(TAG, "Unable to stop projection", e);
+ }
+ }
+
+ /**
* Get the underlying IMediaProjection.
* @hide
*/
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
index 77be8c7..6ec38ba 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepositoryTest.kt
@@ -16,8 +16,9 @@
package com.android.systemui.mediarouter.data.repository
-import androidx.test.filters.SmallTest
+import android.media.projection.StopReason
import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
@@ -101,7 +102,7 @@
origin = CastDevice.CastOrigin.MediaRouter,
)
- underTest.stopCasting(device)
+ underTest.stopCasting(device, StopReason.STOP_UNKNOWN)
assertThat(castController.lastStoppedDevice).isEqualTo(device)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
index 9f12b18..31a627f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/CastTileTest.java
@@ -20,6 +20,7 @@
import static junit.framework.TestCase.assertEquals;
import static org.junit.Assert.assertFalse;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.same;
import static org.mockito.Mockito.any;
import static org.mockito.Mockito.mock;
@@ -30,6 +31,7 @@
import android.media.MediaRouter;
import android.media.MediaRouter.RouteInfo;
import android.media.projection.MediaProjectionInfo;
+import android.media.projection.StopReason;
import android.os.Handler;
import android.service.quicksettings.Tile;
import android.testing.TestableLooper;
@@ -336,7 +338,8 @@
mCastTile.handleClick(null /* view */);
mTestableLooper.processAllMessages();
- verify(mController, times(1)).stopCasting(same(device));
+ verify(mController, times(1))
+ .stopCasting(same(device), eq(StopReason.STOP_QS_TILE));
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
index 53708fd..6ebe830 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/ScreenRecordTileTest.java
@@ -23,11 +23,13 @@
import static org.junit.Assert.assertFalse;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.app.Dialog;
+import android.media.projection.StopReason;
import android.os.Handler;
import android.service.quicksettings.Tile;
import android.testing.TestableLooper;
@@ -214,7 +216,7 @@
mTile.handleClick(null /* view */);
- verify(mController, times(1)).stopRecording();
+ verify(mController, times(1)).stopRecording(eq(StopReason.STOP_QS_TILE));
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
index 0b56d7b..778c73f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractorTest.kt
@@ -17,6 +17,7 @@
package com.android.systemui.qs.tiles.impl.screenrecord.domain.interactor
import android.app.Dialog
+import android.media.projection.StopReason
import android.os.UserHandle
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -92,7 +93,7 @@
underTest.handleInput(QSTileInputTestKtx.click(recordingModel))
- verify(recordingController).stopRecording()
+ verify(recordingController).stopRecording(eq(StopReason.STOP_QS_TILE))
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index a6a1d4a..50fa9d2 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -41,6 +41,7 @@
import android.app.Notification;
import android.app.NotificationManager;
import android.content.Intent;
+import android.media.projection.StopReason;
import android.os.Handler;
import android.os.RemoteException;
import android.os.UserHandle;
@@ -199,16 +200,16 @@
public void testOnSystemRequestedStop_recordingInProgress_endsRecording() throws IOException {
doReturn(true).when(mController).isRecording();
- mRecordingService.onStopped();
+ mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
- verify(mScreenMediaRecorder).end();
+ verify(mScreenMediaRecorder).end(eq(StopReason.STOP_UNKNOWN));
}
@Test
public void testOnSystemRequestedStop_recordingInProgress_updatesState() {
doReturn(true).when(mController).isRecording();
- mRecordingService.onStopped();
+ mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
assertUpdateState(false);
}
@@ -218,18 +219,18 @@
throws IOException {
doReturn(false).when(mController).isRecording();
- mRecordingService.onStopped();
+ mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
- verify(mScreenMediaRecorder, never()).end();
+ verify(mScreenMediaRecorder, never()).end(StopReason.STOP_UNKNOWN);
}
@Test
public void testOnSystemRequestedStop_recorderEndThrowsRuntimeException_releasesRecording()
throws IOException {
doReturn(true).when(mController).isRecording();
- doThrow(new RuntimeException()).when(mScreenMediaRecorder).end();
+ doThrow(new RuntimeException()).when(mScreenMediaRecorder).end(StopReason.STOP_UNKNOWN);
- mRecordingService.onStopped();
+ mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
verify(mScreenMediaRecorder).release();
}
@@ -238,7 +239,7 @@
public void testOnSystemRequestedStop_whenRecordingInProgress_showsNotifications() {
doReturn(true).when(mController).isRecording();
- mRecordingService.onStopped();
+ mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
// Processing notification
ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class);
@@ -271,9 +272,9 @@
public void testOnSystemRequestedStop_recorderEndThrowsRuntimeException_showsErrorNotification()
throws IOException {
doReturn(true).when(mController).isRecording();
- doThrow(new RuntimeException()).when(mScreenMediaRecorder).end();
+ doThrow(new RuntimeException()).when(mScreenMediaRecorder).end(anyInt());
- mRecordingService.onStopped();
+ mRecordingService.onStopped(StopReason.STOP_UNKNOWN);
verify(mRecordingService).createErrorSavingNotification(any());
ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class);
@@ -289,9 +290,9 @@
public void testOnSystemRequestedStop_recorderEndThrowsOOMError_releasesRecording()
throws IOException {
doReturn(true).when(mController).isRecording();
- doThrow(new OutOfMemoryError()).when(mScreenMediaRecorder).end();
+ doThrow(new OutOfMemoryError()).when(mScreenMediaRecorder).end(anyInt());
- assertThrows(Throwable.class, () -> mRecordingService.onStopped());
+ assertThrows(Throwable.class, () -> mRecordingService.onStopped(StopReason.STOP_UNKNOWN));
verify(mScreenMediaRecorder).release();
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
index aceea90..ade5941 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepositoryTest.kt
@@ -16,6 +16,7 @@
package com.android.systemui.screenrecord.data.repository
+import android.media.projection.StopReason
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
@@ -31,6 +32,7 @@
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever
@@ -126,8 +128,8 @@
@Test
fun stopRecording_invokesController() =
testScope.runTest {
- underTest.stopRecording()
+ underTest.stopRecording(StopReason.STOP_PRIVACY_CHIP)
- verify(recordingController).stopRecording()
+ verify(recordingController).stopRecording(eq(StopReason.STOP_PRIVACY_CHIP))
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt
new file mode 100644
index 0000000..44a8518
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplOldTest.kt
@@ -0,0 +1,684 @@
+/*
+ * Copyright (C) 2019 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.systemui.statusbar.notification.headsup
+
+import android.app.Notification
+import android.app.PendingIntent
+import android.app.Person
+import android.os.Handler
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.FlagsParameterization
+import android.testing.TestableLooper.RunWithLooper
+import androidx.test.filters.SmallTest
+import com.android.internal.logging.testing.UiEventLoggerFake
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.kosmos.KosmosJavaAdapter
+import com.android.systemui.log.logcatLogBuffer
+import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.statusbar.notification.collection.NotificationEntry
+import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder
+import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManagerImpl.HeadsUpEntry
+import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow
+import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun
+import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
+import com.android.systemui.util.concurrency.FakeExecutor
+import com.android.systemui.util.kotlin.JavaAdapter
+import com.android.systemui.util.settings.FakeGlobalSettings
+import com.android.systemui.util.time.FakeSystemClock
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.flow.MutableStateFlow
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.eq
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
+
+@SmallTest
+@RunWithLooper
+@RunWith(ParameterizedAndroidJunit4::class)
+// TODO(b/378142453): Merge this with HeadsUpManagerImplTest.
+open class HeadsUpManagerImplOldTest(flags: FlagsParameterization?) : SysuiTestCase() {
+ protected var mKosmos: KosmosJavaAdapter = KosmosJavaAdapter(this)
+
+ @JvmField @Rule var rule: MockitoRule = MockitoJUnit.rule()
+
+ private val mUiEventLoggerFake = UiEventLoggerFake()
+
+ private val mLogger: HeadsUpManagerLogger = Mockito.spy(HeadsUpManagerLogger(logcatLogBuffer()))
+
+ @Mock private val mBgHandler: Handler? = null
+
+ @Mock private val dumpManager: DumpManager? = null
+
+ @Mock private val mShadeInteractor: ShadeInteractor? = null
+ private var mAvalancheController: AvalancheController? = null
+
+ @Mock private val mAccessibilityMgr: AccessibilityManagerWrapper? = null
+
+ protected val mGlobalSettings: FakeGlobalSettings = FakeGlobalSettings()
+ protected val mSystemClock: FakeSystemClock = FakeSystemClock()
+ protected val mExecutor: FakeExecutor = FakeExecutor(mSystemClock)
+
+ @Mock protected var mRow: ExpandableNotificationRow? = null
+
+ private fun createHeadsUpManager(): HeadsUpManagerImpl {
+ return TestableHeadsUpManager(
+ mContext,
+ mLogger,
+ mKosmos.statusBarStateController,
+ mKosmos.keyguardBypassController,
+ GroupMembershipManagerImpl(),
+ mKosmos.visualStabilityProvider,
+ mKosmos.configurationController,
+ mExecutor,
+ mGlobalSettings,
+ mSystemClock,
+ mAccessibilityMgr,
+ mUiEventLoggerFake,
+ JavaAdapter(mKosmos.testScope),
+ mShadeInteractor,
+ mAvalancheController,
+ )
+ }
+
+ private fun createStickyEntry(id: Int): NotificationEntry {
+ val notif =
+ Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .setFullScreenIntent(
+ Mockito.mock(PendingIntent::class.java), /* highPriority */
+ true,
+ )
+ .build()
+ return HeadsUpManagerTestUtil.createEntry(id, notif)
+ }
+
+ private fun createStickyForSomeTimeEntry(id: Int): NotificationEntry {
+ val notif =
+ Notification.Builder(mContext, "")
+ .setSmallIcon(R.drawable.ic_person)
+ .setFlag(Notification.FLAG_FSI_REQUESTED_BUT_DENIED, true)
+ .build()
+ return HeadsUpManagerTestUtil.createEntry(id, notif)
+ }
+
+ private fun useAccessibilityTimeout(use: Boolean) {
+ if (use) {
+ Mockito.doReturn(TEST_A11Y_AUTO_DISMISS_TIME)
+ .`when`(mAccessibilityMgr!!)
+ .getRecommendedTimeoutMillis(ArgumentMatchers.anyInt(), ArgumentMatchers.anyInt())
+ } else {
+ Mockito.`when`(
+ mAccessibilityMgr!!.getRecommendedTimeoutMillis(
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.anyInt(),
+ )
+ )
+ .then { i: InvocationOnMock -> i.getArgument(0) }
+ }
+ }
+
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags!!)
+ }
+
+ @Throws(Exception::class)
+ override fun SysuiSetup() {
+ super.SysuiSetup()
+ mAvalancheController =
+ AvalancheController(dumpManager!!, mUiEventLoggerFake, mLogger, mBgHandler!!)
+ Mockito.`when`(mShadeInteractor!!.isAnyExpanded).thenReturn(MutableStateFlow(true))
+ Mockito.`when`(mKosmos.keyguardBypassController.bypassEnabled).thenReturn(false)
+ }
+
+ @Test
+ fun testHasNotifications_headsUpManagerMapNotEmpty_true() {
+ val bhum = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ bhum.showNotification(entry)
+
+ Truth.assertThat(bhum.mHeadsUpEntryMap).isNotEmpty()
+ Truth.assertThat(bhum.hasNotifications()).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testHasNotifications_avalancheMapNotEmpty_true() {
+ val bhum = createHeadsUpManager()
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
+ mAvalancheController!!.addToNext(headsUpEntry) {}
+
+ Truth.assertThat(mAvalancheController!!.getWaitingEntryList()).isNotEmpty()
+ Truth.assertThat(bhum.hasNotifications()).isTrue()
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testHasNotifications_false() {
+ val bhum = createHeadsUpManager()
+ Truth.assertThat(bhum.mHeadsUpEntryMap).isEmpty()
+ Truth.assertThat(mAvalancheController!!.getWaitingEntryList()).isEmpty()
+ Truth.assertThat(bhum.hasNotifications()).isFalse()
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testGetHeadsUpEntryList_includesAvalancheEntryList() {
+ val bhum = createHeadsUpManager()
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
+ mAvalancheController!!.addToNext(headsUpEntry) {}
+
+ Truth.assertThat(bhum.headsUpEntryList).contains(headsUpEntry)
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testGetHeadsUpEntry_returnsAvalancheEntry() {
+ val bhum = createHeadsUpManager()
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val headsUpEntry = bhum.createHeadsUpEntry(notifEntry)
+ mAvalancheController!!.addToNext(headsUpEntry) {}
+
+ Truth.assertThat(bhum.getHeadsUpEntry(notifEntry.key)).isEqualTo(headsUpEntry)
+ }
+
+ @Test
+ fun testShowNotification_addsEntry() {
+ val alm = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ alm.showNotification(entry)
+
+ assertThat(alm.isHeadsUpEntry(entry.key)).isTrue()
+ assertThat(alm.hasNotifications()).isTrue()
+ assertThat(alm.getEntry(entry.key)).isEqualTo(entry)
+ }
+
+ @Test
+ fun testShowNotification_autoDismisses() {
+ val alm = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ alm.showNotification(entry)
+ mSystemClock.advanceTime((TEST_AUTO_DISMISS_TIME * 3 / 2).toLong())
+
+ assertThat(alm.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testRemoveNotification_removeDeferred() {
+ val alm = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ alm.showNotification(entry)
+
+ val removedImmediately =
+ alm.removeNotification(entry.key, /* releaseImmediately= */ false, "removeDeferred")
+ assertThat(removedImmediately).isFalse()
+ assertThat(alm.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testRemoveNotification_forceRemove() {
+ val alm = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ alm.showNotification(entry)
+
+ val removedImmediately =
+ alm.removeNotification(entry.key, /* releaseImmediately= */ true, "forceRemove")
+ assertThat(removedImmediately).isTrue()
+ assertThat(alm.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testReleaseAllImmediately() {
+ val alm = createHeadsUpManager()
+ for (i in 0 until TEST_NUM_NOTIFICATIONS) {
+ val entry = HeadsUpManagerTestUtil.createEntry(i, mContext)
+ entry.row = mRow
+ alm.showNotification(entry)
+ }
+
+ alm.releaseAllImmediately()
+
+ assertThat(alm.allEntries.count()).isEqualTo(0)
+ }
+
+ @Test
+ fun testCanRemoveImmediately_notShownLongEnough() {
+ val alm = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ alm.showNotification(entry)
+
+ // The entry has just been added so we should not remove immediately.
+ assertThat(alm.canRemoveImmediately(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testHunRemovedLogging() {
+ val hum = createHeadsUpManager()
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ val headsUpEntry = Mockito.mock(HeadsUpEntry::class.java)
+ Mockito.`when`(headsUpEntry.pinnedStatus)
+ .thenReturn(MutableStateFlow(PinnedStatus.NotPinned))
+ headsUpEntry.mEntry = notifEntry
+
+ hum.onEntryRemoved(headsUpEntry, "test")
+
+ Mockito.verify(mLogger, Mockito.times(1)).logNotificationActuallyRemoved(eq(notifEntry))
+ }
+
+ @Test
+ fun testShowNotification_autoDismissesIncludingTouchAcceptanceDelay() {
+ val hum = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(false)
+
+ hum.showNotification(entry)
+ mSystemClock.advanceTime((TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME).toLong())
+
+ assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testShowNotification_autoDismissesWithDefaultTimeout() {
+ val hum = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(false)
+
+ hum.showNotification(entry)
+ mSystemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME +
+ (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+ .toLong()
+ )
+
+ assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testShowNotification_stickyForSomeTime_autoDismissesWithStickyTimeout() {
+ val hum = createHeadsUpManager()
+ val entry = createStickyForSomeTimeEntry(/* id= */ 0)
+ useAccessibilityTimeout(false)
+
+ hum.showNotification(entry)
+ mSystemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME +
+ (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2)
+ .toLong()
+ )
+
+ assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testShowNotification_sticky_neverAutoDismisses() {
+ val hum = createHeadsUpManager()
+ val entry = createStickyEntry(/* id= */ 0)
+ useAccessibilityTimeout(false)
+
+ hum.showNotification(entry)
+ mSystemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME).toLong()
+ )
+
+ assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testShowNotification_autoDismissesWithAccessibilityTimeout() {
+ val hum = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(true)
+
+ hum.showNotification(entry)
+ mSystemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME +
+ (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+ .toLong()
+ )
+
+ assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testShowNotification_stickyForSomeTime_autoDismissesWithAccessibilityTimeout() {
+ val hum = createHeadsUpManager()
+ val entry = createStickyForSomeTimeEntry(/* id= */ 0)
+ useAccessibilityTimeout(true)
+
+ hum.showNotification(entry)
+ mSystemClock.advanceTime(
+ (TEST_TOUCH_ACCEPTANCE_TIME +
+ (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2)
+ .toLong()
+ )
+
+ assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+ }
+
+ @Test
+ fun testRemoveNotification_beforeMinimumDisplayTime() {
+ val hum = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(false)
+
+ hum.showNotification(entry)
+
+ val removedImmediately =
+ hum.removeNotification(
+ entry.key,
+ /* releaseImmediately = */ false,
+ "beforeMinimumDisplayTime",
+ )
+ assertThat(removedImmediately).isFalse()
+ assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+
+ mSystemClock.advanceTime(
+ ((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+ )
+
+ assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testRemoveNotification_afterMinimumDisplayTime() {
+ val hum = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ useAccessibilityTimeout(false)
+
+ hum.showNotification(entry)
+ mSystemClock.advanceTime(
+ ((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2).toLong()
+ )
+
+ assertThat(hum.isHeadsUpEntry(entry.key)).isTrue()
+
+ val removedImmediately =
+ hum.removeNotification(
+ entry.key,
+ /* releaseImmediately = */ false,
+ "afterMinimumDisplayTime",
+ )
+ assertThat(removedImmediately).isTrue()
+ assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testRemoveNotification_releaseImmediately() {
+ val hum = createHeadsUpManager()
+ val entry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ hum.showNotification(entry)
+
+ val removedImmediately =
+ hum.removeNotification(
+ entry.key,
+ /* releaseImmediately = */ true,
+ "afterMinimumDisplayTime",
+ )
+ assertThat(removedImmediately).isTrue()
+ assertThat(hum.isHeadsUpEntry(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testIsSticky_rowPinnedAndExpanded_true() {
+ val hum = createHeadsUpManager()
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+ Mockito.`when`(mRow!!.isPinned).thenReturn(true)
+ notifEntry.row = mRow
+
+ hum.showNotification(notifEntry)
+
+ val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
+ headsUpEntry!!.setExpanded(true)
+
+ assertThat(hum.isSticky(notifEntry.key)).isTrue()
+ }
+
+ @Test
+ fun testIsSticky_remoteInputActive_true() {
+ val hum = createHeadsUpManager()
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ hum.showNotification(notifEntry)
+
+ val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
+ headsUpEntry!!.mRemoteInputActive = true
+
+ assertThat(hum.isSticky(notifEntry.key)).isTrue()
+ }
+
+ @Test
+ fun testIsSticky_hasFullScreenIntent_true() {
+ val hum = createHeadsUpManager()
+ val notifEntry = HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+
+ hum.showNotification(notifEntry)
+
+ assertThat(hum.isSticky(notifEntry.key)).isTrue()
+ }
+
+ @Test
+ fun testIsSticky_stickyForSomeTime_false() {
+ val hum = createHeadsUpManager()
+ val entry = createStickyForSomeTimeEntry(/* id= */ 0)
+
+ hum.showNotification(entry)
+
+ assertThat(hum.isSticky(entry.key)).isFalse()
+ }
+
+ @Test
+ fun testIsSticky_false() {
+ val hum = createHeadsUpManager()
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ hum.showNotification(notifEntry)
+
+ val headsUpEntry = hum.getHeadsUpEntry(notifEntry.key)
+ headsUpEntry!!.setExpanded(false)
+ headsUpEntry.mRemoteInputActive = false
+
+ assertThat(hum.isSticky(notifEntry.key)).isFalse()
+ }
+
+ @Test
+ fun testCompareTo_withNullEntries() {
+ val hum = createHeadsUpManager()
+ val alertEntry = NotificationEntryBuilder().setTag("alert").build()
+
+ hum.showNotification(alertEntry)
+
+ assertThat(hum.compare(alertEntry, null)).isLessThan(0)
+ assertThat(hum.compare(null, alertEntry)).isGreaterThan(0)
+ assertThat(hum.compare(null, null)).isEqualTo(0)
+ }
+
+ @Test
+ fun testCompareTo_withNonAlertEntries() {
+ val hum = createHeadsUpManager()
+
+ val nonAlertEntry1 = NotificationEntryBuilder().setTag("nae1").build()
+ val nonAlertEntry2 = NotificationEntryBuilder().setTag("nae2").build()
+ val alertEntry = NotificationEntryBuilder().setTag("alert").build()
+ hum.showNotification(alertEntry)
+
+ assertThat(hum.compare(alertEntry, nonAlertEntry1)).isLessThan(0)
+ assertThat(hum.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0)
+ assertThat(hum.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0)
+ }
+
+ @Test
+ fun testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
+ val hum = createHeadsUpManager()
+
+ val ongoingCall =
+ hum.HeadsUpEntry(
+ NotificationEntryBuilder()
+ .setSbn(
+ HeadsUpManagerTestUtil.createSbn(
+ /* id = */ 0,
+ Notification.Builder(mContext, "")
+ .setCategory(Notification.CATEGORY_CALL)
+ .setOngoing(true),
+ )
+ )
+ .build()
+ )
+
+ val activeRemoteInput =
+ hum.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
+ activeRemoteInput.mRemoteInputActive = true
+
+ assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0)
+ assertThat(activeRemoteInput.compareTo(ongoingCall)).isGreaterThan(0)
+ }
+
+ @Test
+ fun testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
+ val hum = createHeadsUpManager()
+
+ val person = Person.Builder().setName("person").build()
+ val intent = Mockito.mock(PendingIntent::class.java)
+ val incomingCall =
+ hum.HeadsUpEntry(
+ NotificationEntryBuilder()
+ .setSbn(
+ HeadsUpManagerTestUtil.createSbn(
+ /* id = */ 0,
+ Notification.Builder(mContext, "")
+ .setStyle(
+ Notification.CallStyle.forIncomingCall(person, intent, intent)
+ ),
+ )
+ )
+ .build()
+ )
+
+ val activeRemoteInput =
+ hum.HeadsUpEntry(HeadsUpManagerTestUtil.createEntry(/* id= */ 1, mContext))
+ activeRemoteInput.mRemoteInputActive = true
+
+ assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0)
+ assertThat(activeRemoteInput.compareTo(incomingCall)).isGreaterThan(0)
+ }
+
+ @Test
+ @EnableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testPinEntry_logsPeek_throttleEnabled() {
+ val hum = createHeadsUpManager()
+
+ // Needs full screen intent in order to be pinned
+ val entryToPin =
+ hum.HeadsUpEntry(
+ HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+ )
+
+ // Note: the standard way to show a notification would be calling showNotification rather
+ // than onAlertEntryAdded. However, in practice showNotification in effect adds
+ // the notification and then updates it; in order to not log twice, the entry needs
+ // to have a functional ExpandableNotificationRow that can keep track of whether it's
+ // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
+ hum.onEntryAdded(entryToPin)
+
+ assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(2)
+ assertThat(
+ AvalancheController.ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN.getId(),
+ ).isEqualTo(mUiEventLoggerFake.eventId(0))
+ assertThat(
+ HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id,
+ ).isEqualTo(mUiEventLoggerFake.eventId(1))
+ }
+
+ @Test
+ @DisableFlags(NotificationThrottleHun.FLAG_NAME)
+ fun testPinEntry_logsPeek_throttleDisabled() {
+ val hum = createHeadsUpManager()
+
+ // Needs full screen intent in order to be pinned
+ val entryToPin =
+ hum.HeadsUpEntry(
+ HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id= */ 0, mContext)
+ )
+
+ // Note: the standard way to show a notification would be calling showNotification rather
+ // than onAlertEntryAdded. However, in practice showNotification in effect adds
+ // the notification and then updates it; in order to not log twice, the entry needs
+ // to have a functional ExpandableNotificationRow that can keep track of whether it's
+ // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
+ hum.onEntryAdded(entryToPin)
+
+ assertThat(mUiEventLoggerFake.numLogs()).isEqualTo(1)
+ assertThat(
+ HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.id,
+ ).isEqualTo(mUiEventLoggerFake.eventId(0))
+ }
+
+ @Test
+ fun testSetUserActionMayIndirectlyRemove() {
+ val hum = createHeadsUpManager()
+ val notifEntry = HeadsUpManagerTestUtil.createEntry(/* id= */ 0, mContext)
+
+ hum.showNotification(notifEntry)
+
+ assertThat(hum.canRemoveImmediately(notifEntry.key)).isFalse()
+
+ hum.setUserActionMayIndirectlyRemove(notifEntry)
+
+ assertThat(hum.canRemoveImmediately(notifEntry.key)).isTrue()
+ }
+
+ companion object {
+ const val TEST_TOUCH_ACCEPTANCE_TIME: Int = 200
+ const val TEST_A11Y_AUTO_DISMISS_TIME: Int = 1000
+
+ const val TEST_MINIMUM_DISPLAY_TIME: Int = 400
+ const val TEST_AUTO_DISMISS_TIME: Int = 600
+ const val TEST_STICKY_AUTO_DISMISS_TIME: Int = 800
+
+ // Number of notifications to use in tests requiring multiple notifications
+ private const val TEST_NUM_NOTIFICATIONS = 4
+
+ init {
+ Truth.assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME)
+ Truth.assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME)
+ Truth.assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME)
+ }
+
+ @get:Parameters(name = "{0}")
+ @JvmStatic
+ val flags: List<FlagsParameterization>
+ get() = FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.java
deleted file mode 100644
index 01f78cb..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.java
+++ /dev/null
@@ -1,664 +0,0 @@
-/*
- * Copyright (C) 2019 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.systemui.statusbar.notification.headsup;
-
-import static android.app.Notification.FLAG_FSI_REQUESTED_BUT_DENIED;
-
-import static com.android.systemui.log.LogBufferHelperKt.logcatLogBuffer;
-
-import static com.google.common.truth.Truth.assertThat;
-
-import static junit.framework.Assert.assertEquals;
-import static junit.framework.Assert.assertFalse;
-import static junit.framework.Assert.assertTrue;
-
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
-import android.app.Notification;
-import android.app.PendingIntent;
-import android.app.Person;
-import android.os.Handler;
-import android.platform.test.annotations.DisableFlags;
-import android.platform.test.annotations.EnableFlags;
-import android.platform.test.flag.junit.FlagsParameterization;
-import android.testing.TestableLooper;
-
-import androidx.test.filters.SmallTest;
-
-import com.android.internal.logging.testing.UiEventLoggerFake;
-import com.android.systemui.SysuiTestCase;
-import com.android.systemui.dump.DumpManager;
-import com.android.systemui.kosmos.KosmosJavaAdapter;
-import com.android.systemui.res.R;
-import com.android.systemui.shade.domain.interactor.ShadeInteractor;
-import com.android.systemui.statusbar.notification.collection.NotificationEntry;
-import com.android.systemui.statusbar.notification.collection.NotificationEntryBuilder;
-import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManagerImpl;
-import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
-import com.android.systemui.statusbar.notification.shared.NotificationThrottleHun;
-import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
-import com.android.systemui.util.concurrency.FakeExecutor;
-import com.android.systemui.util.kotlin.JavaAdapter;
-import com.android.systemui.util.settings.FakeGlobalSettings;
-import com.android.systemui.util.time.FakeSystemClock;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.Mock;
-import org.mockito.junit.MockitoJUnit;
-import org.mockito.junit.MockitoRule;
-
-import kotlinx.coroutines.flow.StateFlowKt;
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
-import platform.test.runner.parameterized.Parameters;
-
-import java.util.List;
-
-@SmallTest
-@TestableLooper.RunWithLooper
-@RunWith(ParameterizedAndroidJunit4.class)
-// TODO(b/378142453): Merge this with HeadsUpManagerPhoneTest.
-public class HeadsUpManagerImplTest extends SysuiTestCase {
- protected KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this);
-
- @Rule
- public MockitoRule rule = MockitoJUnit.rule();
-
- static final int TEST_TOUCH_ACCEPTANCE_TIME = 200;
- static final int TEST_A11Y_AUTO_DISMISS_TIME = 1_000;
-
- private UiEventLoggerFake mUiEventLoggerFake = new UiEventLoggerFake();
-
- private final HeadsUpManagerLogger mLogger = spy(new HeadsUpManagerLogger(logcatLogBuffer()));
- @Mock private Handler mBgHandler;
- @Mock private DumpManager dumpManager;
- @Mock private ShadeInteractor mShadeInteractor;
- private AvalancheController mAvalancheController;
-
- @Mock private AccessibilityManagerWrapper mAccessibilityMgr;
-
- protected static final int TEST_MINIMUM_DISPLAY_TIME = 400;
- protected static final int TEST_AUTO_DISMISS_TIME = 600;
- protected static final int TEST_STICKY_AUTO_DISMISS_TIME = 800;
- // Number of notifications to use in tests requiring multiple notifications
- private static final int TEST_NUM_NOTIFICATIONS = 4;
-
- protected final FakeGlobalSettings mGlobalSettings = new FakeGlobalSettings();
- protected final FakeSystemClock mSystemClock = new FakeSystemClock();
- protected final FakeExecutor mExecutor = new FakeExecutor(mSystemClock);
-
- @Mock protected ExpandableNotificationRow mRow;
-
- static {
- assertThat(TEST_MINIMUM_DISPLAY_TIME).isLessThan(TEST_AUTO_DISMISS_TIME);
- assertThat(TEST_AUTO_DISMISS_TIME).isLessThan(TEST_STICKY_AUTO_DISMISS_TIME);
- assertThat(TEST_STICKY_AUTO_DISMISS_TIME).isLessThan(TEST_A11Y_AUTO_DISMISS_TIME);
- }
-
- private HeadsUpManagerImpl createHeadsUpManager() {
- return new TestableHeadsUpManager(
- mContext,
- mLogger,
- mKosmos.getStatusBarStateController(),
- mKosmos.getKeyguardBypassController(),
- new GroupMembershipManagerImpl(),
- mKosmos.getVisualStabilityProvider(),
- mKosmos.getConfigurationController(),
- mExecutor,
- mGlobalSettings,
- mSystemClock,
- mAccessibilityMgr,
- mUiEventLoggerFake,
- new JavaAdapter(mKosmos.getTestScope()),
- mShadeInteractor,
- mAvalancheController);
- }
-
- private NotificationEntry createStickyEntry(int id) {
- final Notification notif = new Notification.Builder(mContext, "")
- .setSmallIcon(R.drawable.ic_person)
- .setFullScreenIntent(mock(PendingIntent.class), /* highPriority */ true)
- .build();
- return HeadsUpManagerTestUtil.createEntry(id, notif);
- }
-
- private NotificationEntry createStickyForSomeTimeEntry(int id) {
- final Notification notif = new Notification.Builder(mContext, "")
- .setSmallIcon(R.drawable.ic_person)
- .setFlag(FLAG_FSI_REQUESTED_BUT_DENIED, true)
- .build();
- return HeadsUpManagerTestUtil.createEntry(id, notif);
- }
-
- private void useAccessibilityTimeout(boolean use) {
- if (use) {
- doReturn(TEST_A11Y_AUTO_DISMISS_TIME).when(mAccessibilityMgr)
- .getRecommendedTimeoutMillis(anyInt(), anyInt());
- } else {
- when(mAccessibilityMgr.getRecommendedTimeoutMillis(anyInt(), anyInt())).then(
- i -> i.getArgument(0));
- }
- }
-
- @Parameters(name = "{0}")
- public static List<FlagsParameterization> getFlags() {
- return FlagsParameterization.allCombinationsOf(NotificationThrottleHun.FLAG_NAME);
- }
-
- public HeadsUpManagerImplTest(FlagsParameterization flags) {
- mSetFlagsRule.setFlagsParameterization(flags);
- }
-
- @Override
- public void SysuiSetup() throws Exception {
- super.SysuiSetup();
- mAvalancheController = new AvalancheController(dumpManager, mUiEventLoggerFake, mLogger,
- mBgHandler);
- when(mShadeInteractor.isAnyExpanded()).thenReturn(MutableStateFlow(true));
- when(mKosmos.getKeyguardBypassController().getBypassEnabled()).thenReturn(false);
- }
-
- @Test
- public void testHasNotifications_headsUpManagerMapNotEmpty_true() {
- final HeadsUpManagerImpl bhum = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
- bhum.showNotification(entry);
-
- assertThat(bhum.mHeadsUpEntryMap).isNotEmpty();
- assertThat(bhum.hasNotifications()).isTrue();
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- public void testHasNotifications_avalancheMapNotEmpty_true() {
- final HeadsUpManagerImpl bhum = createHeadsUpManager();
- final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
- mContext);
- final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = bhum.createHeadsUpEntry(notifEntry);
- mAvalancheController.addToNext(headsUpEntry, () -> {});
-
- assertThat(mAvalancheController.getWaitingEntryList()).isNotEmpty();
- assertThat(bhum.hasNotifications()).isTrue();
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- public void testHasNotifications_false() {
- final HeadsUpManagerImpl bhum = createHeadsUpManager();
- assertThat(bhum.mHeadsUpEntryMap).isEmpty();
- assertThat(mAvalancheController.getWaitingEntryList()).isEmpty();
- assertThat(bhum.hasNotifications()).isFalse();
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- public void testGetHeadsUpEntryList_includesAvalancheEntryList() {
- final HeadsUpManagerImpl bhum = createHeadsUpManager();
- final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
- mContext);
- final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = bhum.createHeadsUpEntry(notifEntry);
- mAvalancheController.addToNext(headsUpEntry, () -> {});
-
- assertThat(bhum.getHeadsUpEntryList()).contains(headsUpEntry);
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- public void testGetHeadsUpEntry_returnsAvalancheEntry() {
- final HeadsUpManagerImpl bhum = createHeadsUpManager();
- final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
- mContext);
- final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = bhum.createHeadsUpEntry(notifEntry);
- mAvalancheController.addToNext(headsUpEntry, () -> {});
-
- assertThat(bhum.getHeadsUpEntry(notifEntry.getKey())).isEqualTo(headsUpEntry);
- }
-
- @Test
- public void testShowNotification_addsEntry() {
- final HeadsUpManagerImpl alm = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
- alm.showNotification(entry);
-
- assertTrue(alm.isHeadsUpEntry(entry.getKey()));
- assertTrue(alm.hasNotifications());
- assertEquals(entry, alm.getEntry(entry.getKey()));
- }
-
- @Test
- public void testShowNotification_autoDismisses() {
- final HeadsUpManagerImpl alm = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
- alm.showNotification(entry);
- mSystemClock.advanceTime(TEST_AUTO_DISMISS_TIME * 3 / 2);
-
- assertFalse(alm.isHeadsUpEntry(entry.getKey()));
- }
-
- @Test
- public void testRemoveNotification_removeDeferred() {
- final HeadsUpManagerImpl alm = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
- alm.showNotification(entry);
-
- final boolean removedImmediately = alm.removeNotification(
- entry.getKey(), /* releaseImmediately = */ false, "removeDeferred");
- assertFalse(removedImmediately);
- assertTrue(alm.isHeadsUpEntry(entry.getKey()));
- }
-
- @Test
- public void testRemoveNotification_forceRemove() {
- final HeadsUpManagerImpl alm = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
- alm.showNotification(entry);
-
- final boolean removedImmediately = alm.removeNotification(
- entry.getKey(), /* releaseImmediately = */ true, "forceRemove");
- assertTrue(removedImmediately);
- assertFalse(alm.isHeadsUpEntry(entry.getKey()));
- }
-
- @Test
- public void testReleaseAllImmediately() {
- final HeadsUpManagerImpl alm = createHeadsUpManager();
- for (int i = 0; i < TEST_NUM_NOTIFICATIONS; i++) {
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(i, mContext);
- entry.setRow(mRow);
- alm.showNotification(entry);
- }
-
- alm.releaseAllImmediately();
-
- assertEquals(0, alm.getAllEntries().count());
- }
-
- @Test
- public void testCanRemoveImmediately_notShownLongEnough() {
- final HeadsUpManagerImpl alm = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
- alm.showNotification(entry);
-
- // The entry has just been added so we should not remove immediately.
- assertFalse(alm.canRemoveImmediately(entry.getKey()));
- }
-
- @Test
- public void testHunRemovedLogging() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
- mContext);
- final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = mock(
- HeadsUpManagerImpl.HeadsUpEntry.class);
- when(headsUpEntry.getPinnedStatus())
- .thenReturn(StateFlowKt.MutableStateFlow(PinnedStatus.NotPinned));
- headsUpEntry.mEntry = notifEntry;
-
- hum.onEntryRemoved(headsUpEntry, "test");
-
- verify(mLogger, times(1)).logNotificationActuallyRemoved(eq(notifEntry));
- }
-
-
- @Test
- public void testShowNotification_autoDismissesIncludingTouchAcceptanceDelay() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
- useAccessibilityTimeout(false);
-
- hum.showNotification(entry);
- mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME / 2 + TEST_AUTO_DISMISS_TIME);
-
- assertTrue(hum.isHeadsUpEntry(entry.getKey()));
- }
-
-
- @Test
- public void testShowNotification_autoDismissesWithDefaultTimeout() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
- useAccessibilityTimeout(false);
-
- hum.showNotification(entry);
- mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME
- + (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2);
-
- assertFalse(hum.isHeadsUpEntry(entry.getKey()));
- }
-
-
- @Test
- public void testShowNotification_stickyForSomeTime_autoDismissesWithStickyTimeout() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = createStickyForSomeTimeEntry(/* id = */ 0);
- useAccessibilityTimeout(false);
-
- hum.showNotification(entry);
- mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME
- + (TEST_AUTO_DISMISS_TIME + TEST_STICKY_AUTO_DISMISS_TIME) / 2);
-
- assertTrue(hum.isHeadsUpEntry(entry.getKey()));
- }
-
-
- @Test
- public void testShowNotification_sticky_neverAutoDismisses() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = createStickyEntry(/* id = */ 0);
- useAccessibilityTimeout(false);
-
- hum.showNotification(entry);
- mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME + 2 * TEST_A11Y_AUTO_DISMISS_TIME);
-
- assertTrue(hum.isHeadsUpEntry(entry.getKey()));
- }
-
-
- @Test
- public void testShowNotification_autoDismissesWithAccessibilityTimeout() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
- useAccessibilityTimeout(true);
-
- hum.showNotification(entry);
- mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME
- + (TEST_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2);
-
- assertTrue(hum.isHeadsUpEntry(entry.getKey()));
- }
-
-
- @Test
- public void testShowNotification_stickyForSomeTime_autoDismissesWithAccessibilityTimeout() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = createStickyForSomeTimeEntry(/* id = */ 0);
- useAccessibilityTimeout(true);
-
- hum.showNotification(entry);
- mSystemClock.advanceTime(TEST_TOUCH_ACCEPTANCE_TIME
- + (TEST_STICKY_AUTO_DISMISS_TIME + TEST_A11Y_AUTO_DISMISS_TIME) / 2);
-
- assertTrue(hum.isHeadsUpEntry(entry.getKey()));
- }
-
-
- @Test
- public void testRemoveNotification_beforeMinimumDisplayTime() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
- useAccessibilityTimeout(false);
-
- hum.showNotification(entry);
-
- final boolean removedImmediately = hum.removeNotification(
- entry.getKey(), /* releaseImmediately = */ false, "beforeMinimumDisplayTime");
- assertFalse(removedImmediately);
- assertTrue(hum.isHeadsUpEntry(entry.getKey()));
-
- mSystemClock.advanceTime((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2);
-
- assertFalse(hum.isHeadsUpEntry(entry.getKey()));
- }
-
-
- @Test
- public void testRemoveNotification_afterMinimumDisplayTime() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
- useAccessibilityTimeout(false);
-
- hum.showNotification(entry);
- mSystemClock.advanceTime((TEST_MINIMUM_DISPLAY_TIME + TEST_AUTO_DISMISS_TIME) / 2);
-
- assertTrue(hum.isHeadsUpEntry(entry.getKey()));
-
- final boolean removedImmediately = hum.removeNotification(
- entry.getKey(), /* releaseImmediately = */ false, "afterMinimumDisplayTime");
- assertTrue(removedImmediately);
- assertFalse(hum.isHeadsUpEntry(entry.getKey()));
- }
-
-
- @Test
- public void testRemoveNotification_releaseImmediately() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0, mContext);
-
- hum.showNotification(entry);
-
- final boolean removedImmediately = hum.removeNotification(
- entry.getKey(), /* releaseImmediately = */ true, "afterMinimumDisplayTime");
- assertTrue(removedImmediately);
- assertFalse(hum.isHeadsUpEntry(entry.getKey()));
- }
-
-
- @Test
- public void testIsSticky_rowPinnedAndExpanded_true() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
- mContext);
- when(mRow.isPinned()).thenReturn(true);
- notifEntry.setRow(mRow);
-
- hum.showNotification(notifEntry);
-
- final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(
- notifEntry.getKey());
- headsUpEntry.setExpanded(true);
-
- assertTrue(hum.isSticky(notifEntry.getKey()));
- }
-
- @Test
- public void testIsSticky_remoteInputActive_true() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
- mContext);
-
- hum.showNotification(notifEntry);
-
- final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(
- notifEntry.getKey());
- headsUpEntry.mRemoteInputActive = true;
-
- assertTrue(hum.isSticky(notifEntry.getKey()));
- }
-
- @Test
- public void testIsSticky_hasFullScreenIntent_true() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry notifEntry =
- HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext);
-
- hum.showNotification(notifEntry);
-
- assertTrue(hum.isSticky(notifEntry.getKey()));
- }
-
-
- @Test
- public void testIsSticky_stickyForSomeTime_false() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry entry = createStickyForSomeTimeEntry(/* id = */ 0);
-
- hum.showNotification(entry);
-
- assertFalse(hum.isSticky(entry.getKey()));
- }
-
-
- @Test
- public void testIsSticky_false() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
- mContext);
-
- hum.showNotification(notifEntry);
-
- final HeadsUpManagerImpl.HeadsUpEntry headsUpEntry = hum.getHeadsUpEntry(
- notifEntry.getKey());
- headsUpEntry.setExpanded(false);
- headsUpEntry.mRemoteInputActive = false;
-
- assertFalse(hum.isSticky(notifEntry.getKey()));
- }
-
- @Test
- public void testCompareTo_withNullEntries() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry alertEntry = new NotificationEntryBuilder().setTag("alert").build();
-
- hum.showNotification(alertEntry);
-
- assertThat(hum.compare(alertEntry, null)).isLessThan(0);
- assertThat(hum.compare(null, alertEntry)).isGreaterThan(0);
- assertThat(hum.compare(null, null)).isEqualTo(0);
- }
-
- @Test
- public void testCompareTo_withNonAlertEntries() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
-
- final NotificationEntry nonAlertEntry1 = new NotificationEntryBuilder().setTag(
- "nae1").build();
- final NotificationEntry nonAlertEntry2 = new NotificationEntryBuilder().setTag(
- "nae2").build();
- final NotificationEntry alertEntry = new NotificationEntryBuilder().setTag("alert").build();
- hum.showNotification(alertEntry);
-
- assertThat(hum.compare(alertEntry, nonAlertEntry1)).isLessThan(0);
- assertThat(hum.compare(nonAlertEntry1, alertEntry)).isGreaterThan(0);
- assertThat(hum.compare(nonAlertEntry1, nonAlertEntry2)).isEqualTo(0);
- }
-
- @Test
- public void testAlertEntryCompareTo_ongoingCallLessThanActiveRemoteInput() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
-
- final HeadsUpManagerImpl.HeadsUpEntry ongoingCall = hum.new HeadsUpEntry(
- new NotificationEntryBuilder()
- .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
- new Notification.Builder(mContext, "")
- .setCategory(Notification.CATEGORY_CALL)
- .setOngoing(true)))
- .build());
-
- final HeadsUpManagerImpl.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(
- HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
- activeRemoteInput.mRemoteInputActive = true;
-
- assertThat(ongoingCall.compareTo(activeRemoteInput)).isLessThan(0);
- assertThat(activeRemoteInput.compareTo(ongoingCall)).isGreaterThan(0);
- }
-
- @Test
- public void testAlertEntryCompareTo_incomingCallLessThanActiveRemoteInput() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
-
- final Person person = new Person.Builder().setName("person").build();
- final PendingIntent intent = mock(PendingIntent.class);
- final HeadsUpManagerImpl.HeadsUpEntry incomingCall = hum.new HeadsUpEntry(
- new NotificationEntryBuilder()
- .setSbn(HeadsUpManagerTestUtil.createSbn(/* id = */ 0,
- new Notification.Builder(mContext, "")
- .setStyle(Notification.CallStyle
- .forIncomingCall(person, intent, intent))))
- .build());
-
- final HeadsUpManagerImpl.HeadsUpEntry activeRemoteInput = hum.new HeadsUpEntry(
- HeadsUpManagerTestUtil.createEntry(/* id = */ 1, mContext));
- activeRemoteInput.mRemoteInputActive = true;
-
- assertThat(incomingCall.compareTo(activeRemoteInput)).isLessThan(0);
- assertThat(activeRemoteInput.compareTo(incomingCall)).isGreaterThan(0);
- }
-
- @Test
- @EnableFlags(NotificationThrottleHun.FLAG_NAME)
- public void testPinEntry_logsPeek_throttleEnabled() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
-
- // Needs full screen intent in order to be pinned
- final HeadsUpManagerImpl.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(
- HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext));
-
- // Note: the standard way to show a notification would be calling showNotification rather
- // than onAlertEntryAdded. However, in practice showNotification in effect adds
- // the notification and then updates it; in order to not log twice, the entry needs
- // to have a functional ExpandableNotificationRow that can keep track of whether it's
- // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
- hum.onEntryAdded(entryToPin);
-
- assertEquals(2, mUiEventLoggerFake.numLogs());
- assertEquals(AvalancheController.ThrottleEvent.AVALANCHE_THROTTLING_HUN_SHOWN.getId(),
- mUiEventLoggerFake.eventId(0));
- assertEquals(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.getId(),
- mUiEventLoggerFake.eventId(1));
- }
-
- @Test
- @DisableFlags(NotificationThrottleHun.FLAG_NAME)
- public void testPinEntry_logsPeek_throttleDisabled() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
-
- // Needs full screen intent in order to be pinned
- final HeadsUpManagerImpl.HeadsUpEntry entryToPin = hum.new HeadsUpEntry(
- HeadsUpManagerTestUtil.createFullScreenIntentEntry(/* id = */ 0, mContext));
-
- // Note: the standard way to show a notification would be calling showNotification rather
- // than onAlertEntryAdded. However, in practice showNotification in effect adds
- // the notification and then updates it; in order to not log twice, the entry needs
- // to have a functional ExpandableNotificationRow that can keep track of whether it's
- // pinned or not (via isRowPinned()). That feels like a lot to pull in to test this one bit.
- hum.onEntryAdded(entryToPin);
-
- assertEquals(1, mUiEventLoggerFake.numLogs());
- assertEquals(HeadsUpManagerImpl.NotificationPeekEvent.NOTIFICATION_PEEK.getId(),
- mUiEventLoggerFake.eventId(0));
- }
-
- @Test
- public void testSetUserActionMayIndirectlyRemove() {
- final HeadsUpManagerImpl hum = createHeadsUpManager();
- final NotificationEntry notifEntry = HeadsUpManagerTestUtil.createEntry(/* id = */ 0,
- mContext);
-
- hum.showNotification(notifEntry);
-
- assertFalse(hum.canRemoveImmediately(notifEntry.getKey()));
-
- hum.setUserActionMayIndirectlyRemove(notifEntry);
-
- assertTrue(hum.canRemoveImmediately(notifEntry.getKey()));
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerPhoneTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
similarity index 98%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerPhoneTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
index 35d8253..65d282f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerPhoneTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/HeadsUpManagerImplTest.kt
@@ -59,7 +59,7 @@
@SmallTest
@RunWith(ParameterizedAndroidJunit4::class)
@RunWithLooper
-class HeadsUpManagerPhoneTest(flags: FlagsParameterization) : HeadsUpManagerImplTest(flags) {
+class HeadsUpManagerImplTest(flags: FlagsParameterization) : HeadsUpManagerImplOldTest(flags) {
private val mHeadsUpManagerLogger = HeadsUpManagerLogger(logcatLogBuffer())
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/TestableHeadsUpManager.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/TestableHeadsUpManager.java
index 2b077ed..7ded1a5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/TestableHeadsUpManager.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/headsup/TestableHeadsUpManager.java
@@ -78,10 +78,10 @@
shadeInteractor,
avalancheController);
- mTouchAcceptanceDelay = HeadsUpManagerImplTest.TEST_TOUCH_ACCEPTANCE_TIME;
- mMinimumDisplayTime = HeadsUpManagerImplTest.TEST_MINIMUM_DISPLAY_TIME;
- mAutoDismissTime = HeadsUpManagerImplTest.TEST_AUTO_DISMISS_TIME;
- mStickyForSomeTimeAutoDismissTime = HeadsUpManagerImplTest.TEST_STICKY_AUTO_DISMISS_TIME;
+ mTouchAcceptanceDelay = HeadsUpManagerImplOldTest.TEST_TOUCH_ACCEPTANCE_TIME;
+ mMinimumDisplayTime = HeadsUpManagerImplOldTest.TEST_MINIMUM_DISPLAY_TIME;
+ mAutoDismissTime = HeadsUpManagerImplOldTest.TEST_AUTO_DISMISS_TIME;
+ mStickyForSomeTimeAutoDismissTime = HeadsUpManagerImplOldTest.TEST_STICKY_AUTO_DISMISS_TIME;
}
@NonNull
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorTest.kt
new file mode 100644
index 0000000..8acf538
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorTest.kt
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2024 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.systemui.volume.dialog.domain.interactor
+
+import android.app.ActivityManager
+import android.media.AudioManager
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.collectLastValue
+import com.android.systemui.kosmos.runTest
+import com.android.systemui.kosmos.useUnconfinedTestDispatcher
+import com.android.systemui.plugins.fakeVolumeDialogController
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.Events
+import com.android.systemui.volume.dialog.data.repository.volumeDialogVisibilityRepository
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+class VolumeDialogSafetyWarningInteractorTest : SysuiTestCase() {
+
+ private val kosmos: Kosmos = testKosmos()
+
+ private lateinit var underTest: VolumeDialogSafetyWarningInteractor
+
+ @Before
+ fun setup() {
+ kosmos.useUnconfinedTestDispatcher()
+ underTest = kosmos.volumeDialogSafetyWarningInteractor
+ }
+
+ @Test
+ fun dismiss_isShowingSafetyWarning_isFalse() =
+ with(kosmos) {
+ runTest {
+ val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+
+ underTest.onSafetyWarningDismissed()
+
+ assertThat(isShowingSafetyWarning).isFalse()
+ }
+ }
+
+ @Test
+ fun flagShowUi_isShowingSafetyWarning_isTrue() =
+ with(kosmos) {
+ runTest {
+ val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+
+ fakeVolumeDialogController.onShowSafetyWarning(AudioManager.FLAG_SHOW_UI)
+
+ assertThat(isShowingSafetyWarning).isTrue()
+ }
+ }
+
+ @Test
+ fun flagShowUiWarnings_isShowingSafetyWarning_isTrue() =
+ with(kosmos) {
+ runTest {
+ val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+
+ fakeVolumeDialogController.onShowSafetyWarning(AudioManager.FLAG_SHOW_UI_WARNINGS)
+
+ assertThat(isShowingSafetyWarning).isTrue()
+ }
+ }
+
+ @Test
+ fun invisibleAndNoFlags_isShowingSafetyWarning_isFalse() =
+ with(kosmos) {
+ runTest {
+ val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+ volumeDialogVisibilityRepository.updateVisibility {
+ VolumeDialogVisibilityModel.Invisible
+ }
+
+ fakeVolumeDialogController.onShowSafetyWarning(0)
+
+ assertThat(isShowingSafetyWarning).isFalse()
+ }
+ }
+
+ @Test
+ fun visibleAndNoFlags_isShowingSafetyWarning_isTrue() =
+ with(kosmos) {
+ runTest {
+ val isShowingSafetyWarning by collectLastValue(underTest.isShowingSafetyWarning)
+ volumeDialogVisibilityRepository.updateVisibility {
+ VolumeDialogVisibilityModel.Visible(
+ Events.SHOW_REASON_VOLUME_CHANGED,
+ false,
+ ActivityManager.LOCK_TASK_MODE_LOCKED,
+ )
+ }
+
+ fakeVolumeDialogController.onShowSafetyWarning(0)
+
+ assertThat(isShowingSafetyWarning).isTrue()
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt
index bba0050..a42ae03 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepository.kt
@@ -18,6 +18,7 @@
import android.content.res.Resources
import com.android.internal.R
+import com.android.systemui.common.ui.GlobalConfig
import com.android.systemui.common.ui.data.repository.ConfigurationRepository
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -36,7 +37,7 @@
constructor(
@Application private val applicationScope: CoroutineScope,
@Main private val resources: Resources,
- configurationRepository: ConfigurationRepository,
+ @GlobalConfig configurationRepository: ConfigurationRepository,
) {
/**
* Whether to enable emergency services calls while the SIM card is locked. This is disabled in
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
index 61cd7c7..641400a 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractor.kt
@@ -42,6 +42,7 @@
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.shared.system.SysUiStatsLog
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
@@ -71,7 +72,7 @@
private val primaryBouncerCallbackInteractor: PrimaryBouncerCallbackInteractor,
private val falsingCollector: FalsingCollector,
private val dismissCallbackRegistry: DismissCallbackRegistry,
- private val context: Context,
+ @ShadeDisplayAware private val context: Context,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val trustRepository: TrustRepository,
@Application private val applicationScope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
index 1aaf4fb..ec9ee91 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractor.kt
@@ -37,6 +37,7 @@
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
+import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository
import com.android.systemui.util.icuMessageFormat
import javax.inject.Inject
@@ -62,7 +63,7 @@
@Background private val backgroundDispatcher: CoroutineDispatcher,
private val repository: SimBouncerRepository,
private val telephonyManager: TelephonyManager,
- @Main private val resources: Resources,
+ @ShadeDisplayAware private val resources: Resources,
private val keyguardUpdateMonitor: KeyguardUpdateMonitor,
private val euiccManager: EuiccManager?,
// TODO(b/307977401): Replace this with `MobileConnectionsInteractor` when available.
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/docking/ui/viewmodel/KeyboardDockingIndicationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/docking/ui/viewmodel/KeyboardDockingIndicationViewModel.kt
index 38fc2a8..84a423e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/docking/ui/viewmodel/KeyboardDockingIndicationViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/docking/ui/viewmodel/KeyboardDockingIndicationViewModel.kt
@@ -21,11 +21,12 @@
import android.view.WindowManager
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.settingslib.Utils
+import com.android.systemui.common.ui.GlobalConfig
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyboard.docking.domain.interactor.KeyboardDockingIndicationInteractor
-import com.android.systemui.shade.ShadeDisplayAware
import com.android.systemui.surfaceeffects.glowboxeffect.GlowBoxConfig
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -37,9 +38,9 @@
@Inject
constructor(
private val windowManager: WindowManager,
- private val context: Context,
+ @Application private val context: Context,
keyboardDockingIndicationInteractor: KeyboardDockingIndicationInteractor,
- @ShadeDisplayAware configurationInteractor: ConfigurationInteractor,
+ @GlobalConfig configurationInteractor: ConfigurationInteractor,
@Background private val backgroundScope: CoroutineScope,
) {
diff --git a/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt b/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt
index debb667..a19c9b3 100644
--- a/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediarouter/data/repository/MediaRouterRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.mediarouter.data.repository
+import android.media.projection.StopReason
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
@@ -40,7 +41,7 @@
val castDevices: StateFlow<List<CastDevice>>
/** Stops the cast to the given device. */
- fun stopCasting(device: CastDevice)
+ fun stopCasting(device: CastDevice, @StopReason stopReason: Int)
}
@SysUISingleton
@@ -67,8 +68,8 @@
.map { it.filter { device -> device.origin == CastDevice.CastOrigin.MediaRouter } }
.stateIn(scope, SharingStarted.WhileSubscribed(), emptyList())
- override fun stopCasting(device: CastDevice) {
- castController.stopCasting(device)
+ override fun stopCasting(device: CastDevice, @StopReason stopReason: Int) {
+ castController.stopCasting(device, stopReason)
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
index eff5fc0..d8fc52b 100644
--- a/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
+++ b/packages/SystemUI/src/com/android/systemui/notetask/NoteTaskInitializer.kt
@@ -216,20 +216,13 @@
"handleKeyGestureEvent: Received OPEN_NOTES gesture event from keycodes: " +
event.keycodes.contentToString()
}
- if (
- event.keycodes.contains(KEYCODE_N) &&
- event.hasModifiers(KeyEvent.META_CTRL_ON or KeyEvent.META_META_ON)
- ) {
- debugLog { "Note task triggered by keyboard shortcut" }
- backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) }
- return true
- }
if (event.keycodes.size == 1 && event.keycodes[0] == KEYCODE_STYLUS_BUTTON_TAIL) {
debugLog { "Note task triggered by stylus tail button" }
backgroundExecutor.execute { controller.showNoteTask(TAIL_BUTTON) }
return true
}
- return false
+ backgroundExecutor.execute { controller.showNoteTask(KEYBOARD_SHORTCUT) }
+ return true
}
private fun isKeyGestureSupported(gestureType: Int): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index 8a72e8d..00b4524 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -24,6 +24,7 @@
import android.app.Dialog;
import android.content.Intent;
import android.media.MediaRouter.RouteInfo;
+import android.media.projection.StopReason;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
@@ -183,7 +184,7 @@
});
}
} else {
- mController.stopCasting(activeDevices.get(0));
+ mController.stopCasting(activeDevices.get(0), StopReason.STOP_QS_TILE);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
index f3be340..564d166 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/ScreenRecordTile.java
@@ -18,6 +18,7 @@
import android.app.Dialog;
import android.content.Intent;
+import android.media.projection.StopReason;
import android.os.Handler;
import android.os.Looper;
import android.service.quicksettings.Tile;
@@ -225,7 +226,7 @@
}
private void stopRecording() {
- mController.stopRecording();
+ mController.stopRecording(StopReason.STOP_QS_TILE);
}
private final class Callback implements RecordingController.RecordingStateChangeCallback {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
index 85aa674..9453447 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/screenrecord/domain/interactor/ScreenRecordTileUserActionInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.qs.tiles.impl.screenrecord.domain.interactor
+import android.media.projection.StopReason
import android.util.Log
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.DialogCuj
@@ -61,7 +62,9 @@
Log.d(TAG, "Cancelling countdown")
withContext(backgroundContext) { recordingController.cancelCountdown() }
}
- is ScreenRecordModel.Recording -> screenRecordRepository.stopRecording()
+ is ScreenRecordModel.Recording -> {
+ screenRecordRepository.stopRecording(StopReason.STOP_QS_TILE)
+ }
is ScreenRecordModel.DoingNothing ->
withContext(mainContext) {
showPrompt(action.expandable, user.identifier)
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
index d7463f8..9ee99e4 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingController.java
@@ -23,6 +23,7 @@
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
+import android.media.projection.StopReason;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.os.Process;
@@ -58,6 +59,7 @@
private boolean mIsStarting;
private boolean mIsRecording;
private PendingIntent mStopIntent;
+ private @StopReason int mStopReason = StopReason.STOP_UNKNOWN;
private final Bundle mInteractiveBroadcastOption;
private CountDownTimer mCountDownTimer = null;
private final Executor mMainExecutor;
@@ -83,7 +85,7 @@
new UserTracker.Callback() {
@Override
public void onUserChanged(int newUser, @NonNull Context userContext) {
- stopRecording();
+ stopRecording(StopReason.STOP_USER_SWITCH);
}
};
@@ -240,9 +242,11 @@
}
/**
- * Stop the recording
+ * Stop the recording and sets the stop reason to be used by the RecordingService
+ * @param stopReason the method of the recording stopped (i.e. QS tile, status bar chip, etc.)
*/
- public void stopRecording() {
+ public void stopRecording(@StopReason int stopReason) {
+ mStopReason = stopReason;
try {
if (mStopIntent != null) {
mRecordingControllerLogger.logRecordingStopped();
@@ -277,6 +281,10 @@
}
}
+ public @StopReason int getStopReason() {
+ return mStopReason;
+ }
+
@Override
public void addCallback(@NonNull RecordingStateChangeCallback listener) {
mListeners.add(listener);
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 8c207d1..f7b5271 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -26,6 +26,7 @@
import android.content.Intent;
import android.graphics.drawable.Icon;
import android.media.MediaRecorder;
+import android.media.projection.StopReason;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
@@ -78,6 +79,7 @@
private static final String EXTRA_SHOW_TAPS = "extra_showTaps";
private static final String EXTRA_CAPTURE_TARGET = "extra_captureTarget";
private static final String EXTRA_DISPLAY_ID = "extra_displayId";
+ private static final String EXTRA_STOP_REASON = "extra_stopReason";
protected static final String ACTION_START = "com.android.systemui.screenrecord.START";
protected static final String ACTION_SHOW_START_NOTIF =
@@ -242,7 +244,8 @@
// Check user ID - we may be getting a stop intent after user switch, in which case
// we want to post the notifications for that user, which is NOT current user
int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, USER_ID_NOT_SPECIFIED);
- stopService(userId);
+ int stopReason = intent.getIntExtra(EXTRA_STOP_REASON, mController.getStopReason());
+ stopService(userId, stopReason);
break;
case ACTION_SHARE:
@@ -486,11 +489,11 @@
getTag(), notificationIdForGroup, groupNotif, currentUser);
}
- private void stopService() {
- stopService(USER_ID_NOT_SPECIFIED);
+ private void stopService(@StopReason int stopReason) {
+ stopService(USER_ID_NOT_SPECIFIED, stopReason);
}
- private void stopService(int userId) {
+ private void stopService(int userId, @StopReason int stopReason) {
if (userId == USER_ID_NOT_SPECIFIED) {
userId = mUserContextTracker.getUserContext().getUserId();
}
@@ -499,7 +502,7 @@
setTapsVisible(mOriginalShowTaps);
try {
if (getRecorder() != null) {
- getRecorder().end();
+ getRecorder().end(stopReason);
}
saveRecording(userId);
} catch (RuntimeException exception) {
@@ -598,7 +601,8 @@
* @return
*/
protected Intent getNotificationIntent(Context context) {
- return new Intent(context, this.getClass()).setAction(ACTION_STOP_NOTIF);
+ return new Intent(context, this.getClass()).setAction(ACTION_STOP_NOTIF)
+ .putExtra(EXTRA_STOP_REASON, StopReason.STOP_HOST_APP);
}
private Intent getShareIntent(Context context, Uri path) {
@@ -610,14 +614,17 @@
@Override
public void onInfo(MediaRecorder mr, int what, int extra) {
Log.d(getTag(), "Media recorder info: " + what);
- onStartCommand(getStopIntent(this), 0, 0);
+ // Stop due to record reaching size limits so log as stopping due to error
+ Intent stopIntent = getStopIntent(this);
+ stopIntent.putExtra(EXTRA_STOP_REASON, StopReason.STOP_ERROR);
+ onStartCommand(stopIntent, 0, 0);
}
@Override
- public void onStopped() {
+ public void onStopped(@StopReason int stopReason) {
if (mController.isRecording()) {
Log.d(getTag(), "Stopping recording because the system requested the stop");
- stopService();
+ stopService(stopReason);
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
index 2ca0621..f4455bf 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenMediaRecorder.java
@@ -41,6 +41,7 @@
import android.media.projection.IMediaProjectionManager;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
+import android.media.projection.StopReason;
import android.net.Uri;
import android.os.Handler;
import android.os.IBinder;
@@ -300,7 +301,7 @@
/**
* End screen recording, throws an exception if stopping recording failed
*/
- void end() throws IOException {
+ void end(@StopReason int stopReason) throws IOException {
Closer closer = new Closer();
// MediaRecorder might throw RuntimeException if stopped immediately after starting
@@ -309,7 +310,17 @@
closer.register(mMediaRecorder::release);
closer.register(mInputSurface::release);
closer.register(mVirtualDisplay::release);
- closer.register(mMediaProjection::stop);
+ closer.register(() -> {
+ if (stopReason == StopReason.STOP_UNKNOWN) {
+ // Attempt to call MediaProjection#stop() even if it might have already been called.
+ // If projection has already been stopped, then nothing will happen. Else, stop
+ // will be logged as a manually requested stop from host app.
+ mMediaProjection.stop();
+ } else {
+ // In any other case, the stop reason is related to the recorder, so pass it on here
+ mMediaProjection.stop(stopReason);
+ }
+ });
closer.register(this::stopInternalAudioRecording);
closer.close();
@@ -323,7 +334,7 @@
@Override
public void onStop() {
Log.d(TAG, "The system notified about stopping the projection");
- mListener.onStopped();
+ mListener.onStopped(StopReason.STOP_UNKNOWN);
}
private void stopInternalAudioRecording() {
@@ -453,7 +464,7 @@
* For example, this might happen when doing partial screen sharing of an app
* and the app that is being captured is closed.
*/
- void onStopped();
+ void onStopped(@StopReason int stopReason);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
index 9eeb3b9..b6b8ffa 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/data/repository/ScreenRecordRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.screenrecord.data.repository
+import android.media.projection.StopReason
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.screenrecord.RecordingController
@@ -41,7 +42,7 @@
val screenRecordState: Flow<ScreenRecordModel>
/** Stops the recording. */
- suspend fun stopRecording()
+ suspend fun stopRecording(@StopReason stopReason: Int)
}
@SysUISingleton
@@ -95,7 +96,7 @@
}
}
- override suspend fun stopRecording() {
- withContext(bgCoroutineContext) { recordingController.stopRecording() }
+ override suspend fun stopRecording(@StopReason stopReason: Int) {
+ withContext(bgCoroutineContext) { recordingController.stopRecording(stopReason) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
index b3dbf29..229cef9 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/casttootherdevice/domain/interactor/MediaRouterChipInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.chips.casttootherdevice.domain.interactor
+import android.media.projection.StopReason
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.log.LogBuffer
@@ -65,7 +66,9 @@
/** Stops the currently active MediaRouter cast. */
fun stopCasting() {
- activeCastDevice.value?.let { mediaRouterRepository.stopCasting(it) }
+ activeCastDevice.value?.let {
+ mediaRouterRepository.stopCasting(it, StopReason.STOP_PRIVACY_CHIP)
+ }
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
index f5952f4..0b5e669 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractor.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.chips.screenrecord.domain.interactor
+import android.media.projection.StopReason
import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -140,7 +141,7 @@
/** Stops the recording. */
fun stopRecording() {
- scope.launch { screenRecordRepository.stopRecording() }
+ scope.launch { screenRecordRepository.stopRecording(StopReason.STOP_PRIVACY_CHIP) }
}
companion object {
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java
index a3dcc3b..ece5a3f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastController.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.policy;
+import android.media.projection.StopReason;
import com.android.systemui.Dumpable;
import com.android.systemui.statusbar.policy.CastController.Callback;
@@ -26,7 +27,7 @@
void setCurrentUserId(int currentUserId);
List<CastDevice> getCastDevices();
void startCasting(CastDevice device);
- void stopCasting(CastDevice device);
+ void stopCasting(CastDevice device, @StopReason int stopReason);
/**
* @return whether we have a connected device.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
index 52f80fb..ab20850 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/CastControllerImpl.java
@@ -185,13 +185,13 @@
}
@Override
- public void stopCasting(CastDevice device) {
+ public void stopCasting(CastDevice device, @StopReason int stopReason) {
final boolean isProjection = device.getTag() instanceof MediaProjectionInfo;
mLogger.logStopCasting(isProjection);
if (isProjection) {
final MediaProjectionInfo projection = (MediaProjectionInfo) device.getTag();
if (Objects.equals(mProjectionManager.getActiveProjectionInfo(), projection)) {
- mProjectionManager.stopActiveProjection(StopReason.STOP_QS_TILE);
+ mProjectionManager.stopActiveProjection(stopReason);
} else {
mLogger.logStopCastingNoProjection(projection);
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
index 094ec39..203a157 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/VolumeDialogPlugin.kt
@@ -16,19 +16,31 @@
package com.android.systemui.volume.dialog
+import android.content.Context
+import android.media.AudioManager
import com.android.app.tracing.coroutines.coroutineScopeTraced
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.plugins.VolumeDialog
+import com.android.systemui.volume.SafetyWarningDialog
import com.android.systemui.volume.dialog.dagger.VolumeDialogPluginComponent
+import com.android.systemui.volume.dialog.ui.viewmodel.VolumeDialogPluginViewModel
import javax.inject.Inject
+import kotlin.coroutines.resume
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.mapLatest
+import kotlinx.coroutines.suspendCancellableCoroutine
+@OptIn(ExperimentalCoroutinesApi::class)
class VolumeDialogPlugin
@Inject
constructor(
@Application private val applicationCoroutineScope: CoroutineScope,
+ private val context: Context,
+ private val audioManager: AudioManager,
private val volumeDialogPluginComponentFactory: VolumeDialogPluginComponent.Factory,
) : VolumeDialog {
@@ -41,14 +53,39 @@
coroutineScopeTraced("[Volume]plugin") {
pluginComponent =
volumeDialogPluginComponentFactory.create(this).also {
- it.viewModel().launchVolumeDialog()
+ bindPlugin(it.viewModel())
}
}
}
}
+ private fun CoroutineScope.bindPlugin(viewModel: VolumeDialogPluginViewModel) {
+ viewModel.launchVolumeDialog()
+
+ viewModel.isShowingSafetyWarning
+ .mapLatest { isShowingSafetyWarning ->
+ if (isShowingSafetyWarning) {
+ showSafetyWarningVisibility { viewModel.onSafetyWarningDismissed() }
+ }
+ }
+ .launchIn(this)
+ }
+
override fun destroy() {
job?.cancel()
pluginComponent = null
}
+
+ private suspend fun showSafetyWarningVisibility(onDismissed: () -> Unit) =
+ suspendCancellableCoroutine { continuation ->
+ val dialog =
+ object : SafetyWarningDialog(context, audioManager) {
+ override fun cleanUp() {
+ onDismissed()
+ continuation.resume(Unit)
+ }
+ }
+ dialog.show()
+ continuation.invokeOnCancellation { dialog.dismiss() }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractor.kt
new file mode 100644
index 0000000..f707d67
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractor.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 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.systemui.volume.dialog.domain.interactor
+
+import android.media.AudioManager
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogSafetyWarningModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+
+private const val VISIBLE_FLAGS = AudioManager.FLAG_SHOW_UI or AudioManager.FLAG_SHOW_UI_WARNINGS
+
+@VolumeDialogPluginScope
+class VolumeDialogSafetyWarningInteractor
+@Inject
+constructor(
+ private val stateInteractor: VolumeDialogStateInteractor,
+ visibilityInteractor: VolumeDialogVisibilityInteractor,
+) {
+
+ val isShowingSafetyWarning: Flow<Boolean> =
+ stateInteractor.volumeDialogState.map {
+ when (it.isShowingSafetyWarning) {
+ is VolumeDialogSafetyWarningModel.Visible ->
+ if (it.isShowingSafetyWarning.flags and VISIBLE_FLAGS == 0) {
+ visibilityInteractor.dialogVisibility.first() is
+ VolumeDialogVisibilityModel.Visible
+ } else {
+ true
+ }
+ is VolumeDialogSafetyWarningModel.Invisible -> false
+ }
+ }
+
+ fun onSafetyWarningDismissed() {
+ stateInteractor.setSafetyWarning(VolumeDialogSafetyWarningModel.Invisible)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
index 5c7289b..51e7924 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogStateInteractor.kt
@@ -23,6 +23,7 @@
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
import com.android.systemui.volume.dialog.data.repository.VolumeDialogStateRepository
import com.android.systemui.volume.dialog.domain.model.VolumeDialogEventModel
+import com.android.systemui.volume.dialog.shared.model.VolumeDialogSafetyWarningModel
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStateModel
import com.android.systemui.volume.dialog.shared.model.VolumeDialogStreamModel
import javax.inject.Inject
@@ -61,6 +62,9 @@
oldState.copy(shouldShowA11ySlider = event.showA11yStream)
}
}
+ is VolumeDialogEventModel.ShowSafetyWarning -> {
+ setSafetyWarning(VolumeDialogSafetyWarningModel.Visible(event.flags))
+ }
else -> {
// do nothing
}
@@ -72,6 +76,10 @@
val volumeDialogState: Flow<VolumeDialogStateModel> = volumeDialogStateRepository.state
+ fun setSafetyWarning(model: VolumeDialogSafetyWarningModel) {
+ volumeDialogStateRepository.updateState { it.copy(isShowingSafetyWarning = model) }
+ }
+
/** Returns a copy of [model] filled with the values from [VolumeDialogController.State]. */
private fun VolumeDialogController.State.copyIntoModel(
model: VolumeDialogStateModel
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogSafetyWarningModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogSafetyWarningModel.kt
new file mode 100644
index 0000000..39fc222
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogSafetyWarningModel.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.systemui.volume.dialog.shared.model
+
+/** Models current Volume Safety Dialog state. */
+sealed interface VolumeDialogSafetyWarningModel {
+
+ /** Volume Safety Dialog is visible and has been shown with the [flags]. */
+ data class Visible(val flags: Int) : VolumeDialogSafetyWarningModel
+
+ /** Volume Safety Dialog is invisible. */
+ data object Invisible : VolumeDialogSafetyWarningModel
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogStateModel.kt
index 1792b99..838006d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/shared/model/VolumeDialogStateModel.kt
@@ -21,6 +21,8 @@
/** Models a state of the Volume Dialog. */
data class VolumeDialogStateModel(
val shouldShowA11ySlider: Boolean = false,
+ val isShowingSafetyWarning: VolumeDialogSafetyWarningModel =
+ VolumeDialogSafetyWarningModel.Invisible,
val streamModels: Map<Int, VolumeDialogStreamModel> = mapOf(),
val ringerModeInternal: Int = 0,
val ringerModeExternal: Int = 0,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
index ff525f46..9bab1b0 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogPluginViewModel.kt
@@ -18,14 +18,17 @@
import com.android.systemui.volume.Events
import com.android.systemui.volume.dialog.VolumeDialog
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPlugin
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogPluginScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogSafetyWarningInteractor
import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogVisibilityInteractor
import com.android.systemui.volume.dialog.shared.VolumeDialogLogger
import com.android.systemui.volume.dialog.shared.model.VolumeDialogVisibilityModel
import javax.inject.Inject
import javax.inject.Provider
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
@@ -34,29 +37,35 @@
class VolumeDialogPluginViewModel
@Inject
constructor(
+ @VolumeDialogPlugin private val coroutineScope: CoroutineScope,
private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
+ private val dialogSafetyWarningInteractor: VolumeDialogSafetyWarningInteractor,
private val volumeDialogProvider: Provider<VolumeDialog>,
private val logger: VolumeDialogLogger,
) {
- suspend fun launchVolumeDialog() {
- coroutineScope {
- dialogVisibilityInteractor.dialogVisibility
- .mapLatest { visibilityModel ->
- with(visibilityModel) {
- if (this is VolumeDialogVisibilityModel.Visible) {
- showDialog()
- Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked)
- logger.onShow(reason)
- }
- if (this is VolumeDialogVisibilityModel.Dismissed) {
- Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason)
- logger.onDismiss(reason)
- }
+ fun launchVolumeDialog() {
+ dialogVisibilityInteractor.dialogVisibility
+ .mapLatest { visibilityModel ->
+ with(visibilityModel) {
+ if (this is VolumeDialogVisibilityModel.Visible) {
+ showDialog()
+ Events.writeEvent(Events.EVENT_SHOW_DIALOG, reason, keyguardLocked)
+ logger.onShow(reason)
+ }
+ if (this is VolumeDialogVisibilityModel.Dismissed) {
+ Events.writeEvent(Events.EVENT_DISMISS_DIALOG, reason)
+ logger.onDismiss(reason)
}
}
- .launchIn(this)
- }
+ }
+ .launchIn(coroutineScope)
+ }
+
+ val isShowingSafetyWarning: Flow<Boolean> = dialogSafetyWarningInteractor.isShowingSafetyWarning
+
+ fun onSafetyWarningDismissed() {
+ dialogSafetyWarningInteractor.onSafetyWarningDismissed()
}
private fun showDialog() {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
index 7a6ede4..b20dffb 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/viewmodel/VolumeDialogViewModel.kt
@@ -44,9 +44,9 @@
@Inject
constructor(
private val context: Context,
- dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
+ private val dialogVisibilityInteractor: VolumeDialogVisibilityInteractor,
volumeDialogSlidersInteractor: VolumeDialogSlidersInteractor,
- volumeDialogStateInteractor: VolumeDialogStateInteractor,
+ private val volumeDialogStateInteractor: VolumeDialogStateInteractor,
devicePostureController: DevicePostureController,
configurationController: ConfigurationController,
) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
index d88b758..266cb51 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/notetask/NoteTaskInitializerTest.kt
@@ -181,11 +181,9 @@
@Test
@EnableFlags(com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER)
- fun handlesShortcut_metaCtrlN() {
+ fun handlesShortcut_keyGestureTypeOpenNotes() {
val gestureEvent =
KeyGestureEvent.Builder()
- .setKeycodes(intArrayOf(KeyEvent.KEYCODE_N))
- .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON)
.setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_OPEN_NOTES)
.setAction(KeyGestureEvent.ACTION_GESTURE_COMPLETE)
.build()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
index afff485..a17f100 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingControllerTest.java
@@ -36,6 +36,7 @@
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.media.projection.StopReason;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -154,7 +155,7 @@
PendingIntent stopIntent = Mockito.mock(PendingIntent.class);
mController.startCountdown(0, 0, startIntent, stopIntent);
- mController.stopRecording();
+ mController.stopRecording(StopReason.STOP_UNKNOWN);
assertFalse(mController.isStarting());
assertFalse(mController.isRecording());
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt
index 8aa7a03..d5637cb 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/mediarouter/data/repository/FakeMediaRouterRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.mediarouter.data.repository
+import android.media.projection.StopReason
import com.android.systemui.statusbar.policy.CastDevice
import kotlinx.coroutines.flow.MutableStateFlow
@@ -25,7 +26,7 @@
var lastStoppedDevice: CastDevice? = null
private set
- override fun stopCasting(device: CastDevice) {
+ override fun stopCasting(device: CastDevice, @StopReason stopReason: Int) {
lastStoppedDevice = device
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
index 30b4763..4c9e174 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/screenrecord/data/repository/FakeScreenRecordRepository.kt
@@ -16,6 +16,7 @@
package com.android.systemui.screenrecord.data.repository
+import android.media.projection.StopReason
import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import kotlinx.coroutines.flow.MutableStateFlow
@@ -25,7 +26,7 @@
var stopRecordingInvoked = false
- override suspend fun stopRecording() {
+ override suspend fun stopRecording(@StopReason stopReason: Int) {
stopRecordingInvoked = true
}
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt
index 2df0c7a5..da6b2ae 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeCastController.kt
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.policy
+import android.media.projection.StopReason
import java.io.PrintWriter
class FakeCastController : CastController {
@@ -45,7 +46,7 @@
override fun startCasting(device: CastDevice?) {}
- override fun stopCasting(device: CastDevice?) {
+ override fun stopCasting(device: CastDevice?, @StopReason stopReason: Int) {
lastStoppedDevice = device
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java
index 2249bc0..857dc85 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/LeakCheckerCastController.java
@@ -16,6 +16,7 @@
package com.android.systemui.utils.leaks;
+import android.media.projection.StopReason;
import android.testing.LeakCheck;
import com.android.systemui.statusbar.policy.CastController;
@@ -51,7 +52,7 @@
}
@Override
- public void stopCasting(CastDevice device) {
+ public void stopCasting(CastDevice device, @StopReason int stopReason) {
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorKosmos.kt
new file mode 100644
index 0000000..ddd0014
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/dialog/domain/interactor/VolumeDialogSafetyWarningInteractorKosmos.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 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.systemui.volume.dialog.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.volumeDialogSafetyWarningInteractor: VolumeDialogSafetyWarningInteractor by
+ Kosmos.Fixture {
+ VolumeDialogSafetyWarningInteractor(
+ volumeDialogStateInteractor,
+ volumeDialogVisibilityInteractor,
+ )
+ }
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index c4e1036..e10bdaa 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -765,6 +765,7 @@
private float mBacklightMinimum = Float.NaN;
private float mBacklightMaximum = Float.NaN;
private float mBrightnessDefault = Float.NaN;
+ private float mBrightnessDim = Float.NaN;
private float mBrightnessRampFastDecrease = Float.NaN;
private float mBrightnessRampFastIncrease = Float.NaN;
private float mBrightnessRampSlowDecrease = Float.NaN;
@@ -1282,6 +1283,24 @@
}
/**
+ * Return the minimum brightness on a scale of 0.0f - 1.0f
+ *
+ * @return minimum brightness
+ */
+ public float getBrightnessMinimum() {
+ return getBrightnessFromBacklight(mBacklightMinimum);
+ }
+
+ /**
+ * Return the maximum brightness on a scale of 0.0f - 1.0f
+ *
+ * @return maximum brightness
+ */
+ public float getBrightnessMaximum() {
+ return getBrightnessFromBacklight(mBacklightMaximum);
+ }
+
+ /**
* Return the default brightness on a scale of 0.0f - 1.0f
*
* @return default brightness
@@ -1290,6 +1309,15 @@
return mBrightnessDefault;
}
+ /**
+ * Return the dim brightness on a scale of 0.0f - 1.0f
+ *
+ * @return dim brightness
+ */
+ public float getBrightnessDim() {
+ return mBrightnessDim;
+ }
+
public float getBrightnessRampFastDecrease() {
return mBrightnessRampFastDecrease;
}
@@ -1689,6 +1717,7 @@
+ ", mBacklightMinimum=" + mBacklightMinimum
+ ", mBacklightMaximum=" + mBacklightMaximum
+ ", mBrightnessDefault=" + mBrightnessDefault
+ + ", mBrightnessDim=" + mBrightnessDim
+ ", mQuirks=" + mQuirks
+ "\n"
+ "mLuxThrottlingData=" + mLuxThrottlingData
@@ -1906,6 +1935,7 @@
mBacklightMinimum = PowerManager.BRIGHTNESS_MIN;
mBacklightMaximum = PowerManager.BRIGHTNESS_MAX;
mBrightnessDefault = BRIGHTNESS_DEFAULT;
+ mBrightnessDim = PowerManager.BRIGHTNESS_INVALID;
mBrightnessRampFastDecrease = PowerManager.BRIGHTNESS_MAX;
mBrightnessRampFastIncrease = PowerManager.BRIGHTNESS_MAX;
mBrightnessRampSlowDecrease = PowerManager.BRIGHTNESS_MAX;
@@ -2003,6 +2033,15 @@
mBacklightMinimum = min;
mBacklightMaximum = max;
}
+ final float dim = mContext.getResources().getFloat(com.android.internal.R.dimen
+ .config_screenBrightnessDimFloat);
+ if (dim == INVALID_BRIGHTNESS_IN_CONFIG) {
+ mBrightnessDim = BrightnessSynchronizer.brightnessIntToFloat(
+ mContext.getResources().getInteger(com.android.internal.R.integer
+ .config_screenBrightnessDim));
+ } else {
+ mBrightnessDim = dim;
+ }
}
private void loadBrightnessMap(DisplayConfiguration config) {
diff --git a/services/core/java/com/android/server/display/DisplayDeviceInfo.java b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
index 1c1bdad..3aaf4f6 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceInfo.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceInfo.java
@@ -477,6 +477,7 @@
public float brightnessMinimum;
public float brightnessMaximum;
public float brightnessDefault;
+ public float brightnessDim;
// NaN means unsupported
public float hdrSdrRatio = Float.NaN;
@@ -561,8 +562,8 @@
|| !Objects.equals(ownerPackageName, other.ownerPackageName)
|| !BrightnessSynchronizer.floatEquals(brightnessMinimum, other.brightnessMinimum)
|| !BrightnessSynchronizer.floatEquals(brightnessMaximum, other.brightnessMaximum)
- || !BrightnessSynchronizer.floatEquals(brightnessDefault,
- other.brightnessDefault)
+ || !BrightnessSynchronizer.floatEquals(brightnessDefault, other.brightnessDefault)
+ || !BrightnessSynchronizer.floatEquals(brightnessDim, other.brightnessDim)
|| !Objects.equals(roundedCorners, other.roundedCorners)
|| installOrientation != other.installOrientation
|| !Objects.equals(displayShape, other.displayShape)
@@ -618,6 +619,7 @@
brightnessMinimum = other.brightnessMinimum;
brightnessMaximum = other.brightnessMaximum;
brightnessDefault = other.brightnessDefault;
+ brightnessDim = other.brightnessDim;
hdrSdrRatio = other.hdrSdrRatio;
roundedCorners = other.roundedCorners;
installOrientation = other.installOrientation;
@@ -672,6 +674,7 @@
sb.append(", brightnessMinimum ").append(brightnessMinimum);
sb.append(", brightnessMaximum ").append(brightnessMaximum);
sb.append(", brightnessDefault ").append(brightnessDefault);
+ sb.append(", brightnessDim ").append(brightnessDim);
sb.append(", hdrSdrRatio ").append(hdrSdrRatio);
if (roundedCorners != null) {
sb.append(", roundedCorners ").append(roundedCorners);
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index 0b8f7d5..d37dd30 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -812,9 +812,10 @@
// The display is trusted since it is created by system.
mInfo.flags |= DisplayDeviceInfo.FLAG_TRUSTED;
- mInfo.brightnessMinimum = PowerManager.BRIGHTNESS_MIN;
- mInfo.brightnessMaximum = PowerManager.BRIGHTNESS_MAX;
+ mInfo.brightnessMinimum = getDisplayDeviceConfig().getBrightnessMinimum();
+ mInfo.brightnessMaximum = getDisplayDeviceConfig().getBrightnessMaximum();
mInfo.brightnessDefault = getDisplayDeviceConfig().getBrightnessDefault();
+ mInfo.brightnessDim = getDisplayDeviceConfig().getBrightnessDim();
mInfo.hdrSdrRatio = mCurrentHdrSdrRatio;
}
return mInfo;
diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java
index 8546598..1de9c95 100644
--- a/services/core/java/com/android/server/display/LogicalDisplay.java
+++ b/services/core/java/com/android/server/display/LogicalDisplay.java
@@ -548,6 +548,7 @@
mBaseDisplayInfo.brightnessMinimum = deviceInfo.brightnessMinimum;
mBaseDisplayInfo.brightnessMaximum = deviceInfo.brightnessMaximum;
mBaseDisplayInfo.brightnessDefault = deviceInfo.brightnessDefault;
+ mBaseDisplayInfo.brightnessDim = deviceInfo.brightnessDim;
mBaseDisplayInfo.hdrSdrRatio = deviceInfo.hdrSdrRatio;
mBaseDisplayInfo.roundedCorners = deviceInfo.roundedCorners;
mBaseDisplayInfo.installOrientation = deviceInfo.installOrientation;
diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
index 6ae58c4..836f4ed 100644
--- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java
@@ -340,6 +340,7 @@
private boolean mIsWindowManagerMirroring;
private final DisplayCutout mDisplayCutout;
private final float mDefaultBrightness;
+ private final float mDimBrightness;
private float mCurrentBrightness;
private final IBrightnessListener mBrightnessListener;
@@ -359,6 +360,7 @@
mRequestedRefreshRate = virtualDisplayConfig.getRequestedRefreshRate();
mDisplayCutout = virtualDisplayConfig.getDisplayCutout();
mDefaultBrightness = virtualDisplayConfig.getDefaultBrightness();
+ mDimBrightness = virtualDisplayConfig.getDimBrightness();
mCurrentBrightness = PowerManager.BRIGHTNESS_INVALID;
mBrightnessListener = virtualDisplayConfig.getBrightnessListener();
mMode = createMode(mWidth, mHeight, getRefreshRate());
@@ -645,6 +647,7 @@
mInfo.brightnessMinimum = PowerManager.BRIGHTNESS_MIN;
mInfo.brightnessMaximum = PowerManager.BRIGHTNESS_MAX;
mInfo.brightnessDefault = mDefaultBrightness;
+ mInfo.brightnessDim = mDimBrightness;
mInfo.ownerUid = mOwnerUid;
mInfo.ownerPackageName = mOwnerPackageName;
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 7ccab05..ceade37 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -267,7 +267,7 @@
}
}
- modifiers.add(new DisplayDimModifier(context));
+ modifiers.add(new DisplayDimModifier(data.mDisplayId, context));
modifiers.add(new BrightnessLowPowerModeModifier());
if (flags.isEvenDimmerEnabled() && data.mDisplayDeviceConfig.isEvenDimmerAvailable()) {
modifiers.add(new BrightnessLowLuxModifier(handler, listener, context,
diff --git a/services/core/java/com/android/server/display/brightness/clamper/DisplayDimModifier.java b/services/core/java/com/android/server/display/brightness/clamper/DisplayDimModifier.java
index ab880bf..0237af3 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/DisplayDimModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/DisplayDimModifier.java
@@ -38,12 +38,12 @@
// mScreenBrightnessDimConfig.
private final float mScreenBrightnessMinimumDimAmount;
- DisplayDimModifier(Context context) {
+ DisplayDimModifier(int displayId, Context context) {
PowerManager pm = Objects.requireNonNull(context.getSystemService(PowerManager.class));
Resources resources = context.getResources();
mScreenBrightnessDimConfig = BrightnessUtils.clampAbsoluteBrightness(
- pm.getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM));
+ pm.getBrightnessConstraint(displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM));
mScreenBrightnessMinimumDimAmount = resources.getFloat(
R.dimen.config_screenBrightnessMinimumDimAmountFloat);
}
diff --git a/services/core/java/com/android/server/pm/InstallDependencyHelper.java b/services/core/java/com/android/server/pm/InstallDependencyHelper.java
index 13aab11..673a102 100644
--- a/services/core/java/com/android/server/pm/InstallDependencyHelper.java
+++ b/services/core/java/com/android/server/pm/InstallDependencyHelper.java
@@ -32,11 +32,13 @@
import android.content.pm.dependencyinstaller.IDependencyInstallerCallback;
import android.content.pm.dependencyinstaller.IDependencyInstallerService;
import android.content.pm.parsing.PackageLite;
+import android.os.Binder;
import android.os.Handler;
import android.os.OutcomeReceiver;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Slog;
@@ -63,12 +65,11 @@
private final Context mContext;
private final SharedLibrariesImpl mSharedLibraries;
private final PackageInstallerService mPackageInstallerService;
- private final Object mRemoteServiceLock = new Object();
@GuardedBy("mTrackers")
private final List<DependencyInstallTracker> mTrackers = new ArrayList<>();
-
- @GuardedBy("mRemoteServiceLock")
- private ServiceConnector<IDependencyInstallerService> mRemoteService = null;
+ @GuardedBy("mRemoteServices")
+ private final ArrayMap<Integer, ServiceConnector<IDependencyInstallerService>> mRemoteServices =
+ new ArrayMap<>();
InstallDependencyHelper(Context context, SharedLibrariesImpl sharedLibraries,
PackageInstallerService packageInstallerService) {
@@ -97,13 +98,18 @@
if (missing.isEmpty()) {
if (DEBUG) {
- Slog.d(TAG, "No missing dependency for " + pkg);
+ Slog.d(TAG, "No missing dependency for " + pkg.getPackageName());
}
// No need for dependency resolution. Move to installation directly.
callback.onResult(null);
return;
}
+ if (DEBUG) {
+ Slog.d(TAG, "Missing dependencies found for pkg: " + pkg.getPackageName()
+ + " user: " + userId);
+ }
+
if (!bindToDependencyInstallerIfNeeded(userId, handler, snapshot)) {
onError(callback, "Dependency Installer Service not found");
return;
@@ -112,8 +118,8 @@
IDependencyInstallerCallback serviceCallback =
new DependencyInstallerCallbackCallOnce(handler, callback, userId);
boolean scheduleSuccess;
- synchronized (mRemoteServiceLock) {
- scheduleSuccess = mRemoteService.run(service -> {
+ synchronized (mRemoteServices) {
+ scheduleSuccess = mRemoteServices.get(userId).run(service -> {
service.onDependenciesRequired(missing,
new DependencyInstallerCallback(serviceCallback.asBinder()));
});
@@ -149,15 +155,17 @@
private boolean bindToDependencyInstallerIfNeeded(int userId, Handler handler,
Computer snapshot) {
- synchronized (mRemoteServiceLock) {
- if (mRemoteService != null) {
+ synchronized (mRemoteServices) {
+ if (mRemoteServices.containsKey(userId)) {
if (DEBUG) {
- Slog.i(TAG, "DependencyInstallerService already bound");
+ Slog.i(TAG, "DependencyInstallerService for user " + userId + " already bound");
}
return true;
}
}
+ Slog.i(TAG, "Attempting to bind to Dependency Installer Service for user " + userId);
+
RoleManager roleManager = mContext.getSystemService(RoleManager.class);
if (roleManager == null) {
Slog.w(TAG, "Cannot find RoleManager system service");
@@ -166,7 +174,7 @@
List<String> holders = roleManager.getRoleHoldersAsUser(
ROLE_SYSTEM_DEPENDENCY_INSTALLER, UserHandle.of(userId));
if (holders.isEmpty()) {
- Slog.w(TAG, "No holders of ROLE_SYSTEM_DEPENDENCY_INSTALLER found");
+ Slog.w(TAG, "No holders of ROLE_SYSTEM_DEPENDENCY_INSTALLER found for user " + userId);
return false;
}
@@ -178,6 +186,8 @@
/*includeInstantApps*/ false, /*resolveForStart*/ false);
if (resolvedIntents.isEmpty()) {
+ Slog.w(TAG, "No package holding ROLE_SYSTEM_DEPENDENCY_INSTALLER found for user "
+ + userId);
return false;
}
@@ -206,13 +216,14 @@
};
- synchronized (mRemoteServiceLock) {
+ synchronized (mRemoteServices) {
// Some other thread managed to connect to the service first
- if (mRemoteService != null) {
+ if (mRemoteServices.containsKey(userId)) {
return true;
}
- mRemoteService = serviceConnector;
- mRemoteService.setServiceLifecycleCallbacks(
+ mRemoteServices.put(userId, serviceConnector);
+ // Block the lock until we connect to the service
+ serviceConnector.setServiceLifecycleCallbacks(
new ServiceConnector.ServiceLifecycleCallbacks<>() {
@Override
public void onDisconnected(@NonNull IDependencyInstallerService service) {
@@ -228,17 +239,18 @@
}
private void destroy() {
- synchronized (mRemoteServiceLock) {
- if (mRemoteService != null) {
- mRemoteService.unbind();
- mRemoteService = null;
+ synchronized (mRemoteServices) {
+ if (mRemoteServices.containsKey(userId)) {
+ mRemoteServices.get(userId).unbind();
+ mRemoteServices.remove(userId);
}
}
}
});
- AndroidFuture<IDependencyInstallerService> unusedFuture = mRemoteService.connect();
+ AndroidFuture<IDependencyInstallerService> unusedFuture = serviceConnector.connect();
}
+ Slog.i(TAG, "Successfully bound to Dependency Installer Service for user " + userId);
return true;
}
@@ -318,30 +330,40 @@
Slog.d(TAG, "onAllDependenciesResolved started");
}
- // Before creating any tracker, validate the arguments
- ArraySet<Integer> validSessionIds = validateSessionIds(sessionIds);
+ try {
+ // Before creating any tracker, validate the arguments
+ ArraySet<Integer> validSessionIds = validateSessionIds(sessionIds);
- if (validSessionIds.isEmpty()) {
- mCallback.onResult(null);
- return;
- }
-
- // Create a tracker now if there are any pending sessions remaining.
- DependencyInstallTracker tracker = new DependencyInstallTracker(
- mCallback, validSessionIds);
- synchronized (mTrackers) {
- mTrackers.add(tracker);
- }
-
- // By the time the tracker was created, some of the sessions in validSessionIds
- // could have finished. Avoid waiting for them indefinitely.
- for (int sessionId : validSessionIds) {
- SessionInfo sessionInfo = mPackageInstallerService.getSessionInfo(sessionId);
-
- // Don't wait for sessions that finished already
- if (sessionInfo == null) {
- notifySessionComplete(sessionId, /*success=*/ true);
+ if (validSessionIds.isEmpty()) {
+ mCallback.onResult(null);
+ return;
}
+
+ // Create a tracker now if there are any pending sessions remaining.
+ DependencyInstallTracker tracker = new DependencyInstallTracker(
+ mCallback, validSessionIds);
+ synchronized (mTrackers) {
+ mTrackers.add(tracker);
+ }
+
+ // By the time the tracker was created, some of the sessions in validSessionIds
+ // could have finished. Avoid waiting for them indefinitely.
+ for (int sessionId : validSessionIds) {
+ SessionInfo sessionInfo = mPackageInstallerService.getSessionInfo(sessionId);
+
+ // Don't wait for sessions that finished already
+ if (sessionInfo == null) {
+ Binder.withCleanCallingIdentity(() -> {
+ notifySessionComplete(sessionId, /*success=*/ true);
+ });
+ }
+ }
+ } catch (Exception e) {
+ // Allow calling the callback again
+ synchronized (this) {
+ mDependencyInstallerCallbackInvoked = false;
+ }
+ throw e;
}
}
@@ -354,7 +376,10 @@
}
mDependencyInstallerCallbackInvoked = true;
}
- onError(mCallback, "Failed to resolve all dependencies automatically");
+
+ Binder.withCleanCallingIdentity(() -> {
+ onError(mCallback, "Failed to resolve all dependencies automatically");
+ });
}
private ArraySet<Integer> validateSessionIds(int[] sessionIds) {
@@ -369,7 +394,8 @@
// Continue waiting if session exists and hasn't passed or failed yet.
if (sessionInfo != null) {
if (sessionInfo.isSessionFailed) {
- throwValidationError("Session already finished: " + sessionId);
+ throw new IllegalArgumentException("Session already finished: "
+ + sessionId);
}
// Wait for session to finish install if it's not already successful.
@@ -398,25 +424,17 @@
s -> s.sessionId == sessionId).findFirst().orElse(null);
if (sessionInfo == null) {
- throwValidationError("Failed to find session: " + sessionId);
+ throw new IllegalArgumentException("Failed to find session: " + sessionId);
}
// Historical session must have been successful, otherwise throw IAE.
if (!sessionInfo.isSessionApplied) {
- throwValidationError("Session already finished: " + sessionId);
+ throw new IllegalArgumentException("Session already finished: " + sessionId);
}
}
return validSessionIds;
}
-
- private void throwValidationError(String msg) {
- // Allow client to invoke callback again.
- synchronized (this) {
- mDependencyInstallerCallbackInvoked = false;
- }
- throw new IllegalArgumentException(msg);
- }
}
/**
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 8e73023..7c4d425 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -4426,9 +4426,9 @@
int screenDisplayId = displayId < 0 ? DEFAULT_DISPLAY : displayId;
float minLinearBrightness = mPowerManager.getBrightnessConstraint(
- PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
+ screenDisplayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
float maxLinearBrightness = mPowerManager.getBrightnessConstraint(
- PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
+ screenDisplayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
float linearBrightness = mDisplayManager.getBrightness(screenDisplayId);
float gammaBrightness = BrightnessUtils.convertLinearToGamma(linearBrightness);
diff --git a/services/core/java/com/android/server/power/PowerManagerService.java b/services/core/java/com/android/server/power/PowerManagerService.java
index c573293..36bc0b9 100644
--- a/services/core/java/com/android/server/power/PowerManagerService.java
+++ b/services/core/java/com/android/server/power/PowerManagerService.java
@@ -6099,16 +6099,28 @@
}
}
- public float getBrightnessConstraint(int constraint) {
+ @Override
+ public float getBrightnessConstraint(
+ int displayId, @PowerManager.BrightnessConstraint int constraint) {
+ DisplayInfo info = null;
+ if (android.companion.virtualdevice.flags.Flags.displayPowerManagerApis()
+ && mDisplayManagerInternal != null) {
+ info = mDisplayManagerInternal.getDisplayInfo(displayId);
+ }
switch (constraint) {
case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM:
- return mScreenBrightnessMinimum;
+ return info != null && isValidBrightnessValue(info.brightnessMinimum)
+ ? info.brightnessMinimum : mScreenBrightnessMinimum;
case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM:
- return mScreenBrightnessMaximum;
+ return info != null && isValidBrightnessValue(info.brightnessMaximum)
+ ? info.brightnessMaximum : mScreenBrightnessMaximum;
case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DEFAULT:
- return mScreenBrightnessDefault;
+ return info != null && isValidBrightnessValue(info.brightnessDefault)
+ ? info.brightnessDefault : mScreenBrightnessDefault;
case PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM:
- return mScreenBrightnessDim;
+ return android.companion.virtualdevice.flags.Flags.deviceAwareDisplayPower()
+ && info != null && isValidBrightnessValue(info.brightnessDim)
+ ? info.brightnessDim : mScreenBrightnessDim;
default:
return PowerManager.BRIGHTNESS_INVALID_FLOAT;
}
diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
index 4e79e37..37e8f62 100644
--- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
+++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java
@@ -424,6 +424,7 @@
|| first.brightnessMinimum != second.brightnessMinimum
|| first.brightnessMaximum != second.brightnessMaximum
|| first.brightnessDefault != second.brightnessDefault
+ || first.brightnessDim != second.brightnessDim
|| first.installOrientation != second.installOrientation
|| first.isForceSdr != second.isForceSdr
|| !Objects.equals(first.layoutLimitedRefreshRate, second.layoutLimitedRefreshRate)
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
index b002a1f..241dc10 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java
@@ -16,6 +16,8 @@
package com.android.server.display;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
@@ -327,6 +329,23 @@
}
@Test
+ public void testBrightnessConfigurationFromDisplayDevice() {
+ mDisplayDeviceInfo.brightnessMinimum = 0.12f;
+ mDisplayDeviceInfo.brightnessDim = 0.34f;
+ mDisplayDeviceInfo.brightnessDefault = 0.56f;
+ mDisplayDeviceInfo.brightnessMaximum = 0.78f;
+
+ mLogicalDisplay = new LogicalDisplay(DISPLAY_ID, LAYER_STACK, mDisplayDevice);
+ mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager);
+
+ DisplayInfo info = mLogicalDisplay.getDisplayInfoLocked();
+ assertThat(info.brightnessMinimum).isEqualTo(0.12f);
+ assertThat(info.brightnessDim).isEqualTo(0.34f);
+ assertThat(info.brightnessDefault).isEqualTo(0.56f);
+ assertThat(info.brightnessMaximum).isEqualTo(0.78f);
+ }
+
+ @Test
public void testGetDisplayPosition() {
Point expectedPosition = new Point(0, 0);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
index 3ac7fb0..dbd5c65 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/VirtualDisplayAdapterTest.java
@@ -16,6 +16,8 @@
package com.android.server.display;
+import static com.google.common.truth.Truth.assertThat;
+
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
@@ -25,10 +27,12 @@
import android.hardware.display.VirtualDisplayConfig;
import android.media.projection.IMediaProjection;
import android.os.IBinder;
+import android.os.PowerManager;
import android.os.Process;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.testing.TestableContext;
+import android.view.Display;
import android.view.Surface;
import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -56,6 +60,9 @@
private static final int MAX_DEVICES = 3;
private static final int MAX_DEVICES_PER_PACKAGE = 2;
+ private static final float DEFAULT_BRIGHTNESS = 0.34f;
+ private static final float DIM_BRIGHTNESS = 0.12f;
+
@Rule
public final TestableContext mContext = new TestableContext(
InstrumentationRegistry.getInstrumentation().getContext());
@@ -123,6 +130,64 @@
}
@Test
+ public void testCreateVirtualDisplay_createDisplayDeviceInfoFromDefaults() {
+ VirtualDisplayConfig config = new VirtualDisplayConfig.Builder(
+ "testDisplayName", /* width= */ 640, /* height= */ 480, /* densityDpi= */ 240)
+ .build();
+
+ final String packageName = "testpackage";
+ final String displayUniqueId = VirtualDisplayAdapter.generateDisplayUniqueId(
+ packageName, Process.myUid(), config);
+
+ DisplayDevice displayDevice = mAdapter.createVirtualDisplayLocked(
+ mMockCallback, /* projection= */ null, /* ownerUid= */ 10,
+ packageName, displayUniqueId, /* surface= */ null, /* flags= */ 0, config);
+
+ assertNotNull(displayDevice);
+ DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked();
+ assertNotNull(info);
+
+ assertThat(info.width).isEqualTo(640);
+ assertThat(info.height).isEqualTo(480);
+ assertThat(info.densityDpi).isEqualTo(240);
+ assertThat(info.xDpi).isEqualTo(240);
+ assertThat(info.yDpi).isEqualTo(240);
+ assertThat(info.name).isEqualTo("testDisplayName");
+ assertThat(info.uniqueId).isEqualTo(displayUniqueId);
+ assertThat(info.ownerPackageName).isEqualTo(packageName);
+ assertThat(info.ownerUid).isEqualTo(10);
+ assertThat(info.type).isEqualTo(Display.TYPE_VIRTUAL);
+ assertThat(info.brightnessMinimum).isEqualTo(PowerManager.BRIGHTNESS_MIN);
+ assertThat(info.brightnessMaximum).isEqualTo(PowerManager.BRIGHTNESS_MAX);
+ assertThat(info.brightnessDefault).isEqualTo(PowerManager.BRIGHTNESS_MIN);
+ assertThat(info.brightnessDim).isEqualTo(PowerManager.BRIGHTNESS_INVALID);
+ }
+
+ @Test
+ public void testCreateVirtualDisplay_createDisplayDeviceInfoFromVirtualDisplayConfig() {
+ VirtualDisplayConfig config = new VirtualDisplayConfig.Builder(
+ "testDisplayName", /* width= */ 640, /* height= */ 480, /* densityDpi= */ 240)
+ .setDefaultBrightness(DEFAULT_BRIGHTNESS)
+ .setDimBrightness(DIM_BRIGHTNESS)
+ .build();
+
+ final String packageName = "testpackage";
+ final String displayUniqueId = VirtualDisplayAdapter.generateDisplayUniqueId(
+ packageName, Process.myUid(), config);
+
+ DisplayDevice displayDevice = mAdapter.createVirtualDisplayLocked(
+ mMockCallback, /* projection= */ null, /* ownerUid= */ 10,
+ packageName, displayUniqueId, /* surface= */ null, /* flags= */ 0, config);
+
+ assertNotNull(displayDevice);
+ DisplayDeviceInfo info = displayDevice.getDisplayDeviceInfoLocked();
+ assertNotNull(info);
+
+ assertThat(info.brightnessDefault).isEqualTo(DEFAULT_BRIGHTNESS);
+ assertThat(info.brightnessDim).isEqualTo(DIM_BRIGHTNESS);
+ }
+
+ @Test
public void testCreatesVirtualDisplay_checkGeneratedDisplayUniqueIdPrefix() {
VirtualDisplayConfig config = new VirtualDisplayConfig.Builder("test", /* width= */ 1,
/* height= */ 1, /* densityDpi= */ 1).build();
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/DisplayDimModifierTest.java b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/DisplayDimModifierTest.java
index be4e7c7..7e4042e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/DisplayDimModifierTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/DisplayDimModifierTest.java
@@ -44,6 +44,8 @@
private static final float MIN_DIM_AMOUNT = 0.05f;
private static final float DIM_CONFIG = 0.4f;
+ private static final int DISPLAY_ID = 3;
+
@Mock
private Context mMockContext;
@@ -66,9 +68,9 @@
R.dimen.config_screenBrightnessMinimumDimAmountFloat)).thenReturn(MIN_DIM_AMOUNT);
when(mMockContext.getSystemService(PowerManager.class)).thenReturn(mMockPowerManager);
when(mMockPowerManager.getBrightnessConstraint(
- PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM)).thenReturn(DIM_CONFIG);
+ DISPLAY_ID, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM)).thenReturn(DIM_CONFIG);
- mModifier = new DisplayDimModifier(mMockContext);
+ mModifier = new DisplayDimModifier(DISPLAY_ID, mMockContext);
mRequest.policy = DisplayManagerInternal.DisplayPowerRequest.POLICY_DIM;
}
diff --git a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
index b48c2d7..376091e 100644
--- a/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
+++ b/services/tests/powerservicetests/src/com/android/server/power/PowerManagerServiceTest.java
@@ -1257,6 +1257,36 @@
.isEqualTo(WAKEFULNESS_DOZING);
}
+ @EnableFlags({
+ android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER,
+ android.companion.virtualdevice.flags.Flags.FLAG_DISPLAY_POWER_MANAGER_APIS})
+ @Test
+ public void getBrightnessConstraint_valuesMatchDisplayInfo() {
+ final int displayId = 7;
+ final DisplayInfo info = new DisplayInfo();
+ info.brightnessMinimum = 0.12f;
+ info.brightnessDim = 0.34f;
+ info.brightnessDefault = 0.56f;
+ info.brightnessMaximum = 0.78f;
+ when(mDisplayManagerInternalMock.getDisplayInfo(displayId)).thenReturn(info);
+
+ createService();
+ startSystem();
+
+ assertThat(mService.getBinderServiceInstance().getBrightnessConstraint(
+ displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM))
+ .isEqualTo(info.brightnessMinimum);
+ assertThat(mService.getBinderServiceInstance().getBrightnessConstraint(
+ displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM))
+ .isEqualTo(info.brightnessMaximum);
+ assertThat(mService.getBinderServiceInstance().getBrightnessConstraint(
+ displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DEFAULT))
+ .isEqualTo(info.brightnessDefault);
+ assertThat(mService.getBinderServiceInstance().getBrightnessConstraint(
+ displayId, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_DIM))
+ .isEqualTo(info.brightnessDim);
+ }
+
@SuppressWarnings("GuardedBy")
@Test
public void testAmbientSuppression_disablesDreamingAndWakesDevice() {
diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
index bc03c23..9db76d4 100644
--- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
+++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java
@@ -523,12 +523,12 @@
}
void prepareBrightnessDecrease(float currentBrightness) {
- doReturn(0.0f).when(mPowerManager)
- .getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
- doReturn(1.0f).when(mPowerManager)
- .getBrightnessConstraint(PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
+ doReturn(0.0f).when(mPowerManager).getBrightnessConstraint(
+ DEFAULT_DISPLAY, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MINIMUM);
+ doReturn(1.0f).when(mPowerManager).getBrightnessConstraint(
+ DEFAULT_DISPLAY, PowerManager.BRIGHTNESS_CONSTRAINT_TYPE_MAXIMUM);
doReturn(currentBrightness).when(mDisplayManager)
- .getBrightness(0);
+ .getBrightness(DEFAULT_DISPLAY);
}
void verifyNewBrightness(float newBrightness) {