Merge "Guard minSdkVersionFull attribute with android.sdk.major_minor_versioning_scheme" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 3689ff5..7977d73 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -100,6 +100,7 @@
"com.android.media.flags.editing-aconfig-java",
"com.android.media.flags.performance-aconfig-java",
"com.android.media.flags.projection-aconfig-java",
+ "com.android.net.http.flags-aconfig-exported-java",
"com.android.net.thread.platform.flags-aconfig-java",
"com.android.ranging.flags.ranging-aconfig-java-export",
"com.android.server.contextualsearch.flags-java",
diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java
index 2e108a1..2f16115 100644
--- a/core/java/android/companion/AssociationInfo.java
+++ b/core/java/android/companion/AssociationInfo.java
@@ -460,8 +460,8 @@
} else {
mDeviceIcon = null;
}
-
- if (Flags.associationTag() && in.readInt() == 1) {
+ int deviceId = in.readInt();
+ if (Flags.associationTag() && deviceId == 1) {
mDeviceId = in.readTypedObject(DeviceId.CREATOR);
} else {
mDeviceId = null;
diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java
index 0e53d87..54d0dd0 100644
--- a/core/java/android/hardware/display/DisplayTopology.java
+++ b/core/java/android/hardware/display/DisplayTopology.java
@@ -28,6 +28,7 @@
import android.os.Parcel;
import android.os.Parcelable;
import android.util.IndentingPrintWriter;
+import android.util.MathUtils;
import android.util.Pair;
import android.util.Slog;
import android.view.Display;
@@ -284,6 +285,146 @@
}
/**
+ * Clamp offsets and remove any overlaps between displays.
+ */
+ public void normalize() {
+ if (mRoot == null) {
+ return;
+ }
+ clampOffsets(mRoot);
+
+ Map<TreeNode, RectF> bounds = new HashMap<>();
+ Map<TreeNode, Integer> depths = new HashMap<>();
+ Map<TreeNode, TreeNode> parents = new HashMap<>();
+ getInfo(bounds, depths, parents, mRoot, /* x= */ 0, /* y= */ 0, /* depth= */ 0);
+
+ // Sort the displays first by their depth in the tree, then by the distance of their top
+ // left point from the root display's origin (0, 0). This way we process the displays
+ // starting at the root and we push out a display if necessary.
+ Comparator<TreeNode> comparator = (d1, d2) -> {
+ if (d1 == d2) {
+ return 0;
+ }
+
+ int compareDepths = Integer.compare(depths.get(d1), depths.get(d2));
+ if (compareDepths != 0) {
+ return compareDepths;
+ }
+
+ RectF bounds1 = bounds.get(d1);
+ RectF bounds2 = bounds.get(d2);
+ return Double.compare(Math.hypot(bounds1.left, bounds1.top),
+ Math.hypot(bounds2.left, bounds2.top));
+ };
+ List<TreeNode> displays = new ArrayList<>(bounds.keySet());
+ displays.sort(comparator);
+
+ for (int i = 1; i < displays.size(); i++) {
+ TreeNode targetDisplay = displays.get(i);
+ TreeNode lastIntersectingSourceDisplay = null;
+ float lastOffsetX = 0;
+ float lastOffsetY = 0;
+
+ for (int j = 0; j < i; j++) {
+ TreeNode sourceDisplay = displays.get(j);
+ RectF sourceBounds = bounds.get(sourceDisplay);
+ RectF targetBounds = bounds.get(targetDisplay);
+
+ if (!RectF.intersects(sourceBounds, targetBounds)) {
+ continue;
+ }
+
+ // Find the offset by which to move the display. Pick the smaller one among the x
+ // and y axes.
+ float offsetX = targetBounds.left >= 0
+ ? sourceBounds.right - targetBounds.left
+ : sourceBounds.left - targetBounds.right;
+ float offsetY = targetBounds.top >= 0
+ ? sourceBounds.bottom - targetBounds.top
+ : sourceBounds.top - targetBounds.bottom;
+ if (Math.abs(offsetX) <= Math.abs(offsetY)) {
+ targetBounds.left += offsetX;
+ targetBounds.right += offsetX;
+ // We need to also update the offset in the tree
+ if (targetDisplay.mPosition == POSITION_TOP
+ || targetDisplay.mPosition == POSITION_BOTTOM) {
+ targetDisplay.mOffset += offsetX;
+ }
+ offsetY = 0;
+ } else {
+ targetBounds.top += offsetY;
+ targetBounds.bottom += offsetY;
+ // We need to also update the offset in the tree
+ if (targetDisplay.mPosition == POSITION_LEFT
+ || targetDisplay.mPosition == POSITION_RIGHT) {
+ targetDisplay.mOffset += offsetY;
+ }
+ offsetX = 0;
+ }
+
+ lastIntersectingSourceDisplay = sourceDisplay;
+ lastOffsetX = offsetX;
+ lastOffsetY = offsetY;
+ }
+
+ // Now re-parent the target display to the last intersecting source display if it no
+ // longer touches its parent.
+ if (lastIntersectingSourceDisplay == null) {
+ // There was no overlap.
+ continue;
+ }
+ TreeNode parent = parents.get(targetDisplay);
+ if (parent == lastIntersectingSourceDisplay) {
+ // The displays are moved in such a way that they're adjacent to the intersecting
+ // display. If the last intersecting display happens to be the parent then we
+ // already know that the display is adjacent to its parent.
+ continue;
+ }
+
+ RectF childBounds = bounds.get(targetDisplay);
+ RectF parentBounds = bounds.get(parent);
+ // Check that the edges are on the same line
+ boolean areTouching = switch (targetDisplay.mPosition) {
+ case POSITION_LEFT -> floatEquals(parentBounds.left, childBounds.right);
+ case POSITION_RIGHT -> floatEquals(parentBounds.right, childBounds.left);
+ case POSITION_TOP -> floatEquals(parentBounds.top, childBounds.bottom);
+ case POSITION_BOTTOM -> floatEquals(parentBounds.bottom, childBounds.top);
+ default -> throw new IllegalStateException(
+ "Unexpected value: " + targetDisplay.mPosition);
+ };
+ // Check that the offset is within bounds
+ areTouching &= switch (targetDisplay.mPosition) {
+ case POSITION_LEFT, POSITION_RIGHT ->
+ childBounds.bottom + EPSILON >= parentBounds.top
+ && childBounds.top <= parentBounds.bottom + EPSILON;
+ case POSITION_TOP, POSITION_BOTTOM ->
+ childBounds.right + EPSILON >= parentBounds.left
+ && childBounds.left <= parentBounds.right + EPSILON;
+ default -> throw new IllegalStateException(
+ "Unexpected value: " + targetDisplay.mPosition);
+ };
+
+ if (!areTouching) {
+ // Re-parent the display.
+ parent.mChildren.remove(targetDisplay);
+ RectF lastIntersectingSourceDisplayBounds =
+ bounds.get(lastIntersectingSourceDisplay);
+ lastIntersectingSourceDisplay.mChildren.add(targetDisplay);
+
+ if (lastOffsetX != 0) {
+ targetDisplay.mPosition = lastOffsetX > 0 ? POSITION_RIGHT : POSITION_LEFT;
+ targetDisplay.mOffset =
+ childBounds.top - lastIntersectingSourceDisplayBounds.top;
+ } else if (lastOffsetY != 0) {
+ targetDisplay.mPosition = lastOffsetY > 0 ? POSITION_BOTTOM : POSITION_TOP;
+ targetDisplay.mOffset =
+ childBounds.left - lastIntersectingSourceDisplayBounds.left;
+ }
+ }
+ }
+ }
+
+ /**
* @return A deep copy of the topology that will not be modified by the system.
*/
public DisplayTopology copy() {
@@ -442,145 +583,6 @@
}
/**
- * Update the topology to remove any overlaps between displays.
- */
- @VisibleForTesting
- public void normalize() {
- if (mRoot == null) {
- return;
- }
- Map<TreeNode, RectF> bounds = new HashMap<>();
- Map<TreeNode, Integer> depths = new HashMap<>();
- Map<TreeNode, TreeNode> parents = new HashMap<>();
- getInfo(bounds, depths, parents, mRoot, /* x= */ 0, /* y= */ 0, /* depth= */ 0);
-
- // Sort the displays first by their depth in the tree, then by the distance of their top
- // left point from the root display's origin (0, 0). This way we process the displays
- // starting at the root and we push out a display if necessary.
- Comparator<TreeNode> comparator = (d1, d2) -> {
- if (d1 == d2) {
- return 0;
- }
-
- int compareDepths = Integer.compare(depths.get(d1), depths.get(d2));
- if (compareDepths != 0) {
- return compareDepths;
- }
-
- RectF bounds1 = bounds.get(d1);
- RectF bounds2 = bounds.get(d2);
- return Double.compare(Math.hypot(bounds1.left, bounds1.top),
- Math.hypot(bounds2.left, bounds2.top));
- };
- List<TreeNode> displays = new ArrayList<>(bounds.keySet());
- displays.sort(comparator);
-
- for (int i = 1; i < displays.size(); i++) {
- TreeNode targetDisplay = displays.get(i);
- TreeNode lastIntersectingSourceDisplay = null;
- float lastOffsetX = 0;
- float lastOffsetY = 0;
-
- for (int j = 0; j < i; j++) {
- TreeNode sourceDisplay = displays.get(j);
- RectF sourceBounds = bounds.get(sourceDisplay);
- RectF targetBounds = bounds.get(targetDisplay);
-
- if (!RectF.intersects(sourceBounds, targetBounds)) {
- continue;
- }
-
- // Find the offset by which to move the display. Pick the smaller one among the x
- // and y axes.
- float offsetX = targetBounds.left >= 0
- ? sourceBounds.right - targetBounds.left
- : sourceBounds.left - targetBounds.right;
- float offsetY = targetBounds.top >= 0
- ? sourceBounds.bottom - targetBounds.top
- : sourceBounds.top - targetBounds.bottom;
- if (Math.abs(offsetX) <= Math.abs(offsetY)) {
- targetBounds.left += offsetX;
- targetBounds.right += offsetX;
- // We need to also update the offset in the tree
- if (targetDisplay.mPosition == POSITION_TOP
- || targetDisplay.mPosition == POSITION_BOTTOM) {
- targetDisplay.mOffset += offsetX;
- }
- offsetY = 0;
- } else {
- targetBounds.top += offsetY;
- targetBounds.bottom += offsetY;
- // We need to also update the offset in the tree
- if (targetDisplay.mPosition == POSITION_LEFT
- || targetDisplay.mPosition == POSITION_RIGHT) {
- targetDisplay.mOffset += offsetY;
- }
- offsetX = 0;
- }
-
- lastIntersectingSourceDisplay = sourceDisplay;
- lastOffsetX = offsetX;
- lastOffsetY = offsetY;
- }
-
- // Now re-parent the target display to the last intersecting source display if it no
- // longer touches its parent.
- if (lastIntersectingSourceDisplay == null) {
- // There was no overlap.
- continue;
- }
- TreeNode parent = parents.get(targetDisplay);
- if (parent == lastIntersectingSourceDisplay) {
- // The displays are moved in such a way that they're adjacent to the intersecting
- // display. If the last intersecting display happens to be the parent then we
- // already know that the display is adjacent to its parent.
- continue;
- }
-
- RectF childBounds = bounds.get(targetDisplay);
- RectF parentBounds = bounds.get(parent);
- // Check that the edges are on the same line
- boolean areTouching = switch (targetDisplay.mPosition) {
- case POSITION_LEFT -> floatEquals(parentBounds.left, childBounds.right);
- case POSITION_RIGHT -> floatEquals(parentBounds.right, childBounds.left);
- case POSITION_TOP -> floatEquals(parentBounds.top, childBounds.bottom);
- case POSITION_BOTTOM -> floatEquals(parentBounds.bottom, childBounds.top);
- default -> throw new IllegalStateException(
- "Unexpected value: " + targetDisplay.mPosition);
- };
- // Check that the offset is within bounds
- areTouching &= switch (targetDisplay.mPosition) {
- case POSITION_LEFT, POSITION_RIGHT ->
- childBounds.bottom + EPSILON >= parentBounds.top
- && childBounds.top <= parentBounds.bottom + EPSILON;
- case POSITION_TOP, POSITION_BOTTOM ->
- childBounds.right + EPSILON >= parentBounds.left
- && childBounds.left <= parentBounds.right + EPSILON;
- default -> throw new IllegalStateException(
- "Unexpected value: " + targetDisplay.mPosition);
- };
-
- if (!areTouching) {
- // Re-parent the display.
- parent.mChildren.remove(targetDisplay);
- RectF lastIntersectingSourceDisplayBounds =
- bounds.get(lastIntersectingSourceDisplay);
- lastIntersectingSourceDisplay.mChildren.add(targetDisplay);
-
- if (lastOffsetX != 0) {
- targetDisplay.mPosition = lastOffsetX > 0 ? POSITION_RIGHT : POSITION_LEFT;
- targetDisplay.mOffset =
- childBounds.top - lastIntersectingSourceDisplayBounds.top;
- } else if (lastOffsetY != 0) {
- targetDisplay.mPosition = lastOffsetY > 0 ? POSITION_BOTTOM : POSITION_TOP;
- targetDisplay.mOffset =
- childBounds.left - lastIntersectingSourceDisplayBounds.left;
- }
- }
- }
- }
-
- /**
* Tests whether two brightness float values are within a small enough tolerance
* of each other.
* @param a first float to compare
@@ -605,6 +607,24 @@
return found;
}
+ /**
+ * Ensure that the offsets of all displays within the given tree are within bounds.
+ * @param display The starting node
+ */
+ private void clampOffsets(TreeNode display) {
+ if (display == null) {
+ return;
+ }
+ for (TreeNode child : display.mChildren) {
+ if (child.mPosition == POSITION_LEFT || child.mPosition == POSITION_RIGHT) {
+ child.mOffset = MathUtils.constrain(child.mOffset, -child.mHeight, display.mHeight);
+ } else if (child.mPosition == POSITION_TOP || child.mPosition == POSITION_BOTTOM) {
+ child.mOffset = MathUtils.constrain(child.mOffset, -child.mWidth, display.mWidth);
+ }
+ clampOffsets(child);
+ }
+ }
+
public static final class TreeNode implements Parcelable {
public static final int POSITION_LEFT = 0;
public static final int POSITION_TOP = 1;
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 049189f..02c7901 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 && !mParent.isLayoutRequested()) {
+ if (mParent != null) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java
index e402ddf..e60879e 100644
--- a/core/java/com/android/internal/os/ZygoteInit.java
+++ b/core/java/com/android/internal/os/ZygoteInit.java
@@ -19,6 +19,8 @@
import static android.system.OsConstants.S_IRWXG;
import static android.system.OsConstants.S_IRWXO;
+import static android.net.http.Flags.preloadHttpengineInZygote;
+
import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SECONDARY_ZYGOTE_INIT_START;
import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__ZYGOTE_INIT_START;
@@ -27,6 +29,7 @@
import android.content.pm.SharedLibraryInfo;
import android.content.res.Resources;
import android.os.Build;
+import android.net.http.HttpEngine;
import android.os.Environment;
import android.os.IInstalld;
import android.os.Process;
@@ -144,6 +147,23 @@
Trace.traceEnd(Trace.TRACE_TAG_DALVIK);
preloadSharedLibraries();
preloadTextResources();
+
+ // TODO: remove the try/catch and the flag read as soon as the flag is ramped and 25Q2
+ // starts building from source.
+ if (preloadHttpengineInZygote()) {
+ try {
+ HttpEngine.preload();
+ } catch (NoSuchMethodError e){
+ // The flag protecting this API is not an exported
+ // flag because ZygoteInit happens before the
+ // system service has initialized the flag which means
+ // that we can't query the real value of the flag
+ // from the tethering module. In order to avoid crashing
+ // in the case where we have (new zygote, old tethering).
+ // we catch the NoSuchMethodError and just log.
+ Log.d(TAG, "HttpEngine.preload() threw " + e);
+ }
+ }
// Ask the WebViewFactory to do any initialization that must run in the zygote process,
// for memory sharing purposes.
WebViewFactory.prepareWebViewInZygote();
diff --git a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt
index f584ab9..18e4fde 100644
--- a/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt
+++ b/core/tests/coretests/src/android/hardware/display/DisplayTopologyTest.kt
@@ -38,12 +38,7 @@
topology.addDisplay(displayId, width, height)
assertThat(topology.primaryDisplayId).isEqualTo(displayId)
-
- val display = topology.root!!
- assertThat(display.displayId).isEqualTo(displayId)
- assertThat(display.width).isEqualTo(width)
- assertThat(display.height).isEqualTo(height)
- assertThat(display.children).isEmpty()
+ verifyDisplay(topology.root!!, displayId, width, height, noOfChildren = 0)
}
@Test
@@ -62,18 +57,9 @@
assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
val display1 = topology.root!!
- assertThat(display1.displayId).isEqualTo(displayId1)
- assertThat(display1.width).isEqualTo(width1)
- assertThat(display1.height).isEqualTo(height1)
- assertThat(display1.children).hasSize(1)
-
- val display2 = display1.children[0]
- assertThat(display2.displayId).isEqualTo(displayId2)
- assertThat(display2.width).isEqualTo(width2)
- assertThat(display2.height).isEqualTo(height2)
- assertThat(display2.children).isEmpty()
- assertThat(display2.position).isEqualTo(POSITION_TOP)
- assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2)
+ verifyDisplay(display1, displayId1, width1, height1, noOfChildren = 1)
+ verifyDisplay(display1.children[0], displayId2, width2, height2, POSITION_TOP,
+ offset = width1 / 2 - width2 / 2, noOfChildren = 0)
}
@Test
@@ -97,29 +83,18 @@
assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
val display1 = topology.root!!
- assertThat(display1.displayId).isEqualTo(displayId1)
- assertThat(display1.width).isEqualTo(width1)
- assertThat(display1.height).isEqualTo(height1)
- assertThat(display1.children).hasSize(1)
+ verifyDisplay(display1, displayId1, width1, height1, noOfChildren = 1)
val display2 = display1.children[0]
- assertThat(display2.displayId).isEqualTo(displayId2)
- assertThat(display2.width).isEqualTo(width2)
- assertThat(display2.height).isEqualTo(height2)
- assertThat(display2.children).hasSize(1)
- assertThat(display2.position).isEqualTo(POSITION_TOP)
- assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2)
+ verifyDisplay(display1.children[0], displayId2, width2, height2, POSITION_TOP,
+ offset = width1 / 2 - width2 / 2, noOfChildren = 1)
var display = display2
for (i in 3..noOfDisplays) {
display = display.children[0]
- assertThat(display.displayId).isEqualTo(i)
- assertThat(display.width).isEqualTo(width1)
- assertThat(display.height).isEqualTo(height1)
// The last display should have no children
- assertThat(display.children).hasSize(if (i < noOfDisplays) 1 else 0)
- assertThat(display.position).isEqualTo(POSITION_RIGHT)
- assertThat(display.offset).isEqualTo(0)
+ verifyDisplay(display, id = i, width1, height1, POSITION_RIGHT, offset = 0f,
+ noOfChildren = if (i < noOfDisplays) 1 else 0)
}
}
@@ -147,18 +122,11 @@
assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
var display1 = topology.root!!
- assertThat(display1.displayId).isEqualTo(displayId1)
- assertThat(display1.width).isEqualTo(width1)
- assertThat(display1.height).isEqualTo(height1)
- assertThat(display1.children).hasSize(1)
+ verifyDisplay(display1, displayId1, width1, height1, noOfChildren = 1)
var display2 = display1.children[0]
- assertThat(display2.displayId).isEqualTo(displayId2)
- assertThat(display2.width).isEqualTo(width2)
- assertThat(display2.height).isEqualTo(height2)
- assertThat(display2.children).hasSize(1)
- assertThat(display2.position).isEqualTo(POSITION_TOP)
- assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2)
+ verifyDisplay(display2, displayId2, width2, height2, POSITION_TOP,
+ offset = width1 / 2 - width2 / 2, noOfChildren = 1)
var display = display2
for (i in 3..noOfDisplays) {
@@ -166,13 +134,9 @@
continue
}
display = display.children[0]
- assertThat(display.displayId).isEqualTo(i)
- assertThat(display.width).isEqualTo(width1)
- assertThat(display.height).isEqualTo(height1)
// The last display should have no children
- assertThat(display.children).hasSize(if (i < noOfDisplays) 1 else 0)
- assertThat(display.position).isEqualTo(POSITION_RIGHT)
- assertThat(display.offset).isEqualTo(0)
+ verifyDisplay(display, id = i, width1, height1, POSITION_RIGHT, offset = 0f,
+ noOfChildren = if (i < noOfDisplays) 1 else 0)
}
topology.removeDisplay(22)
@@ -185,18 +149,11 @@
assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
display1 = topology.root!!
- assertThat(display1.displayId).isEqualTo(displayId1)
- assertThat(display1.width).isEqualTo(width1)
- assertThat(display1.height).isEqualTo(height1)
- assertThat(display1.children).hasSize(1)
+ verifyDisplay(display1, displayId1, width1, height1, noOfChildren = 1)
display2 = display1.children[0]
- assertThat(display2.displayId).isEqualTo(displayId2)
- assertThat(display2.width).isEqualTo(width2)
- assertThat(display2.height).isEqualTo(height2)
- assertThat(display2.children).hasSize(1)
- assertThat(display2.position).isEqualTo(POSITION_TOP)
- assertThat(display2.offset).isEqualTo(width1 / 2 - width2 / 2)
+ verifyDisplay(display2, displayId2, width2, height2, POSITION_TOP,
+ offset = width1 / 2 - width2 / 2, noOfChildren = 1)
display = display2
for (i in 3..noOfDisplays) {
@@ -204,13 +161,9 @@
continue
}
display = display.children[0]
- assertThat(display.displayId).isEqualTo(i)
- assertThat(display.width).isEqualTo(width1)
- assertThat(display.height).isEqualTo(height1)
// The last display should have no children
- assertThat(display.children).hasSize(if (i < noOfDisplays) 1 else 0)
- assertThat(display.position).isEqualTo(POSITION_RIGHT)
- assertThat(display.offset).isEqualTo(0)
+ verifyDisplay(display, id = i, width1, height1, POSITION_RIGHT, offset = 0f,
+ noOfChildren = if (i < noOfDisplays) 1 else 0)
}
}
@@ -237,12 +190,7 @@
topology.removeDisplay(3)
assertThat(topology.primaryDisplayId).isEqualTo(displayId)
-
- val display = topology.root!!
- assertThat(display.displayId).isEqualTo(displayId)
- assertThat(display.width).isEqualTo(width)
- assertThat(display.height).isEqualTo(height)
- assertThat(display.children).isEmpty()
+ verifyDisplay(topology.root!!, displayId, width, height, noOfChildren = 0)
}
@Test
@@ -258,11 +206,46 @@
topology.removeDisplay(displayId2)
assertThat(topology.primaryDisplayId).isEqualTo(displayId1)
- val display = topology.root!!
- assertThat(display.displayId).isEqualTo(displayId1)
- assertThat(display.width).isEqualTo(width)
- assertThat(display.height).isEqualTo(height)
- assertThat(display.children).isEmpty()
+ verifyDisplay(topology.root!!, displayId1, width, height, noOfChildren = 0)
+ }
+
+ @Test
+ fun normalization_clampsOffsets() {
+ val display1 = DisplayTopology.TreeNode(/* displayId= */ 1, /* width= */ 200f,
+ /* height= */ 600f, /* position= */ 0, /* offset= */ 0f)
+
+ val display2 = DisplayTopology.TreeNode(/* displayId= */ 2, /* width= */ 600f,
+ /* height= */ 200f, POSITION_RIGHT, /* offset= */ 800f)
+ display1.addChild(display2)
+
+ val primaryDisplayId = 3
+ val display3 = DisplayTopology.TreeNode(primaryDisplayId, /* width= */ 600f,
+ /* height= */ 200f, POSITION_LEFT, /* offset= */ -300f)
+ display1.addChild(display3)
+
+ val display4 = DisplayTopology.TreeNode(/* displayId= */ 4, /* width= */ 200f,
+ /* height= */ 600f, POSITION_TOP, /* offset= */ 1000f)
+ display2.addChild(display4)
+
+ topology = DisplayTopology(display1, primaryDisplayId)
+ topology.normalize()
+
+ assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId)
+
+ val actualDisplay1 = topology.root!!
+ verifyDisplay(actualDisplay1, id = 1, width = 200f, height = 600f, noOfChildren = 2)
+
+ val actualDisplay2 = actualDisplay1.children[0]
+ verifyDisplay(actualDisplay2, id = 2, width = 600f, height = 200f, POSITION_RIGHT,
+ offset = 600f, noOfChildren = 1)
+
+ val actualDisplay3 = actualDisplay1.children[1]
+ verifyDisplay(actualDisplay3, id = 3, width = 600f, height = 200f, POSITION_LEFT,
+ offset = -200f, noOfChildren = 0)
+
+ val actualDisplay4 = actualDisplay2.children[0]
+ verifyDisplay(actualDisplay4, id = 4, width = 200f, height = 600f, POSITION_TOP,
+ offset = 600f, noOfChildren = 0)
}
@Test
@@ -289,34 +272,19 @@
assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId)
val actualDisplay1 = topology.root!!
- assertThat(actualDisplay1.displayId).isEqualTo(1)
- assertThat(actualDisplay1.width).isEqualTo(200f)
- assertThat(actualDisplay1.height).isEqualTo(600f)
- assertThat(actualDisplay1.children).hasSize(2)
+ verifyDisplay(actualDisplay1, id = 1, width = 200f, height = 600f, noOfChildren = 2)
val actualDisplay2 = actualDisplay1.children[0]
- assertThat(actualDisplay2.displayId).isEqualTo(2)
- assertThat(actualDisplay2.width).isEqualTo(600f)
- assertThat(actualDisplay2.height).isEqualTo(200f)
- assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay2.offset).isEqualTo(0f)
- assertThat(actualDisplay2.children).hasSize(1)
+ verifyDisplay(actualDisplay2, id = 2, width = 600f, height = 200f, POSITION_RIGHT,
+ offset = 0f, noOfChildren = 1)
val actualDisplay3 = actualDisplay1.children[1]
- assertThat(actualDisplay3.displayId).isEqualTo(3)
- assertThat(actualDisplay3.width).isEqualTo(600f)
- assertThat(actualDisplay3.height).isEqualTo(200f)
- assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay3.offset).isEqualTo(400f)
- assertThat(actualDisplay3.children).isEmpty()
+ verifyDisplay(actualDisplay3, id = 3, width = 600f, height = 200f, POSITION_RIGHT,
+ offset = 400f, noOfChildren = 0)
val actualDisplay4 = actualDisplay2.children[0]
- assertThat(actualDisplay4.displayId).isEqualTo(4)
- assertThat(actualDisplay4.width).isEqualTo(200f)
- assertThat(actualDisplay4.height).isEqualTo(600f)
- assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay4.offset).isEqualTo(0f)
- assertThat(actualDisplay4.children).isEmpty()
+ verifyDisplay(actualDisplay4, id = 4, width = 200f, height = 600f, POSITION_RIGHT,
+ offset = 0f, noOfChildren = 0)
}
@Test
@@ -344,34 +312,19 @@
assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId)
val actualDisplay1 = topology.root!!
- assertThat(actualDisplay1.displayId).isEqualTo(1)
- assertThat(actualDisplay1.width).isEqualTo(200f)
- assertThat(actualDisplay1.height).isEqualTo(600f)
- assertThat(actualDisplay1.children).hasSize(1)
+ verifyDisplay(actualDisplay1, id = 1, width = 200f, height = 600f, noOfChildren = 1)
val actualDisplay2 = actualDisplay1.children[0]
- assertThat(actualDisplay2.displayId).isEqualTo(2)
- assertThat(actualDisplay2.width).isEqualTo(200f)
- assertThat(actualDisplay2.height).isEqualTo(600f)
- assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay2.offset).isEqualTo(0f)
- assertThat(actualDisplay2.children).hasSize(2)
+ verifyDisplay(actualDisplay2, id = 2, width = 200f, height = 600f, POSITION_RIGHT,
+ offset = 0f, noOfChildren = 2)
val actualDisplay3 = actualDisplay2.children[1]
- assertThat(actualDisplay3.displayId).isEqualTo(3)
- assertThat(actualDisplay3.width).isEqualTo(600f)
- assertThat(actualDisplay3.height).isEqualTo(200f)
- assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay3.offset).isEqualTo(10f)
- assertThat(actualDisplay3.children).isEmpty()
+ verifyDisplay(actualDisplay3, id = 3, width = 600f, height = 200f, POSITION_RIGHT,
+ offset = 10f, noOfChildren = 0)
val actualDisplay4 = actualDisplay2.children[0]
- assertThat(actualDisplay4.displayId).isEqualTo(4)
- assertThat(actualDisplay4.width).isEqualTo(200f)
- assertThat(actualDisplay4.height).isEqualTo(600f)
- assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay4.offset).isEqualTo(210f)
- assertThat(actualDisplay4.children).isEmpty()
+ verifyDisplay(actualDisplay4, id = 4, width = 200f, height = 600f, POSITION_RIGHT,
+ offset = 210f, noOfChildren = 0)
}
@Test
@@ -397,26 +350,15 @@
assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId)
val actualDisplay1 = topology.root!!
- assertThat(actualDisplay1.displayId).isEqualTo(1)
- assertThat(actualDisplay1.width).isEqualTo(200f)
- assertThat(actualDisplay1.height).isEqualTo(50f)
- assertThat(actualDisplay1.children).hasSize(1)
+ verifyDisplay(actualDisplay1, id = 1, width = 200f, height = 50f, noOfChildren = 1)
val actualDisplay2 = actualDisplay1.children[0]
- assertThat(actualDisplay2.displayId).isEqualTo(2)
- assertThat(actualDisplay2.width).isEqualTo(600f)
- assertThat(actualDisplay2.height).isEqualTo(200f)
- assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay2.offset).isEqualTo(0f)
- assertThat(actualDisplay2.children).hasSize(1)
+ verifyDisplay(actualDisplay2, id = 2, width = 600f, height = 200f, POSITION_RIGHT,
+ offset = 0f, noOfChildren = 1)
val actualDisplay3 = actualDisplay2.children[0]
- assertThat(actualDisplay3.displayId).isEqualTo(3)
- assertThat(actualDisplay3.width).isEqualTo(600f)
- assertThat(actualDisplay3.height).isEqualTo(200f)
- assertThat(actualDisplay3.position).isEqualTo(POSITION_BOTTOM)
- assertThat(actualDisplay3.offset).isEqualTo(0f)
- assertThat(actualDisplay3.children).isEmpty()
+ verifyDisplay(actualDisplay3, id = 3, width = 600f, height = 200f, POSITION_BOTTOM,
+ offset = 0f, noOfChildren = 0)
}
@Test
@@ -443,34 +385,19 @@
assertThat(topology.primaryDisplayId).isEqualTo(primaryDisplayId)
val actualDisplay1 = topology.root!!
- assertThat(actualDisplay1.displayId).isEqualTo(1)
- assertThat(actualDisplay1.width).isEqualTo(200f)
- assertThat(actualDisplay1.height).isEqualTo(600f)
- assertThat(actualDisplay1.children).hasSize(1)
+ verifyDisplay(actualDisplay1, id = 1, width = 200f, height = 600f, noOfChildren = 1)
val actualDisplay2 = actualDisplay1.children[0]
- assertThat(actualDisplay2.displayId).isEqualTo(2)
- assertThat(actualDisplay2.width).isEqualTo(200f)
- assertThat(actualDisplay2.height).isEqualTo(600f)
- assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay2.offset).isEqualTo(0f)
- assertThat(actualDisplay2.children).hasSize(1)
+ verifyDisplay(actualDisplay2, id = 2, width = 200f, height = 600f, POSITION_RIGHT,
+ offset = 0f, noOfChildren = 1)
val actualDisplay3 = actualDisplay2.children[0]
- assertThat(actualDisplay3.displayId).isEqualTo(3)
- assertThat(actualDisplay3.width).isEqualTo(600f)
- assertThat(actualDisplay3.height).isEqualTo(200f)
- assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay3.offset).isEqualTo(400f)
- assertThat(actualDisplay3.children).hasSize(1)
+ verifyDisplay(actualDisplay3, id = 3, width = 600f, height = 200f, POSITION_RIGHT,
+ offset = 400f, noOfChildren = 1)
val actualDisplay4 = actualDisplay3.children[0]
- assertThat(actualDisplay4.displayId).isEqualTo(4)
- assertThat(actualDisplay4.width).isEqualTo(200f)
- assertThat(actualDisplay4.height).isEqualTo(600f)
- assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay4.offset).isEqualTo(-400f)
- assertThat(actualDisplay4.children).isEmpty()
+ verifyDisplay(actualDisplay4, id = 4, width = 200f, height = 600f, POSITION_RIGHT,
+ offset = -400f, noOfChildren = 0)
}
@Test
@@ -635,7 +562,7 @@
nodes,
// In the case of corner adjacency, we prefer a left/right attachment.
Pair(POSITION_RIGHT, 10f),
- Pair(POSITION_BOTTOM, 40.5f), // TODO: fix implementation to remove this gap
+ Pair(POSITION_BOTTOM, 30f),
)
assertThat(nodes[0].children).containsExactly(nodes[1])
@@ -667,34 +594,19 @@
assertThat(copy.primaryDisplayId).isEqualTo(primaryDisplayId)
val actualDisplay1 = copy.root!!
- assertThat(actualDisplay1.displayId).isEqualTo(1)
- assertThat(actualDisplay1.width).isEqualTo(200f)
- assertThat(actualDisplay1.height).isEqualTo(600f)
- assertThat(actualDisplay1.children).hasSize(2)
+ verifyDisplay(actualDisplay1, id = 1, width = 200f, height = 600f, noOfChildren = 2)
val actualDisplay2 = actualDisplay1.children[0]
- assertThat(actualDisplay2.displayId).isEqualTo(2)
- assertThat(actualDisplay2.width).isEqualTo(600f)
- assertThat(actualDisplay2.height).isEqualTo(200f)
- assertThat(actualDisplay2.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay2.offset).isEqualTo(0f)
- assertThat(actualDisplay2.children).hasSize(1)
+ verifyDisplay(actualDisplay2, id = 2, width = 600f, height = 200f, POSITION_RIGHT,
+ offset = 0f, noOfChildren = 1)
val actualDisplay3 = actualDisplay1.children[1]
- assertThat(actualDisplay3.displayId).isEqualTo(3)
- assertThat(actualDisplay3.width).isEqualTo(600f)
- assertThat(actualDisplay3.height).isEqualTo(200f)
- assertThat(actualDisplay3.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay3.offset).isEqualTo(400f)
- assertThat(actualDisplay3.children).isEmpty()
+ verifyDisplay(actualDisplay3, id = 3, width = 600f, height = 200f, POSITION_RIGHT,
+ offset = 400f, noOfChildren = 0)
val actualDisplay4 = actualDisplay2.children[0]
- assertThat(actualDisplay4.displayId).isEqualTo(4)
- assertThat(actualDisplay4.width).isEqualTo(200f)
- assertThat(actualDisplay4.height).isEqualTo(600f)
- assertThat(actualDisplay4.position).isEqualTo(POSITION_RIGHT)
- assertThat(actualDisplay4.offset).isEqualTo(0f)
- assertThat(actualDisplay4.children).isEmpty()
+ verifyDisplay(actualDisplay4, id = 4, width = 200f, height = 600f, POSITION_RIGHT,
+ offset = 0f, noOfChildren = 0)
}
/**
@@ -722,9 +634,20 @@
return nodes
}
+ private fun verifyDisplay(display: DisplayTopology.TreeNode, id: Int, width: Float,
+ height: Float, @DisplayTopology.TreeNode.Position position: Int = 0,
+ offset: Float = 0f, noOfChildren: Int) {
+ assertThat(display.displayId).isEqualTo(id)
+ assertThat(display.width).isEqualTo(width)
+ assertThat(display.height).isEqualTo(height)
+ assertThat(display.position).isEqualTo(position)
+ assertThat(display.offset).isEqualTo(offset)
+ assertThat(display.children).hasSize(noOfChildren)
+ }
+
private fun assertPositioning(
nodes: List<DisplayTopology.TreeNode>, vararg positions: Pair<Int, Float>) {
- assertThat(nodes.drop(1).map { Pair(it.position, it.offset )})
+ assertThat(nodes.drop(1).map { Pair(it.position, it.offset) })
.containsExactly(*positions)
.inOrder()
}
diff --git a/core/tests/coretests/src/android/view/ViewGroupTest.java b/core/tests/coretests/src/android/view/ViewGroupTest.java
index ae3ad36..43c404e 100644
--- a/core/tests/coretests/src/android/view/ViewGroupTest.java
+++ b/core/tests/coretests/src/android/view/ViewGroupTest.java
@@ -213,6 +213,35 @@
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();
@@ -240,6 +269,19 @@
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/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
index 2128cbc..0d16880 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManager.java
@@ -26,6 +26,7 @@
import android.graphics.Rect;
import android.util.Pair;
import android.view.LayoutInflater;
+import android.view.SurfaceControl;
import android.view.View;
import android.window.DesktopModeFlags;
@@ -70,6 +71,9 @@
private final float mHideScmTolerance;
+ @NonNull
+ private final Rect mLayoutBounds = new Rect();
+
CompatUIWindowManager(@NonNull Context context, @NonNull TaskInfo taskInfo,
@NonNull SyncTransactionQueue syncQueue,
@NonNull Consumer<CompatUIEvent> callback,
@@ -105,6 +109,7 @@
@Override
protected void removeLayout() {
+ mLayoutBounds.setEmpty();
mLayout = null;
}
@@ -171,18 +176,21 @@
@Override
@VisibleForTesting
public void updateSurfacePosition() {
- if (mLayout == null) {
+ updateLayoutBounds();
+ if (mLayoutBounds.isEmpty()) {
return;
}
- // Position of the button in the container coordinate.
- final Rect taskBounds = getTaskBounds();
- final Rect taskStableBounds = getTaskStableBounds();
- final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
- ? taskStableBounds.left - taskBounds.left
- : taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth();
- final int positionY = taskStableBounds.bottom - taskBounds.top
- - mLayout.getMeasuredHeight();
- updateSurfacePosition(positionX, positionY);
+ updateSurfacePosition(mLayoutBounds.left, mLayoutBounds.top);
+ }
+
+ @Override
+ @VisibleForTesting
+ public void updateSurfacePosition(@NonNull SurfaceControl.Transaction tx) {
+ updateLayoutBounds();
+ if (mLayoutBounds.isEmpty()) {
+ return;
+ }
+ updateSurfaceBounds(tx, mLayoutBounds);
}
@VisibleForTesting
@@ -219,6 +227,23 @@
return percentageAreaOfLetterboxInTask < mHideScmTolerance;
}
+ private void updateLayoutBounds() {
+ if (mLayout == null) {
+ mLayoutBounds.setEmpty();
+ return;
+ }
+ // Position of the button in the container coordinate.
+ final Rect taskBounds = getTaskBounds();
+ final Rect taskStableBounds = getTaskStableBounds();
+ final int layoutWidth = mLayout.getMeasuredWidth();
+ final int layoutHeight = mLayout.getMeasuredHeight();
+ final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+ ? taskStableBounds.left - taskBounds.left
+ : taskStableBounds.right - taskBounds.left - layoutWidth;
+ final int positionY = taskStableBounds.bottom - taskBounds.top - layoutHeight;
+ mLayoutBounds.set(positionX, positionY, positionX + layoutWidth, positionY + layoutHeight);
+ }
+
private void updateVisibilityOfViews() {
if (mLayout == null) {
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
index d2b4f1a..82acfe5 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIWindowManagerAbstract.java
@@ -43,6 +43,7 @@
import android.view.WindowlessWindowManager;
import com.android.internal.annotations.VisibleForTesting;
+import com.android.window.flags.Flags;
import com.android.wm.shell.ShellTaskOrganizer;
import com.android.wm.shell.common.DisplayLayout;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -327,8 +328,15 @@
if (mViewHost == null) {
return;
}
- mViewHost.relayout(windowLayoutParams);
- updateSurfacePosition();
+ if (Flags.appCompatAsyncRelayout()) {
+ mViewHost.relayout(windowLayoutParams, tx -> {
+ updateSurfacePosition(tx);
+ tx.apply();
+ });
+ } else {
+ mViewHost.relayout(windowLayoutParams);
+ updateSurfacePosition();
+ }
}
@NonNull
@@ -349,6 +357,10 @@
*/
protected abstract void updateSurfacePosition();
+ protected void updateSurfacePosition(@NonNull SurfaceControl.Transaction tx) {
+
+ }
+
/**
* Updates the position of the surface with respect to the given {@code positionX} and {@code
* positionY}.
@@ -366,6 +378,15 @@
});
}
+ protected void updateSurfaceBounds(@NonNull SurfaceControl.Transaction tx,
+ @NonNull Rect bounds) {
+ if (mLeash == null) {
+ return;
+ }
+ tx.setPosition(mLeash, bounds.left, bounds.top)
+ .setWindowCrop(mLeash, bounds.width(), bounds.height());
+ }
+
protected int getLayoutDirection() {
return mContext.getResources().getConfiguration().getLayoutDirection();
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
index 3f67172..650d2170 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManager.java
@@ -27,6 +27,7 @@
import android.graphics.Rect;
import android.os.SystemClock;
import android.view.LayoutInflater;
+import android.view.SurfaceControl;
import android.view.View;
import android.view.accessibility.AccessibilityManager;
@@ -69,6 +70,9 @@
@NonNull
final CompatUIHintsState mCompatUIHintsState;
+ @NonNull
+ private final Rect mLayoutBounds = new Rect();
+
@Nullable
private UserAspectRatioSettingsLayout mLayout;
@@ -108,6 +112,7 @@
@Override
protected void removeLayout() {
+ mLayoutBounds.setEmpty();
mLayout = null;
}
@@ -168,18 +173,21 @@
@Override
@VisibleForTesting
public void updateSurfacePosition() {
- if (mLayout == null) {
+ updateLayoutBounds();
+ if (mLayoutBounds.isEmpty()) {
return;
}
- // Position of the button in the container coordinate.
- final Rect taskBounds = getTaskBounds();
- final Rect taskStableBounds = getTaskStableBounds();
- final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
- ? taskStableBounds.left - taskBounds.left
- : taskStableBounds.right - taskBounds.left - mLayout.getMeasuredWidth();
- final int positionY = taskStableBounds.bottom - taskBounds.top
- - mLayout.getMeasuredHeight();
- updateSurfacePosition(positionX, positionY);
+ updateSurfacePosition(mLayoutBounds.left, mLayoutBounds.top);
+ }
+
+ @Override
+ @VisibleForTesting
+ public void updateSurfacePosition(@NonNull SurfaceControl.Transaction tx) {
+ updateLayoutBounds();
+ if (mLayoutBounds.isEmpty()) {
+ return;
+ }
+ updateSurfaceBounds(tx, mLayoutBounds);
}
@VisibleForTesting
@@ -202,6 +210,23 @@
&& !isHideDelayReached(mNextButtonHideTimeMs));
}
+ private void updateLayoutBounds() {
+ if (mLayout == null) {
+ mLayoutBounds.setEmpty();
+ return;
+ }
+ // Position of the button in the container coordinate.
+ final Rect taskBounds = getTaskBounds();
+ final Rect taskStableBounds = getTaskStableBounds();
+ final int layoutWidth = mLayout.getMeasuredWidth();
+ final int layoutHeight = mLayout.getMeasuredHeight();
+ final int positionX = getLayoutDirection() == View.LAYOUT_DIRECTION_RTL
+ ? taskStableBounds.left - taskBounds.left
+ : taskStableBounds.right - taskBounds.left - layoutWidth;
+ final int positionY = taskStableBounds.bottom - taskBounds.top - layoutHeight;
+ mLayoutBounds.set(positionX, positionY, positionX + layoutWidth, positionY + layoutHeight);
+ }
+
private void showUserAspectRatioButton() {
if (mLayout == null) {
return;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
index 36904fb5..7764688 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt
@@ -23,6 +23,7 @@
import android.os.IBinder
import android.view.SurfaceControl
import android.view.WindowManager
+import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
import android.window.DesktopModeFlags
import android.window.TransitionInfo
@@ -197,8 +198,9 @@
logW("Should have closing desktop task")
return false
}
- if (isLastDesktopTask(closeChange)) {
- // Dispatch close desktop task animation to the default transition handlers.
+ if (isWallpaperActivityClosing(info)) {
+ // If the wallpaper activity is closing then the desktop is closing, animate the closing
+ // desktop by dispatching to other transition handlers.
return dispatchCloseLastDesktopTaskAnimation(
transition,
info,
@@ -419,10 +421,12 @@
) != null
}
- private fun isLastDesktopTask(change: TransitionInfo.Change): Boolean =
- change.taskInfo?.let {
- desktopUserRepositories.getProfile(it.userId).getExpandedTaskCount(it.displayId) == 1
- } ?: false
+ private fun isWallpaperActivityClosing(info: TransitionInfo) =
+ info.changes.any { change ->
+ change.mode == TRANSIT_CLOSE &&
+ change.taskInfo != null &&
+ DesktopWallpaperActivity.isWallpaperTask(change.taskInfo!!)
+ }
private fun findCloseDesktopTaskChange(info: TransitionInfo): TransitionInfo.Change? {
if (info.type != WindowManager.TRANSIT_CLOSE) return null
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
index 267bbb6..49a7e29 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt
@@ -21,6 +21,7 @@
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN
import android.app.WindowConfiguration.WindowingMode
+import android.content.Intent
import android.os.Binder
import android.os.Handler
import android.os.IBinder
@@ -36,7 +37,9 @@
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
import android.view.WindowManager.TransitionType
+import android.window.IWindowContainerToken
import android.window.TransitionInfo
+import android.window.WindowContainerToken
import android.window.WindowContainerTransaction
import androidx.test.filters.SmallTest
import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE
@@ -188,7 +191,7 @@
fun startAnimation_withoutClosingDesktopTask_returnsFalse() {
val transition = mock<IBinder>()
val transitionInfo =
- createTransitionInfo(
+ createCloseTransitionInfo(
changeMode = TRANSIT_OPEN,
task = createTask(WINDOWING_MODE_FREEFORM)
)
@@ -213,8 +216,7 @@
fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() {
val wct = WindowContainerTransaction()
val transition = mock<IBinder>()
- val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
- whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2)
+ val transitionInfo = createCloseTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
whenever(
closeDesktopTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any())
)
@@ -243,8 +245,8 @@
fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() {
val wct = WindowContainerTransaction()
val transition = mock<IBinder>()
- val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM))
- whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(1)
+ val transitionInfo = createCloseTransitionInfo(
+ task = createTask(WINDOWING_MODE_FREEFORM), withWallpaper = true)
whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any()))
.thenReturn(mock())
whenever(transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler))
@@ -355,7 +357,7 @@
val otherChange = createChange(createTask(WINDOWING_MODE_FREEFORM))
mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(launchTaskChange, otherChange)
),
@@ -395,7 +397,7 @@
val immersiveChange = createChange(immersiveTask)
mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(launchTaskChange, immersiveChange)
),
@@ -437,7 +439,7 @@
)
mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(launchTaskChange)
),
@@ -471,7 +473,7 @@
)
mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(launchTaskChange, minimizeChange)
),
@@ -505,7 +507,7 @@
val started = mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(nonLaunchTaskChange)
),
@@ -535,7 +537,7 @@
val started = mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(createChange(task, mode = TRANSIT_OPEN))
),
@@ -569,7 +571,7 @@
val openingChange = createChange(openingTask, mode = TRANSIT_OPEN)
val started = mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(immersiveChange, openingChange)
),
@@ -604,7 +606,7 @@
)
mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(launchTaskChange)
),
@@ -640,7 +642,7 @@
)
mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(launchTaskChange, minimizeChange)
),
@@ -670,7 +672,7 @@
val launchTaskChange = createChange(launchingTask)
mixedHandler.startAnimation(
transition,
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_OPEN,
listOf(launchTaskChange)
),
@@ -727,7 +729,7 @@
val started = mixedHandler.startAnimation(
transition = transition,
info =
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_TO_BACK,
listOf(minimizingTaskChange)
),
@@ -766,7 +768,7 @@
mixedHandler.startAnimation(
transition = transition,
info =
- createTransitionInfo(
+ createCloseTransitionInfo(
TRANSIT_TO_BACK,
listOf(minimizingTaskChange)
),
@@ -786,12 +788,12 @@
)
}
- private fun createTransitionInfo(
- type: Int = WindowManager.TRANSIT_CLOSE,
+ private fun createCloseTransitionInfo(
changeMode: Int = WindowManager.TRANSIT_CLOSE,
- task: RunningTaskInfo
+ task: RunningTaskInfo,
+ withWallpaper: Boolean = false,
): TransitionInfo =
- TransitionInfo(type, 0 /* flags */).apply {
+ TransitionInfo(WindowManager.TRANSIT_CLOSE, 0 /* flags */).apply {
addChange(
TransitionInfo.Change(mock(), closingTaskLeash).apply {
mode = changeMode
@@ -799,9 +801,18 @@
taskInfo = task
}
)
+ if (withWallpaper) {
+ addChange(
+ TransitionInfo.Change(/* container= */ mock(), /* leash= */ mock()).apply {
+ mode = WindowManager.TRANSIT_CLOSE
+ parent = null
+ taskInfo = createWallpaperTask()
+ }
+ )
+ }
}
- private fun createTransitionInfo(
+ private fun createCloseTransitionInfo(
@TransitionType type: Int,
changes: List<TransitionInfo.Change> = emptyList()
): TransitionInfo = TransitionInfo(type, /* flags= */ 0).apply {
@@ -822,4 +833,13 @@
.setActivityType(ACTIVITY_TYPE_STANDARD)
.setWindowingMode(windowingMode)
.build()
+
+ private fun createWallpaperTask() =
+ RunningTaskInfo().apply {
+ token = WindowContainerToken(mock<IWindowContainerToken>())
+ baseIntent =
+ Intent().apply {
+ component = DesktopWallpaperActivity.wallpaperActivityComponent
+ }
+ }
}
diff --git a/media/java/android/media/AudioFormat.java b/media/java/android/media/AudioFormat.java
index 0da8371b..c72a74e 100644
--- a/media/java/android/media/AudioFormat.java
+++ b/media/java/android/media/AudioFormat.java
@@ -714,7 +714,7 @@
/**
* @hide
* Return a channel mask ready to be used by native code
- * @param mask a combination of the CHANNEL_OUT_* definitions, but not CHANNEL_OUT_DEFAULT
+ * @param javaMask a combination of the CHANNEL_OUT_* definitions, but not CHANNEL_OUT_DEFAULT
* @return a native channel mask
*/
public static int convertChannelOutMaskToNativeMask(int javaMask) {
@@ -724,13 +724,98 @@
/**
* @hide
* Return a java output channel mask
- * @param mask a native channel mask
+ * @param nativeMask a native channel mask
* @return a combination of the CHANNEL_OUT_* definitions
*/
public static int convertNativeChannelMaskToOutMask(int nativeMask) {
return (nativeMask << 2);
}
+ /**
+ * @hide
+ * Return a human-readable string from a java channel mask
+ * @param javaMask a bit field of CHANNEL_OUT_* values
+ * @return a string in the "mono", "stereo", "5.1" style, or the hex version when not a standard
+ * mask.
+ */
+ public static String javaChannelOutMaskToString(int javaMask) {
+ // save haptics info for end of string
+ int haptics = javaMask & (CHANNEL_OUT_HAPTIC_A | CHANNEL_OUT_HAPTIC_B);
+ // continue without looking at haptic channels
+ javaMask &= ~(CHANNEL_OUT_HAPTIC_A | CHANNEL_OUT_HAPTIC_B);
+ StringBuilder result = new StringBuilder("");
+ switch (javaMask) {
+ case CHANNEL_OUT_MONO:
+ result.append("mono");
+ break;
+ case CHANNEL_OUT_STEREO:
+ result.append("stereo");
+ break;
+ case CHANNEL_OUT_QUAD:
+ result.append("quad");
+ break;
+ case CHANNEL_OUT_QUAD_SIDE:
+ result.append("quad side");
+ break;
+ case CHANNEL_OUT_SURROUND:
+ result.append("4.0");
+ break;
+ case CHANNEL_OUT_5POINT1:
+ result.append("5.1");
+ break;
+ case CHANNEL_OUT_6POINT1:
+ result.append("6.1");
+ break;
+ case CHANNEL_OUT_5POINT1_SIDE:
+ result.append("5.1 side");
+ break;
+ case CHANNEL_OUT_7POINT1:
+ result.append("7.1 (5 fronts)");
+ break;
+ case CHANNEL_OUT_7POINT1_SURROUND:
+ result.append("7.1");
+ break;
+ case CHANNEL_OUT_5POINT1POINT2:
+ result.append("5.1.2");
+ break;
+ case CHANNEL_OUT_5POINT1POINT4:
+ result.append("5.1.4");
+ break;
+ case CHANNEL_OUT_7POINT1POINT2:
+ result.append("7.1.2");
+ break;
+ case CHANNEL_OUT_7POINT1POINT4:
+ result.append("7.1.4");
+ break;
+ case CHANNEL_OUT_9POINT1POINT4:
+ result.append("9.1.4");
+ break;
+ case CHANNEL_OUT_9POINT1POINT6:
+ result.append("9.1.6");
+ break;
+ case CHANNEL_OUT_13POINT_360RA:
+ result.append("360RA 13ch");
+ break;
+ case CHANNEL_OUT_22POINT2:
+ result.append("22.2");
+ break;
+ default:
+ result.append("0x").append(Integer.toHexString(javaMask));
+ break;
+ }
+ if ((haptics & (CHANNEL_OUT_HAPTIC_A | CHANNEL_OUT_HAPTIC_B)) != 0) {
+ result.append("(+haptic ");
+ if ((haptics & CHANNEL_OUT_HAPTIC_A) == CHANNEL_OUT_HAPTIC_A) {
+ result.append("A");
+ }
+ if ((haptics & CHANNEL_OUT_HAPTIC_B) == CHANNEL_OUT_HAPTIC_B) {
+ result.append("B");
+ }
+ result.append(")");
+ }
+ return result.toString();
+ }
+
public static final int CHANNEL_IN_DEFAULT = 1;
// These directly match native
public static final int CHANNEL_IN_LEFT = 0x4;
diff --git a/media/java/android/media/IMediaRoute2ProviderServiceCallback.aidl b/media/java/android/media/IMediaRoute2ProviderServiceCallback.aidl
index 63c52a1..8f03057 100644
--- a/media/java/android/media/IMediaRoute2ProviderServiceCallback.aidl
+++ b/media/java/android/media/IMediaRoute2ProviderServiceCallback.aidl
@@ -25,7 +25,6 @@
* @hide
*/
oneway interface IMediaRoute2ProviderServiceCallback {
- // TODO: Change it to updateRoutes?
void notifyProviderUpdated(in MediaRoute2ProviderInfo providerInfo);
void notifySessionCreated(long requestId, in RoutingSessionInfo sessionInfo);
void notifySessionsUpdated(in List<RoutingSessionInfo> sessionInfo);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java
new file mode 100644
index 0000000..7a64965
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManager.java
@@ -0,0 +1,408 @@
+/*
+ * 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.settingslib.bluetooth;
+
+import static com.android.settingslib.bluetooth.HearingDeviceLocalDataManager.Data.INVALID_VOLUME;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.ArrayMap;
+import android.util.KeyValueListParser;
+import android.util.Log;
+
+import androidx.annotation.GuardedBy;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.settingslib.utils.ThreadUtils;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * The class to manage hearing device local data from Settings.
+ *
+ * <p><b>Note:</b> Before calling any methods to get or change the local data, you must first call
+ * the {@code start()} method to load the data from Settings. Whenever the data is modified, you
+ * must call the {@code stop()} method to save the data into Settings. After calling {@code stop()},
+ * you should not call any methods to get or change the local data without again calling
+ * {@code start()}.
+ */
+public class HearingDeviceLocalDataManager {
+ private static final String TAG = "HearingDeviceDataMgr";
+ private static final boolean DEBUG = true;
+
+ /** Interface for listening hearing device local data changed */
+ public interface OnDeviceLocalDataChangeListener {
+ /**
+ * The method is called when the local data of the device with the address is changed.
+ *
+ * @param address the device anonymized address
+ * @param data the updated data
+ */
+ void onDeviceLocalDataChange(@NonNull String address, @Nullable Data data);
+ }
+
+ static final String KEY_ADDR = "addr";
+ static final String KEY_AMBIENT = "ambient";
+ static final String KEY_GROUP_AMBIENT = "group_ambient";
+ static final String KEY_AMBIENT_CONTROL_EXPANDED = "control_expanded";
+ static final String LOCAL_AMBIENT_VOLUME_SETTINGS =
+ Settings.Global.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME;
+
+ private static final Object sLock = new Object();
+
+ private final Context mContext;
+ private Executor mListenerExecutor;
+ @GuardedBy("sLock")
+ private final Map<String, Data> mAddrToDataMap = new HashMap<>();
+ private OnDeviceLocalDataChangeListener mListener;
+ private SettingsObserver mSettingsObserver;
+ private boolean mIsStarted = false;
+
+ public HearingDeviceLocalDataManager(@NonNull Context context) {
+ mContext = context;
+ mSettingsObserver = new SettingsObserver(ThreadUtils.getUiThreadHandler());
+ }
+
+ /** Starts the manager. Loads the data from Settings and start observing any changes. */
+ public synchronized void start() {
+ if (mIsStarted) {
+ return;
+ }
+ mIsStarted = true;
+ getLocalDataFromSettings();
+ mSettingsObserver.register(mContext.getContentResolver());
+ }
+
+ /** Stops the manager. Flushes the data into Settings and stop observing. */
+ public synchronized void stop() {
+ if (!mIsStarted) {
+ return;
+ }
+ putAmbientVolumeSettings();
+ mSettingsObserver.unregister(mContext.getContentResolver());
+ mIsStarted = false;
+ }
+
+ /**
+ * Sets a listener which will be be notified when hearing device local data is changed.
+ *
+ * @param listener the listener to be notified
+ * @param executor the executor to run the
+ * {@link OnDeviceLocalDataChangeListener#onDeviceLocalDataChange(String,
+ * Data)} callback
+ */
+ public void setOnDeviceLocalDataChangeListener(
+ @NonNull OnDeviceLocalDataChangeListener listener, @NonNull Executor executor) {
+ mListener = listener;
+ mListenerExecutor = executor;
+ }
+
+ /**
+ * Gets the local data of the corresponding hearing device. This should be called after
+ * {@link #start()} is called().
+ *
+ * @param device the device to query the local data
+ */
+ @NonNull
+ public Data get(@NonNull BluetoothDevice device) {
+ if (!mIsStarted) {
+ Log.w(TAG, "Manager is not started. Please call start() first.");
+ return new Data();
+ }
+ synchronized (sLock) {
+ return mAddrToDataMap.getOrDefault(device.getAnonymizedAddress(), new Data());
+ }
+ }
+
+ /**
+ * Puts the local data of the corresponding hearing device.
+ *
+ * @param device the device to update the local data
+ */
+ private void put(BluetoothDevice device, Data data) {
+ if (device == null) {
+ return;
+ }
+ synchronized (sLock) {
+ final String addr = device.getAnonymizedAddress();
+ mAddrToDataMap.put(addr, data);
+ if (mListener != null && mListenerExecutor != null) {
+ mListenerExecutor.execute(() -> mListener.onDeviceLocalDataChange(addr, data));
+ }
+ }
+ }
+
+ /**
+ * Updates the ambient volume of the corresponding hearing device. This should be called after
+ * {@link #start()} is called().
+ *
+ * @param device the device to update
+ * @param value the ambient value
+ * @return if the local data is updated
+ */
+ public boolean updateAmbient(@Nullable BluetoothDevice device, int value) {
+ if (!mIsStarted) {
+ Log.w(TAG, "Manager is not started. Please call start() first.");
+ return false;
+ }
+ if (device == null) {
+ return false;
+ }
+ synchronized (sLock) {
+ Data data = get(device);
+ if (value == data.ambient) {
+ return false;
+ }
+ put(device, new Data.Builder(data).ambient(value).build());
+ return true;
+ }
+ }
+
+ /**
+ * Updates the group ambient volume of the corresponding hearing device. This should be called
+ * after {@link #start()} is called().
+ *
+ * @param device the device to update
+ * @param value the group ambient value
+ * @return if the local data is updated
+ */
+ public boolean updateGroupAmbient(@Nullable BluetoothDevice device, int value) {
+ if (!mIsStarted) {
+ Log.w(TAG, "Manager is not started. Please call start() first.");
+ return false;
+ }
+ if (device == null) {
+ return false;
+ }
+ synchronized (sLock) {
+ Data data = get(device);
+ if (value == data.groupAmbient) {
+ return false;
+ }
+ put(device, new Data.Builder(data).groupAmbient(value).build());
+ return true;
+ }
+ }
+
+ /**
+ * Updates the ambient control is expanded or not of the corresponding hearing device. This
+ * should be called after {@link #start()} is called().
+ *
+ * @param device the device to update
+ * @param expanded the ambient control is expanded or not
+ * @return if the local data is updated
+ */
+ public boolean updateAmbientControlExpanded(@Nullable BluetoothDevice device,
+ boolean expanded) {
+ if (!mIsStarted) {
+ Log.w(TAG, "Manager is not started. Please call start() first.");
+ return false;
+ }
+ if (device == null) {
+ return false;
+ }
+ synchronized (sLock) {
+ Data data = get(device);
+ if (expanded == data.ambientControlExpanded) {
+ return false;
+ }
+ put(device, new Data.Builder(data).ambientControlExpanded(expanded).build());
+ return true;
+ }
+ }
+
+ void getLocalDataFromSettings() {
+ synchronized (sLock) {
+ Map<String, Data> updatedAddrToDataMap = parseFromSettings();
+ notifyIfDataChanged(mAddrToDataMap, updatedAddrToDataMap);
+ mAddrToDataMap.clear();
+ mAddrToDataMap.putAll(updatedAddrToDataMap);
+ if (DEBUG) {
+ Log.v(TAG, "getLocalDataFromSettings, " + mAddrToDataMap + ", manager: " + this);
+ }
+ }
+ }
+
+ void putAmbientVolumeSettings() {
+ synchronized (sLock) {
+ StringBuilder builder = new StringBuilder();
+ for (Map.Entry<String, Data> entry : mAddrToDataMap.entrySet()) {
+ builder.append(KEY_ADDR).append("=").append(entry.getKey());
+ builder.append(entry.getValue().toSettingsFormat()).append(";");
+ }
+ if (DEBUG) {
+ Log.v(TAG, "putAmbientVolumeSettings, " + builder + ", manager: " + this);
+ }
+ Settings.Global.putStringForUser(mContext.getContentResolver(),
+ LOCAL_AMBIENT_VOLUME_SETTINGS, builder.toString(),
+ UserHandle.USER_SYSTEM);
+ }
+ }
+
+ @GuardedBy("sLock")
+ private Map<String, Data> parseFromSettings() {
+ String settings = Settings.Global.getStringForUser(mContext.getContentResolver(),
+ LOCAL_AMBIENT_VOLUME_SETTINGS, UserHandle.USER_SYSTEM);
+ Map<String, Data> addrToDataMap = new ArrayMap<>();
+ if (settings != null && !settings.isEmpty()) {
+ String[] localDataArray = settings.split(";");
+ for (String localData : localDataArray) {
+ KeyValueListParser parser = new KeyValueListParser(',');
+ parser.setString(localData);
+ String address = parser.getString(KEY_ADDR, "");
+ if (!address.isEmpty()) {
+ Data data = new Data.Builder()
+ .ambient(parser.getInt(KEY_AMBIENT, INVALID_VOLUME))
+ .groupAmbient(parser.getInt(KEY_GROUP_AMBIENT, INVALID_VOLUME))
+ .ambientControlExpanded(
+ parser.getBoolean(KEY_AMBIENT_CONTROL_EXPANDED, false))
+ .build();
+ addrToDataMap.put(address, data);
+ }
+ }
+ }
+ return addrToDataMap;
+ }
+
+ @GuardedBy("sLock")
+ private void notifyIfDataChanged(Map<String, Data> oldAddrToDataMap,
+ Map<String, Data> newAddrToDataMap) {
+ newAddrToDataMap.forEach((addr, data) -> {
+ Data oldData = oldAddrToDataMap.get(addr);
+ if (oldData == null || !oldData.equals(data)) {
+ if (mListener != null) {
+ mListenerExecutor.execute(() -> mListener.onDeviceLocalDataChange(addr, data));
+ }
+ }
+ });
+ }
+
+ private final class SettingsObserver extends ContentObserver {
+ private final Uri mAmbientVolumeUri = Settings.Global.getUriFor(
+ LOCAL_AMBIENT_VOLUME_SETTINGS);
+
+ SettingsObserver(Handler handler) {
+ super(handler);
+ }
+
+ void register(ContentResolver contentResolver) {
+ contentResolver.registerContentObserver(mAmbientVolumeUri, false, this,
+ UserHandle.USER_SYSTEM);
+ }
+
+ void unregister(ContentResolver contentResolver) {
+ contentResolver.unregisterContentObserver(this);
+ }
+
+ @Override
+ public void onChange(boolean selfChange, @Nullable Uri uri) {
+ if (mAmbientVolumeUri.equals(uri)) {
+ Log.v(TAG, "Local data on change, manager: " + HearingDeviceLocalDataManager.this);
+ getLocalDataFromSettings();
+ }
+ }
+ }
+
+ public record Data(int ambient, int groupAmbient, boolean ambientControlExpanded) {
+
+ public static int INVALID_VOLUME = Integer.MIN_VALUE;
+
+ private Data() {
+ this(INVALID_VOLUME, INVALID_VOLUME, false);
+ }
+
+ /**
+ * Return {@code true} if one of {@link #ambient} or {@link #groupAmbient} is assigned to
+ * a valid value.
+ */
+ public boolean hasAmbientData() {
+ return ambient != INVALID_VOLUME || groupAmbient != INVALID_VOLUME;
+ }
+
+ /**
+ * @return the composed string which is used to store the local data in
+ * {@link Settings.Global#HEARING_DEVICE_LOCAL_AMBIENT_VOLUME}
+ */
+ @NonNull
+ public String toSettingsFormat() {
+ String string = "";
+ if (ambient != INVALID_VOLUME) {
+ string += ("," + KEY_AMBIENT + "=" + ambient);
+ }
+ if (groupAmbient != INVALID_VOLUME) {
+ string += ("," + KEY_GROUP_AMBIENT + "=" + groupAmbient);
+ }
+ string += ("," + KEY_AMBIENT_CONTROL_EXPANDED + "=" + ambientControlExpanded);
+ return string;
+ }
+
+ /** Builder for a Data object */
+ public static final class Builder {
+ private int mAmbient;
+ private int mGroupAmbient;
+ private boolean mAmbientControlExpanded;
+
+ public Builder() {
+ this.mAmbient = INVALID_VOLUME;
+ this.mGroupAmbient = INVALID_VOLUME;
+ this.mAmbientControlExpanded = false;
+ }
+
+ public Builder(@NonNull Data other) {
+ this.mAmbient = other.ambient;
+ this.mGroupAmbient = other.groupAmbient;
+ this.mAmbientControlExpanded = other.ambientControlExpanded;
+ }
+
+ /** Sets the ambient volume */
+ @NonNull
+ public Builder ambient(int ambient) {
+ this.mAmbient = ambient;
+ return this;
+ }
+
+ /** Sets the group ambient volume */
+ @NonNull
+ public Builder groupAmbient(int groupAmbient) {
+ this.mGroupAmbient = groupAmbient;
+ return this;
+ }
+
+ /** Sets the ambient control expanded */
+ @NonNull
+ public Builder ambientControlExpanded(boolean ambientControlExpanded) {
+ this.mAmbientControlExpanded = ambientControlExpanded;
+ return this;
+ }
+
+ /** Build the Data object */
+ @NonNull
+ public Data build() {
+ return new Data(mAmbient, mGroupAmbient, mAmbientControlExpanded);
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
index ab7a3db..d85b92f 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/VolumeControlProfile.java
@@ -21,6 +21,7 @@
import android.annotation.CallbackExecutor;
import android.annotation.IntRange;
+import android.bluetooth.AudioInputControl;
import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
@@ -34,6 +35,7 @@
import androidx.annotation.RequiresApi;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
import java.util.concurrent.Executor;
@@ -168,6 +170,7 @@
}
mService.setVolumeOffset(device, volumeOffset);
}
+
/**
* Provides information about the possibility to set volume offset on the remote device. If the
* remote device supports Volume Offset Control Service, it is automatically connected.
@@ -210,6 +213,22 @@
mService.setDeviceVolume(device, volume, isGroupOp);
}
+ /**
+ * Returns a list of {@link AudioInputControl} objects associated with a Bluetooth device.
+ *
+ * @param device The remote Bluetooth device.
+ * @return A list of {@link AudioInputControl} objects, or an empty list if no AICS instances
+ * are found or if an error occurs.
+ * @hide
+ */
+ public @NonNull List<AudioInputControl> getAudioInputControlServices(
+ @NonNull BluetoothDevice device) {
+ if (mService == null) {
+ return Collections.emptyList();
+ }
+ return mService.getAudioInputControlServices(device);
+ }
+
@Override
public boolean accessProfileEnabled() {
return false;
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
index 23be7ba..496c3e6 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/data/repository/FakeZenModeRepository.kt
@@ -74,10 +74,6 @@
mutableModesFlow.value = mutableModesFlow.value.filter { it.id != id }
}
- fun replaceMode(modeId: String, mode: ZenMode) {
- mutableModesFlow.value = (mutableModesFlow.value.filter { it.id != modeId }) + mode
- }
-
fun clearModes() {
mutableModesFlow.value = listOf()
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 6842d0a..abc1638 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -41,32 +41,24 @@
private String mId;
private AutomaticZenRule mRule;
private ZenModeConfig.ZenRule mConfigZenRule;
+ private boolean mIsManualDnd;
public static final ZenMode EXAMPLE = new TestModeBuilder().build();
- public static final ZenMode MANUAL_DND_ACTIVE = manualDnd(Uri.EMPTY,
+ public static final ZenMode MANUAL_DND_ACTIVE = manualDnd(
INTERRUPTION_FILTER_PRIORITY, true);
- public static final ZenMode MANUAL_DND_INACTIVE = manualDnd(Uri.EMPTY,
+ public static final ZenMode MANUAL_DND_INACTIVE = manualDnd(
INTERRUPTION_FILTER_PRIORITY, false);
@NonNull
public static ZenMode manualDnd(@NotificationManager.InterruptionFilter int filter,
boolean isActive) {
- return manualDnd(Uri.EMPTY, filter, isActive);
- }
-
- private static ZenMode manualDnd(Uri conditionId,
- @NotificationManager.InterruptionFilter int filter, boolean isActive) {
- return ZenMode.manualDndMode(
- new AutomaticZenRule.Builder("Do Not Disturb", conditionId)
- .setInterruptionFilter(filter)
- .setType(AutomaticZenRule.TYPE_OTHER)
- .setManualInvocationAllowed(true)
- .setPackage(SystemZenRules.PACKAGE_ANDROID)
- .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
- .build(),
- isActive);
+ return new TestModeBuilder()
+ .makeManualDnd()
+ .setInterruptionFilter(filter)
+ .setActive(isActive)
+ .build();
}
public TestModeBuilder() {
@@ -91,6 +83,10 @@
mConfigZenRule.enabled = previous.getRule().isEnabled();
mConfigZenRule.pkg = previous.getRule().getPackageName();
setActive(previous.isActive());
+
+ if (previous.isManualDnd()) {
+ makeManualDnd();
+ }
}
public TestModeBuilder setId(String id) {
@@ -222,7 +218,25 @@
return this;
}
+ public TestModeBuilder makeManualDnd() {
+ mIsManualDnd = true;
+ // Set the "fixed" properties of a DND mode. Other things, such as policy/filter may be set
+ // separately or copied from a preexisting DND, so they are not overwritten here.
+ setId(ZenMode.MANUAL_DND_MODE_ID);
+ setName("Do Not Disturb");
+ setType(AutomaticZenRule.TYPE_OTHER);
+ setManualInvocationAllowed(true);
+ setPackage(SystemZenRules.PACKAGE_ANDROID);
+ setConditionId(Uri.EMPTY);
+ return this;
+ }
+
public ZenMode build() {
- return new ZenMode(mId, mRule, mConfigZenRule);
+ if (mIsManualDnd) {
+ return ZenMode.manualDndMode(mRule, mConfigZenRule.condition != null
+ && mConfigZenRule.condition.state == Condition.STATE_TRUE);
+ } else {
+ return new ZenMode(mId, mRule, mConfigZenRule);
+ }
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java
new file mode 100644
index 0000000..b659c02
--- /dev/null
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingDeviceLocalDataManagerTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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.settingslib.bluetooth;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.bluetooth.BluetoothDevice;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.os.UserHandle;
+import android.provider.Settings;
+
+import androidx.test.core.app.ApplicationProvider;
+
+import org.junit.Before;
+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 org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+import org.robolectric.shadows.ShadowSettings;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Tests for {@link HearingDeviceLocalDataManager}. */
+@RunWith(RobolectricTestRunner.class)
+@Config(shadows = {HearingDeviceLocalDataManagerTest.ShadowGlobal.class})
+public class HearingDeviceLocalDataManagerTest {
+
+ private static final String TEST_ADDRESS = "XX:XX:XX:XX:11:22";
+ private static final int TEST_AMBIENT = 10;
+ private static final int TEST_GROUP_AMBIENT = 20;
+ private static final boolean TEST_AMBIENT_CONTROL_EXPANDED = true;
+ private static final int TEST_UPDATED_AMBIENT = 30;
+ private static final int TEST_UPDATED_GROUP_AMBIENT = 40;
+ private static final boolean TEST_UPDATED_AMBIENT_CONTROL_EXPANDED = false;
+
+ @Rule
+ public MockitoRule mMockitoRule = MockitoJUnit.rule();
+ @Mock
+ private BluetoothDevice mDevice;
+ @Mock
+ private HearingDeviceLocalDataManager.OnDeviceLocalDataChangeListener mListener;
+
+ private final Context mContext = ApplicationProvider.getApplicationContext();
+ private HearingDeviceLocalDataManager mLocalDataManager;
+
+ @Before
+ public void setUp() {
+ prepareTestDataInSettings();
+ mLocalDataManager = new HearingDeviceLocalDataManager(mContext);
+ mLocalDataManager.start();
+ mLocalDataManager.setOnDeviceLocalDataChangeListener(mListener,
+ mContext.getMainExecutor());
+
+ when(mDevice.getAnonymizedAddress()).thenReturn(TEST_ADDRESS);
+ }
+
+ @Test
+ public void stop_verifyDataIsSaved() {
+ mLocalDataManager.updateAmbient(mDevice, TEST_UPDATED_AMBIENT);
+ mLocalDataManager.stop();
+
+ String settings = Settings.Global.getStringForUser(mContext.getContentResolver(),
+ Settings.Global.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME, UserHandle.USER_SYSTEM);
+ String expectedSettings = generateSettingsString(TEST_ADDRESS, TEST_UPDATED_AMBIENT,
+ TEST_GROUP_AMBIENT, TEST_AMBIENT_CONTROL_EXPANDED);
+ assertThat(settings).isEqualTo(expectedSettings);
+ }
+
+ @Test
+ public void get_correctDataFromSettings() {
+ HearingDeviceLocalDataManager.Data data = mLocalDataManager.get(mDevice);
+
+ assertThat(data.ambient()).isEqualTo(TEST_AMBIENT);
+ assertThat(data.groupAmbient()).isEqualTo(TEST_GROUP_AMBIENT);
+ assertThat(data.ambientControlExpanded()).isEqualTo(TEST_AMBIENT_CONTROL_EXPANDED);
+ }
+
+ @Test
+ public void updateAmbient_correctValue_listenerCalled() {
+ HearingDeviceLocalDataManager.Data oldData = mLocalDataManager.get(mDevice);
+ assertThat(oldData.ambient()).isEqualTo(TEST_AMBIENT);
+
+ mLocalDataManager.updateAmbient(mDevice, TEST_UPDATED_AMBIENT);
+
+ HearingDeviceLocalDataManager.Data newData = mLocalDataManager.get(mDevice);
+ assertThat(newData.ambient()).isEqualTo(TEST_UPDATED_AMBIENT);
+ verify(mListener).onDeviceLocalDataChange(TEST_ADDRESS, newData);
+ }
+
+ @Test
+ public void updateAmbient_sameValue_listenerNotCalled() {
+ HearingDeviceLocalDataManager.Data oldData = mLocalDataManager.get(mDevice);
+ assertThat(oldData.ambient()).isEqualTo(TEST_AMBIENT);
+
+ mLocalDataManager.updateAmbient(mDevice, TEST_AMBIENT);
+
+ HearingDeviceLocalDataManager.Data newData = mLocalDataManager.get(mDevice);
+ assertThat(newData.ambient()).isEqualTo(TEST_AMBIENT);
+ verify(mListener, never()).onDeviceLocalDataChange(any(), any());
+ }
+
+ @Test
+ public void updateGroupAmbient_correctValue_listenerCalled() {
+ HearingDeviceLocalDataManager.Data oldData = mLocalDataManager.get(mDevice);
+ assertThat(oldData.groupAmbient()).isEqualTo(TEST_GROUP_AMBIENT);
+
+ mLocalDataManager.updateGroupAmbient(mDevice, TEST_UPDATED_GROUP_AMBIENT);
+
+ HearingDeviceLocalDataManager.Data newData = mLocalDataManager.get(mDevice);
+ assertThat(newData.groupAmbient()).isEqualTo(TEST_UPDATED_GROUP_AMBIENT);
+ verify(mListener).onDeviceLocalDataChange(TEST_ADDRESS, newData);
+ }
+
+ @Test
+ public void updateGroupAmbient_sameValue_listenerNotCalled() {
+ HearingDeviceLocalDataManager.Data oldData = mLocalDataManager.get(mDevice);
+ assertThat(oldData.groupAmbient()).isEqualTo(TEST_GROUP_AMBIENT);
+
+ mLocalDataManager.updateGroupAmbient(mDevice, TEST_GROUP_AMBIENT);
+
+ HearingDeviceLocalDataManager.Data newData = mLocalDataManager.get(mDevice);
+ assertThat(newData.groupAmbient()).isEqualTo(TEST_GROUP_AMBIENT);
+ verify(mListener, never()).onDeviceLocalDataChange(any(), any());
+ }
+
+ @Test
+ public void updateAmbientControlExpanded_correctValue_listenerCalled() {
+ HearingDeviceLocalDataManager.Data oldData = mLocalDataManager.get(mDevice);
+ assertThat(oldData.ambientControlExpanded()).isEqualTo(TEST_AMBIENT_CONTROL_EXPANDED);
+
+ mLocalDataManager.updateAmbientControlExpanded(mDevice,
+ TEST_UPDATED_AMBIENT_CONTROL_EXPANDED);
+
+ HearingDeviceLocalDataManager.Data newData = mLocalDataManager.get(mDevice);
+ assertThat(newData.ambientControlExpanded()).isEqualTo(
+ TEST_UPDATED_AMBIENT_CONTROL_EXPANDED);
+ verify(mListener).onDeviceLocalDataChange(TEST_ADDRESS, newData);
+ }
+
+ @Test
+ public void updateAmbientControlExpanded_sameValue_listenerNotCalled() {
+ HearingDeviceLocalDataManager.Data oldData = mLocalDataManager.get(mDevice);
+ assertThat(oldData.ambientControlExpanded()).isEqualTo(TEST_AMBIENT_CONTROL_EXPANDED);
+
+ mLocalDataManager.updateAmbientControlExpanded(mDevice, TEST_AMBIENT_CONTROL_EXPANDED);
+
+ HearingDeviceLocalDataManager.Data newData = mLocalDataManager.get(mDevice);
+ assertThat(newData.ambientControlExpanded()).isEqualTo(TEST_AMBIENT_CONTROL_EXPANDED);
+ verify(mListener, never()).onDeviceLocalDataChange(any(), any());
+ }
+
+ @Test
+ public void getLocalDataFromSettings_dataChanged_correctValue_listenerCalled() {
+ HearingDeviceLocalDataManager.Data oldData = mLocalDataManager.get(mDevice);
+ assertThat(oldData.ambient()).isEqualTo(TEST_AMBIENT);
+ assertThat(oldData.groupAmbient()).isEqualTo(TEST_GROUP_AMBIENT);
+ assertThat(oldData.ambientControlExpanded()).isEqualTo(TEST_AMBIENT_CONTROL_EXPANDED);
+
+ prepareUpdatedDataInSettings();
+ mLocalDataManager.getLocalDataFromSettings();
+
+ HearingDeviceLocalDataManager.Data newData = mLocalDataManager.get(mDevice);
+ assertThat(newData.ambient()).isEqualTo(TEST_UPDATED_AMBIENT);
+ assertThat(newData.groupAmbient()).isEqualTo(TEST_UPDATED_GROUP_AMBIENT);
+ assertThat(newData.ambientControlExpanded()).isEqualTo(
+ TEST_UPDATED_AMBIENT_CONTROL_EXPANDED);
+ verify(mListener).onDeviceLocalDataChange(TEST_ADDRESS, newData);
+ }
+
+ private void prepareTestDataInSettings() {
+ String data = generateSettingsString(TEST_ADDRESS, TEST_AMBIENT, TEST_GROUP_AMBIENT,
+ TEST_AMBIENT_CONTROL_EXPANDED);
+ Settings.Global.putStringForUser(mContext.getContentResolver(),
+ Settings.Global.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME, data,
+ UserHandle.USER_SYSTEM);
+ }
+
+ private void prepareUpdatedDataInSettings() {
+ String data = generateSettingsString(TEST_ADDRESS, TEST_UPDATED_AMBIENT,
+ TEST_UPDATED_GROUP_AMBIENT, TEST_UPDATED_AMBIENT_CONTROL_EXPANDED);
+ Settings.Global.putStringForUser(mContext.getContentResolver(),
+ Settings.Global.HEARING_DEVICE_LOCAL_AMBIENT_VOLUME, data,
+ UserHandle.USER_SYSTEM);
+ }
+
+ private String generateSettingsString(String addr, int ambient, int groupAmbient,
+ boolean ambientControlExpanded) {
+ return "addr=" + addr + ",ambient=" + ambient + ",group_ambient=" + groupAmbient
+ + ",control_expanded=" + ambientControlExpanded + ";";
+ }
+
+ @Implements(value = Settings.Global.class)
+ public static class ShadowGlobal extends ShadowSettings.ShadowGlobal {
+ private static final Map<ContentResolver, Map<String, String>> sDataMap = new HashMap<>();
+
+ @Implementation
+ protected static boolean putStringForUser(
+ ContentResolver cr, String name, String value, int userHandle) {
+ get(cr).put(name, value);
+ return true;
+ }
+
+ @Implementation
+ protected static String getStringForUser(ContentResolver cr, String name, int userHandle) {
+ return get(cr).get(name);
+ }
+
+ private static Map<String, String> get(ContentResolver cr) {
+ return sDataMap.computeIfAbsent(cr, k -> new HashMap<>());
+ }
+ }
+}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/VolumeControlProfileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/VolumeControlProfileTest.java
index 9c518de..bd67394 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/VolumeControlProfileTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/VolumeControlProfileTest.java
@@ -24,6 +24,7 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.bluetooth.AudioInputControl;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothManager;
import android.bluetooth.BluetoothProfile;
@@ -45,6 +46,7 @@
import org.robolectric.annotation.Config;
import org.robolectric.shadow.api.Shadow;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executor;
@@ -248,4 +250,16 @@
verify(mService).isVolumeOffsetAvailable(mBluetoothDevice);
assertThat(available).isFalse();
}
+
+ @Test
+ public void getAudioInputControlServices_verifyIsCalledAndReturnNonNullList() {
+ mServiceListener.onServiceConnected(BluetoothProfile.VOLUME_CONTROL, mService);
+ when(mService.getAudioInputControlServices(mBluetoothDevice)).thenReturn(new ArrayList<>());
+
+ final List<AudioInputControl> controls = mProfile.getAudioInputControlServices(
+ mBluetoothDevice);
+
+ verify(mService).getAudioInputControlServices(mBluetoothDevice);
+ assertThat(controls).isNotNull();
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
index fcf4662..50ac2619 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/DoNotDisturbQuickAffordanceConfigTest.kt
@@ -405,7 +405,8 @@
testScope.runTest {
val lockScreenState by collectLastValue(underTest.lockScreenState)
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_INACTIVE)
+ val manualDnd = TestModeBuilder.MANUAL_DND_INACTIVE
+ zenModeRepository.addMode(manualDnd)
runCurrent()
assertThat(lockScreenState)
@@ -419,8 +420,7 @@
)
)
- zenModeRepository.removeMode(TestModeBuilder.MANUAL_DND_INACTIVE.id)
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+ zenModeRepository.activateMode(manualDnd)
runCurrent()
assertThat(lockScreenState)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
index 74d4178..4e33a59 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/ZenModeInteractorTest.kt
@@ -378,8 +378,7 @@
assertThat(dndMode!!.isActive).isFalse()
- zenModeRepository.removeMode(TestModeBuilder.MANUAL_DND_INACTIVE.id)
- zenModeRepository.addMode(TestModeBuilder.MANUAL_DND_ACTIVE)
+ zenModeRepository.activateMode(TestModeBuilder.MANUAL_DND_INACTIVE.id)
runCurrent()
assertThat(dndMode!!.isActive).isTrue()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
index 1e6e52a..d8184db 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModelTest.kt
@@ -83,7 +83,7 @@
assertThat(ringerViewModel).isInstanceOf(RingerViewModelState.Available::class.java)
assertThat((ringerViewModel as RingerViewModelState.Available).uiModel.drawerState)
- .isEqualTo(RingerDrawerState.Closed(normalRingerMode))
+ .isEqualTo(RingerDrawerState.Closed(normalRingerMode, normalRingerMode))
}
@Test
@@ -91,8 +91,9 @@
testScope.runTest {
val ringerViewModel by collectLastValue(underTest.ringerViewModel)
val vibrateRingerMode = RingerMode(RINGER_MODE_VIBRATE)
+ val normalRingerMode = RingerMode(RINGER_MODE_NORMAL)
- setUpRingerModeAndOpenDrawer(RingerMode(RINGER_MODE_NORMAL))
+ setUpRingerModeAndOpenDrawer(normalRingerMode)
// Select vibrate ringer mode.
underTest.onRingerButtonClicked(vibrateRingerMode)
controller.getState()
@@ -103,7 +104,8 @@
var uiModel = (ringerViewModel as RingerViewModelState.Available).uiModel
assertThat(uiModel.availableButtons[uiModel.currentButtonIndex]?.ringerMode)
.isEqualTo(vibrateRingerMode)
- assertThat(uiModel.drawerState).isEqualTo(RingerDrawerState.Closed(vibrateRingerMode))
+ assertThat(uiModel.drawerState)
+ .isEqualTo(RingerDrawerState.Closed(vibrateRingerMode, normalRingerMode))
val silentRingerMode = RingerMode(RINGER_MODE_SILENT)
// Open drawer
@@ -120,7 +122,8 @@
uiModel = (ringerViewModel as RingerViewModelState.Available).uiModel
assertThat(uiModel.availableButtons[uiModel.currentButtonIndex]?.ringerMode)
.isEqualTo(silentRingerMode)
- assertThat(uiModel.drawerState).isEqualTo(RingerDrawerState.Closed(silentRingerMode))
+ assertThat(uiModel.drawerState)
+ .isEqualTo(RingerDrawerState.Closed(silentRingerMode, vibrateRingerMode))
assertThat(controller.hasScheduledTouchFeedback).isFalse()
assertThat(vibratorHelper.totalVibrations).isEqualTo(2)
}
diff --git a/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml b/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml
index 877637e..1607121 100644
--- a/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml
+++ b/packages/SystemUI/res/xml/volume_dialog_ringer_drawer_motion_scene.xml
@@ -17,10 +17,10 @@
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<Transition
- android:id="@+id/transition"
+ android:id="@+id/close_to_open_transition"
app:constraintSetEnd="@+id/volume_dialog_ringer_drawer_open"
app:constraintSetStart="@+id/volume_dialog_ringer_drawer_close"
- app:transitionEasing="path(0.05f, 0.7f, 0.1f, 1f)"
+ app:transitionEasing="cubic(0.05, 0.7, 0.1, 1.0)"
app:duration="400">
</Transition>
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
index 1963ba2..82ac056 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/binder/VolumeDialogRingerViewBinder.kt
@@ -16,6 +16,8 @@
package com.android.systemui.volume.dialog.ringer.ui.binder
+import android.animation.ArgbEvaluator
+import android.graphics.drawable.GradientDrawable
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageButton
@@ -23,6 +25,10 @@
import androidx.compose.ui.util.fastForEachIndexed
import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.widget.ConstraintSet
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.FloatValueHolder
+import androidx.dynamicanimation.animation.SpringAnimation
+import androidx.dynamicanimation.animation.SpringForce
import com.android.internal.R as internalR
import com.android.settingslib.Utils
import com.android.systemui.lifecycle.WindowLifecycleState
@@ -31,24 +37,44 @@
import com.android.systemui.res.R
import com.android.systemui.util.children
import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonUiModel
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerButtonViewModel
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerDrawerState
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerViewModel
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.RingerViewModelState
import com.android.systemui.volume.dialog.ringer.ui.viewmodel.VolumeDialogRingerDrawerViewModel
+import com.android.systemui.volume.dialog.ui.utils.suspendAnimate
import javax.inject.Inject
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+private const val CLOSE_DRAWER_DELAY = 300L
@VolumeDialogScope
class VolumeDialogRingerViewBinder
@Inject
constructor(private val viewModelFactory: VolumeDialogRingerDrawerViewModel.Factory) {
+ private val roundnessSpringForce =
+ SpringForce(0F).apply {
+ stiffness = 800F
+ dampingRatio = 0.6F
+ }
+ private val colorSpringForce =
+ SpringForce(0F).apply {
+ stiffness = 3800F
+ dampingRatio = 1F
+ }
+ private val rgbEvaluator = ArgbEvaluator()
fun bind(view: View) {
with(view) {
val volumeDialogBackgroundView = requireViewById<View>(R.id.volume_dialog_background)
val drawerContainer = requireViewById<MotionLayout>(R.id.volume_ringer_drawer)
+ val unselectedButtonUiModel = RingerButtonUiModel.getUnselectedButton(context)
+ val selectedButtonUiModel = RingerButtonUiModel.getSelectedButton(context)
+
repeatWhenAttached {
viewModel(
traceName = "VolumeDialogRingerViewBinder",
@@ -61,26 +87,53 @@
is RingerViewModelState.Available -> {
val uiModel = ringerState.uiModel
- bindDrawerButtons(viewModel, uiModel)
-
// Set up view background and visibility
drawerContainer.visibility = View.VISIBLE
when (uiModel.drawerState) {
is RingerDrawerState.Initial -> {
+ drawerContainer.animateAndBindDrawerButtons(
+ viewModel,
+ uiModel,
+ selectedButtonUiModel,
+ unselectedButtonUiModel,
+ )
drawerContainer.closeDrawer(uiModel.currentButtonIndex)
volumeDialogBackgroundView.setBackgroundResource(
R.drawable.volume_dialog_background
)
}
is RingerDrawerState.Closed -> {
- drawerContainer.closeDrawer(uiModel.currentButtonIndex)
- volumeDialogBackgroundView.setBackgroundResource(
- R.drawable.volume_dialog_background
- )
+ if (
+ uiModel.selectedButton.ringerMode ==
+ uiModel.drawerState.currentMode
+ ) {
+ drawerContainer.animateAndBindDrawerButtons(
+ viewModel,
+ uiModel,
+ selectedButtonUiModel,
+ unselectedButtonUiModel,
+ ) {
+ drawerContainer.closeDrawer(
+ uiModel.currentButtonIndex
+ )
+ volumeDialogBackgroundView
+ .setBackgroundResource(
+ R.drawable.volume_dialog_background
+ )
+ }
+ }
}
is RingerDrawerState.Open -> {
+ drawerContainer.animateAndBindDrawerButtons(
+ viewModel,
+ uiModel,
+ selectedButtonUiModel,
+ unselectedButtonUiModel,
+ )
// Open drawer
- drawerContainer.transitionToEnd()
+ drawerContainer.transitionToState(
+ R.id.volume_dialog_ringer_drawer_open
+ )
if (
uiModel.currentButtonIndex !=
uiModel.availableButtons.size - 1
@@ -106,45 +159,93 @@
}
}
- private fun View.bindDrawerButtons(
+ private suspend fun MotionLayout.animateAndBindDrawerButtons(
viewModel: VolumeDialogRingerDrawerViewModel,
uiModel: RingerViewModel,
+ selectedButtonUiModel: RingerButtonUiModel,
+ unselectedButtonUiModel: RingerButtonUiModel,
+ onAnimationEnd: Runnable? = null,
) {
- val drawerContainer = requireViewById<MotionLayout>(R.id.volume_ringer_drawer)
- val count = uiModel.availableButtons.size
- drawerContainer.ensureChildCount(R.layout.volume_ringer_button, count)
+ ensureChildCount(R.layout.volume_ringer_button, uiModel.availableButtons.size)
+ if (
+ uiModel.drawerState is RingerDrawerState.Closed &&
+ uiModel.drawerState.currentMode != uiModel.drawerState.previousMode
+ ) {
+ val count = uiModel.availableButtons.size
+ val selectedButton =
+ getChildAt(count - uiModel.currentButtonIndex - 1)
+ .requireViewById<ImageButton>(R.id.volume_drawer_button)
+ val previousIndex =
+ uiModel.availableButtons.indexOfFirst {
+ it?.ringerMode == uiModel.drawerState.previousMode
+ }
+ val unselectedButton =
+ getChildAt(count - previousIndex - 1)
+ .requireViewById<ImageButton>(R.id.volume_drawer_button)
+ // On roundness animation end.
+ val roundnessAnimationEndListener =
+ DynamicAnimation.OnAnimationEndListener { _, _, _, _ ->
+ postDelayed(
+ { bindButtons(viewModel, uiModel, onAnimationEnd, isAnimated = true) },
+ CLOSE_DRAWER_DELAY,
+ )
+ }
+
+ // We only need to execute on roundness animation end once.
+ selectedButton.animateTo(selectedButtonUiModel, roundnessAnimationEndListener)
+ unselectedButton.animateTo(unselectedButtonUiModel)
+ } else {
+ bindButtons(viewModel, uiModel, onAnimationEnd)
+ }
+ }
+
+ private fun MotionLayout.bindButtons(
+ viewModel: VolumeDialogRingerDrawerViewModel,
+ uiModel: RingerViewModel,
+ onAnimationEnd: Runnable? = null,
+ isAnimated: Boolean = false,
+ ) {
+ val count = uiModel.availableButtons.size
uiModel.availableButtons.fastForEachIndexed { index, ringerButton ->
ringerButton?.let {
- val view = drawerContainer.getChildAt(count - index - 1)
- // TODO (b/369995871): object animator for button switch ( active <-> inactive )
+ val view = getChildAt(count - index - 1)
if (index == uiModel.currentButtonIndex) {
- view.bindDrawerButton(uiModel.selectedButton, viewModel, isSelected = true)
+ view.bindDrawerButton(
+ uiModel.selectedButton,
+ viewModel,
+ isSelected = true,
+ isAnimated = isAnimated,
+ )
} else {
- view.bindDrawerButton(it, viewModel)
+ view.bindDrawerButton(it, viewModel, isAnimated)
}
}
}
+ onAnimationEnd?.run()
}
private fun View.bindDrawerButton(
buttonViewModel: RingerButtonViewModel,
viewModel: VolumeDialogRingerDrawerViewModel,
isSelected: Boolean = false,
+ isAnimated: Boolean = false,
) {
with(requireViewById<ImageButton>(R.id.volume_drawer_button)) {
setImageResource(buttonViewModel.imageResId)
contentDescription = context.getString(buttonViewModel.contentDescriptionResId)
- if (isSelected) {
+ if (isSelected && !isAnimated) {
setBackgroundResource(R.drawable.volume_drawer_selection_bg)
setColorFilter(
Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnPrimary)
)
- } else {
+ background = background.mutate()
+ } else if (!isAnimated) {
setBackgroundResource(R.drawable.volume_ringer_item_bg)
setColorFilter(
Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnSurface)
)
+ background = background.mutate()
}
setOnClickListener {
viewModel.onRingerButtonClicked(buttonViewModel.ringerMode, isSelected)
@@ -171,9 +272,10 @@
}
private fun MotionLayout.closeDrawer(selectedIndex: Int) {
+ setTransition(R.id.close_to_open_transition)
cloneConstraintSet(R.id.volume_dialog_ringer_drawer_close)
.adjustClosedConstraintsForDrawer(selectedIndex, this)
- transitionToStart()
+ transitionToState(R.id.volume_dialog_ringer_drawer_close)
}
private fun ConstraintSet.adjustOpenConstraintsForDrawer(motionLayout: MotionLayout) {
@@ -263,4 +365,47 @@
connect(button.id, ConstraintSet.START, motionLayout.id, ConstraintSet.START)
connect(button.id, ConstraintSet.END, motionLayout.id, ConstraintSet.END)
}
+
+ private suspend fun ImageButton.animateTo(
+ ringerButtonUiModel: RingerButtonUiModel,
+ roundnessAnimationEndListener: DynamicAnimation.OnAnimationEndListener? = null,
+ ) {
+ val roundnessAnimation =
+ SpringAnimation(FloatValueHolder(0F)).setSpring(roundnessSpringForce)
+ val colorAnimation = SpringAnimation(FloatValueHolder(0F)).setSpring(colorSpringForce)
+ val radius = (background as GradientDrawable).cornerRadius
+ val cornerRadiusDiff =
+ ringerButtonUiModel.cornerRadius - (background as GradientDrawable).cornerRadius
+ val roundnessAnimationUpdateListener =
+ DynamicAnimation.OnAnimationUpdateListener { _, value, _ ->
+ (background as GradientDrawable).cornerRadius = radius + value * cornerRadiusDiff
+ background.invalidateSelf()
+ }
+ val colorAnimationUpdateListener =
+ DynamicAnimation.OnAnimationUpdateListener { _, value, _ ->
+ val currentIconColor =
+ rgbEvaluator.evaluate(
+ value.coerceIn(0F, 1F),
+ imageTintList?.colors?.first(),
+ ringerButtonUiModel.tintColor,
+ ) as Int
+ val currentBgColor =
+ rgbEvaluator.evaluate(
+ value.coerceIn(0F, 1F),
+ (background as GradientDrawable).color?.colors?.get(0),
+ ringerButtonUiModel.backgroundColor,
+ ) as Int
+
+ (background as GradientDrawable).setColor(currentBgColor)
+ background.invalidateSelf()
+ setColorFilter(currentIconColor)
+ }
+ coroutineScope {
+ launch { colorAnimation.suspendAnimate(colorAnimationUpdateListener) }
+ roundnessAnimation.suspendAnimate(
+ roundnessAnimationUpdateListener,
+ roundnessAnimationEndListener,
+ )
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerButtonUiModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerButtonUiModel.kt
new file mode 100644
index 0000000..3c46567
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerButtonUiModel.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.ringer.ui.viewmodel
+
+import android.content.Context
+import com.android.internal.R as internalR
+import com.android.settingslib.Utils
+import com.android.systemui.res.R
+
+/** Models the UI state of ringer button */
+data class RingerButtonUiModel(
+ /** Icon color. */
+ val tintColor: Int,
+ val backgroundColor: Int,
+ val cornerRadius: Int,
+) {
+ companion object {
+ fun getUnselectedButton(context: Context): RingerButtonUiModel {
+ return RingerButtonUiModel(
+ tintColor =
+ Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnSurface),
+ backgroundColor =
+ Utils.getColorAttrDefaultColor(
+ context,
+ internalR.attr.materialColorSurfaceContainerHighest,
+ ),
+ cornerRadius =
+ context.resources.getDimensionPixelSize(
+ R.dimen.volume_dialog_background_square_corner_radius
+ ),
+ )
+ }
+
+ fun getSelectedButton(context: Context): RingerButtonUiModel {
+ return RingerButtonUiModel(
+ tintColor =
+ Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorOnPrimary),
+ backgroundColor =
+ Utils.getColorAttrDefaultColor(context, internalR.attr.materialColorPrimary),
+ cornerRadius =
+ context.resources.getDimensionPixelSize(
+ R.dimen.volume_dialog_ringer_selected_button_background_radius
+ ),
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.kt
index f321837..afb3f68 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/RingerDrawerState.kt
@@ -25,7 +25,8 @@
data class Open(val mode: RingerMode) : RingerDrawerState
/** When clicked to close drawer */
- data class Closed(val mode: RingerMode) : RingerDrawerState
+ data class Closed(val currentMode: RingerMode, val previousMode: RingerMode) :
+ RingerDrawerState
/** Initial state when volume dialog is shown with a closed drawer. */
interface Initial : RingerDrawerState {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
index 624dcc7..45338e4 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ringer/ui/viewmodel/VolumeDialogRingerDrawerViewModel.kt
@@ -98,7 +98,10 @@
RingerDrawerState.Open(ringerMode)
}
is RingerDrawerState.Open -> {
- RingerDrawerState.Closed(ringerMode)
+ RingerDrawerState.Closed(
+ ringerMode,
+ (drawerState.value as RingerDrawerState.Open).mode,
+ )
}
is RingerDrawerState.Closed -> {
RingerDrawerState.Open(ringerMode)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
index c7f5801..10cf615 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/utils/SuspendAnimators.kt
@@ -20,6 +20,8 @@
import android.animation.AnimatorListenerAdapter
import android.animation.ValueAnimator
import android.view.ViewPropertyAnimator
+import androidx.dynamicanimation.animation.DynamicAnimation
+import androidx.dynamicanimation.animation.SpringAnimation
import kotlin.coroutines.resume
import kotlinx.coroutines.CancellableContinuation
import kotlinx.coroutines.suspendCancellableCoroutine
@@ -80,6 +82,27 @@
}
}
+/**
+ * Starts spring animation and suspends until it's finished. Cancels the animation if the running
+ * coroutine is cancelled.
+ */
+suspend fun SpringAnimation.suspendAnimate(
+ animationUpdateListener: DynamicAnimation.OnAnimationUpdateListener? = null,
+ animationEndListener: DynamicAnimation.OnAnimationEndListener? = null,
+) = suspendCancellableCoroutine { continuation ->
+ animationUpdateListener?.let(::addUpdateListener)
+ addEndListener { animation, canceled, value, velocity ->
+ continuation.resumeIfCan(Unit)
+ animationEndListener?.onAnimationEnd(animation, canceled, value, velocity)
+ }
+ animateToFinalPosition(1F)
+ continuation.invokeOnCancellation {
+ animationUpdateListener?.let(::removeUpdateListener)
+ animationEndListener?.let(::removeEndListener)
+ cancel()
+ }
+}
+
private fun <T> CancellableContinuation<T>.resumeIfCan(value: T) {
if (!isCancelled && !isCompleted) {
resume(value)
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
index 2aa6e7b..ae94544 100644
--- a/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/keyguard/ClockEventControllerTest.kt
@@ -24,7 +24,6 @@
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_ACTIVE
import com.android.settingslib.notification.modes.TestModeBuilder.MANUAL_DND_INACTIVE
import com.android.systemui.Flags as AConfigFlags
import com.android.systemui.SysuiTestCase
@@ -107,6 +106,7 @@
private lateinit var repository: FakeKeyguardRepository
private val clockBuffers = ClockMessageBuffers(LogcatOnlyMessageBuffer(LogLevel.DEBUG))
private lateinit var underTest: ClockEventController
+ private lateinit var dndModeId: String
@Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Mock private lateinit var batteryController: BatteryController
@@ -156,6 +156,7 @@
whenever(largeClockController.theme).thenReturn(ThemeConfig(true, null))
whenever(userTracker.userId).thenReturn(1)
+ dndModeId = MANUAL_DND_INACTIVE.id
zenModeRepository.addMode(MANUAL_DND_INACTIVE)
repository = FakeKeyguardRepository()
@@ -528,7 +529,7 @@
testScope.runTest {
underTest.listenForDnd(testScope.backgroundScope)
- zenModeRepository.replaceMode(MANUAL_DND_INACTIVE.id, MANUAL_DND_ACTIVE)
+ zenModeRepository.activateMode(dndModeId)
runCurrent()
verify(events)
@@ -536,7 +537,7 @@
eq(ZenData(ZenMode.IMPORTANT_INTERRUPTIONS, R.string::dnd_is_on.name))
)
- zenModeRepository.replaceMode(MANUAL_DND_ACTIVE.id, MANUAL_DND_INACTIVE)
+ zenModeRepository.deactivateMode(dndModeId)
runCurrent()
verify(events).onZenDataChanged(eq(ZenData(ZenMode.OFF, R.string::dnd_is_off.name)))
diff --git a/services/core/java/com/android/server/audio/SpatializerHelper.java b/services/core/java/com/android/server/audio/SpatializerHelper.java
index afa90d5..608edbb 100644
--- a/services/core/java/com/android/server/audio/SpatializerHelper.java
+++ b/services/core/java/com/android/server/audio/SpatializerHelper.java
@@ -129,6 +129,8 @@
/** current level as reported by native Spatializer in callback */
private int mSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
private int mCapableSpatLevel = Spatializer.SPATIALIZER_IMMERSIVE_LEVEL_NONE;
+ /** cached version of Spatializer.getSpatializedChannelMasks */
+ private List<Integer> mSpatializedChannelMasks = Collections.emptyList();
private boolean mTransauralSupported = false;
private boolean mBinauralSupported = false;
@@ -1030,6 +1032,17 @@
return;
}
try {
+ final int[] nativeMasks = mSpat.getSpatializedChannelMasks();
+ for (int i = 0; i < nativeMasks.length; i++) {
+ nativeMasks[i] = AudioFormat.convertNativeChannelMaskToOutMask(nativeMasks[i]);
+ }
+ mSpatializedChannelMasks = Arrays.stream(nativeMasks).boxed().toList();
+
+ } catch (Exception e) { // just catch Exception in case nativeMasks is null
+ Log.e(TAG, "Error calling getSpatializedChannelMasks", e);
+ mSpatializedChannelMasks = Collections.emptyList();
+ }
+ try {
//TODO: register heatracking callback only when sensors are registered
if (mIsHeadTrackingSupported) {
mActualHeadTrackingMode =
@@ -1103,20 +1116,7 @@
}
synchronized @NonNull List<Integer> getSpatializedChannelMasks() {
- if (!checkSpatializer("getSpatializedChannelMasks")) {
- return Collections.emptyList();
- }
- try {
- final int[] nativeMasks = new int[0]; // FIXME mSpat query goes here
- for (int i = 0; i < nativeMasks.length; i++) {
- nativeMasks[i] = AudioFormat.convertNativeChannelMaskToOutMask(nativeMasks[i]);
- }
- final List<Integer> masks = Arrays.stream(nativeMasks).boxed().toList();
- return masks;
- } catch (Exception e) { // just catch Exception in case nativeMasks is null
- Log.e(TAG, "Error calling getSpatializedChannelMasks", e);
- return Collections.emptyList();
- }
+ return mSpatializedChannelMasks;
}
//------------------------------------------------------
@@ -1622,6 +1622,14 @@
pw.println("\tmState:" + mState);
pw.println("\tmSpatLevel:" + mSpatLevel);
pw.println("\tmCapableSpatLevel:" + mCapableSpatLevel);
+ List<Integer> speakerMasks = getSpatializedChannelMasks();
+ StringBuilder masks = speakerMasks.isEmpty()
+ ? new StringBuilder("none") : new StringBuilder("");
+ for (Integer mask : speakerMasks) {
+ masks.append(AudioFormat.javaChannelOutMaskToString(mask)).append(" ");
+ }
+ pw.println("\tspatialized speaker masks: " + masks);
+
pw.println("\tmIsHeadTrackingSupported:" + mIsHeadTrackingSupported);
StringBuilder modesString = new StringBuilder();
for (int mode : mSupportedHeadTrackingModes) {
diff --git a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
index 55b292a..5b78726 100644
--- a/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
+++ b/services/core/java/com/android/server/display/DisplayTopologyCoordinator.java
@@ -100,13 +100,14 @@
*/
DisplayTopology getTopology() {
synchronized (mSyncRoot) {
- return mTopology;
+ return mTopology.copy();
}
}
void setTopology(DisplayTopology topology) {
synchronized (mSyncRoot) {
mTopology = topology;
+ mTopology.normalize();
sendTopologyUpdateLocked();
}
}
diff --git a/services/core/java/com/android/server/media/MediaSession2Record.java b/services/core/java/com/android/server/media/MediaSession2Record.java
index c8a8799..e7b79ab 100644
--- a/services/core/java/com/android/server/media/MediaSession2Record.java
+++ b/services/core/java/com/android/server/media/MediaSession2Record.java
@@ -16,7 +16,6 @@
package com.android.server.media;
-import android.app.ForegroundServiceDelegationOptions;
import android.app.Notification;
import android.media.MediaController2;
import android.media.Session2CommandGroup;
@@ -98,11 +97,8 @@
}
@Override
- public ForegroundServiceDelegationOptions getForegroundServiceDelegationOptions() {
- // For an app to be eligible for FGS delegation, it needs a media session liked to a media
- // notification. Currently, notifications cannot be linked to MediaSession2 so it is not
- // supported.
- return null;
+ public boolean hasLinkedNotificationSupport() {
+ return false;
}
@Override
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index 668ee2a..5f7c86f 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -29,7 +29,6 @@
import android.annotation.RequiresPermission;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
-import android.app.ForegroundServiceDelegationOptions;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.compat.CompatChanges;
@@ -184,8 +183,6 @@
private final UriGrantsManagerInternal mUgmInternal;
private final Context mContext;
- private final ForegroundServiceDelegationOptions mForegroundServiceDelegationOptions;
-
private final Object mLock = new Object();
// This field is partially guarded by mLock. Writes and non-atomic iterations (for example:
// index-based-iterations) must be guarded by mLock. But it is safe to acquire an iterator
@@ -306,32 +303,10 @@
mPolicies = policies;
mUgmInternal = LocalServices.getService(UriGrantsManagerInternal.class);
- mForegroundServiceDelegationOptions = createForegroundServiceDelegationOptions();
-
// May throw RemoteException if the session app is killed.
mSessionCb.mCb.asBinder().linkToDeath(this, 0);
}
- private ForegroundServiceDelegationOptions createForegroundServiceDelegationOptions() {
- return new ForegroundServiceDelegationOptions.Builder()
- .setClientPid(mOwnerPid)
- .setClientUid(getUid())
- .setClientPackageName(getPackageName())
- .setClientAppThread(null)
- .setSticky(false)
- .setClientInstanceName(
- "MediaSessionFgsDelegate_"
- + getUid()
- + "_"
- + mOwnerPid
- + "_"
- + getPackageName())
- .setForegroundServiceTypes(0)
- .setDelegationService(
- ForegroundServiceDelegationOptions.DELEGATION_SERVICE_MEDIA_PLAYBACK)
- .build();
- }
-
/**
* Get the session binder for the {@link MediaSession}.
*
@@ -389,6 +364,11 @@
return mUserId;
}
+ @Override
+ public boolean hasLinkedNotificationSupport() {
+ return true;
+ }
+
/**
* Check if this session has system priorty and should receive media buttons
* before any other sessions.
@@ -752,11 +732,6 @@
return mPackageName + "/" + mTag + "/" + getUniqueId() + " (userId=" + mUserId + ")";
}
- @Override
- public ForegroundServiceDelegationOptions getForegroundServiceDelegationOptions() {
- return mForegroundServiceDelegationOptions;
- }
-
private void postAdjustLocalVolume(final int stream, final int direction, final int flags,
final String callingOpPackageName, final int callingPid, final int callingUid,
final boolean asSystemService, final boolean useSuggested,
diff --git a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java
index 6c3b123..3966608 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecordImpl.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecordImpl.java
@@ -16,10 +16,8 @@
package com.android.server.media;
-import android.app.ForegroundServiceDelegationOptions;
import android.app.Notification;
import android.media.AudioManager;
-import android.media.session.PlaybackState;
import android.os.ResultReceiver;
import android.view.KeyEvent;
@@ -63,13 +61,14 @@
public abstract int getUserId();
/**
- * Get the {@link ForegroundServiceDelegationOptions} needed for notifying activity manager
- * service with changes in the {@link PlaybackState} for this session.
+ * Returns whether this session supports linked notifications.
*
- * @return the {@link ForegroundServiceDelegationOptions} needed for notifying the activity
- * manager service with changes in the {@link PlaybackState} for this session.
+ * <p>A notification is linked to a media session if it contains
+ * {@link android.app.Notification#EXTRA_MEDIA_SESSION}.
+ *
+ * @return {@code true} if this session supports linked notifications, {@code false} otherwise.
*/
- public abstract ForegroundServiceDelegationOptions getForegroundServiceDelegationOptions();
+ public abstract boolean hasLinkedNotificationSupport();
/**
* Check if this session has system priority and should receive media buttons before any other
diff --git a/services/core/java/com/android/server/media/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java
index 2b29fbd..e091fc6 100644
--- a/services/core/java/com/android/server/media/MediaSessionService.java
+++ b/services/core/java/com/android/server/media/MediaSessionService.java
@@ -30,7 +30,6 @@
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
-import android.app.ForegroundServiceDelegationOptions;
import android.app.KeyguardManager;
import android.app.Notification;
import android.app.NotificationManager;
@@ -185,9 +184,9 @@
/**
* Maps uid with all user engaged session records associated to it. It's used for calling
- * ActivityManagerInternal startFGS and stopFGS. This collection doesn't contain
- * MediaSession2Record(s). When the media session is paused, There exists a timeout before
- * calling stopFGS unlike usage logging which considers it disengaged immediately.
+ * ActivityManagerInternal internal api to set fgs active/inactive. This collection doesn't
+ * contain MediaSession2Record(s). When the media session is paused, There exists a timeout
+ * before setting FGS inactive unlike usage logging which considers it disengaged immediately.
*/
@GuardedBy("mLock")
private final Map<Integer, Set<MediaSessionRecordImpl>> mUserEngagedSessionsForFgs =
@@ -195,7 +194,7 @@
/* Maps uid with all media notifications associated to it */
@GuardedBy("mLock")
- private final Map<Integer, Set<Notification>> mMediaNotifications = new HashMap<>();
+ private final Map<Integer, Set<StatusBarNotification>> mMediaNotifications = new HashMap<>();
/**
* Holds all {@link MediaSessionRecordImpl} which we've reported as being {@link
@@ -700,10 +699,10 @@
MediaSessionRecordImpl mediaSessionRecord, boolean isUserEngaged) {
if (isUserEngaged) {
addUserEngagedSession(mediaSessionRecord);
- startFgsIfSessionIsLinkedToNotification(mediaSessionRecord);
+ setFgsActiveIfSessionIsLinkedToNotification(mediaSessionRecord);
} else {
removeUserEngagedSession(mediaSessionRecord);
- stopFgsIfNoSessionIsLinkedToNotification(mediaSessionRecord);
+ setFgsInactiveIfNoSessionIsLinkedToNotification(mediaSessionRecord);
}
}
@@ -737,17 +736,20 @@
}
}
- private void startFgsIfSessionIsLinkedToNotification(
+ private void setFgsActiveIfSessionIsLinkedToNotification(
MediaSessionRecordImpl mediaSessionRecord) {
- Log.d(TAG, "startFgsIfSessionIsLinkedToNotification: record=" + mediaSessionRecord);
+ Log.d(TAG, "setFgsIfSessionIsLinkedToNotification: record=" + mediaSessionRecord);
if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
return;
}
+ if (!mediaSessionRecord.hasLinkedNotificationSupport()) {
+ return;
+ }
synchronized (mLock) {
int uid = mediaSessionRecord.getUid();
- for (Notification mediaNotification : mMediaNotifications.getOrDefault(uid, Set.of())) {
- if (mediaSessionRecord.isLinkedToNotification(mediaNotification)) {
- startFgsDelegateLocked(mediaSessionRecord);
+ for (StatusBarNotification sbn : mMediaNotifications.getOrDefault(uid, Set.of())) {
+ if (mediaSessionRecord.isLinkedToNotification(sbn.getNotification())) {
+ setFgsActiveLocked(mediaSessionRecord, sbn);
return;
}
}
@@ -755,81 +757,92 @@
}
@GuardedBy("mLock")
- private void startFgsDelegateLocked(MediaSessionRecordImpl mediaSessionRecord) {
- ForegroundServiceDelegationOptions foregroundServiceDelegationOptions =
- mediaSessionRecord.getForegroundServiceDelegationOptions();
- if (foregroundServiceDelegationOptions == null) {
- return; // This record doesn't support FGS. Typically a MediaSession2 record.
- }
+ private void setFgsActiveLocked(MediaSessionRecordImpl mediaSessionRecord,
+ StatusBarNotification sbn) {
if (!mFgsAllowedMediaSessionRecords.add(mediaSessionRecord)) {
- return; // This record is already FGS-started.
+ return; // This record already is FGS-activated.
}
final long token = Binder.clearCallingIdentity();
try {
+ final String packageName = sbn.getPackageName();
+ final int uid = sbn.getUid();
+ final int notificationId = sbn.getId();
Log.i(
TAG,
TextUtils.formatSimple(
- "startFgsDelegate: pkg=%s uid=%d",
- foregroundServiceDelegationOptions.mClientPackageName,
- foregroundServiceDelegationOptions.mClientUid));
- mActivityManagerInternal.startForegroundServiceDelegate(
- foregroundServiceDelegationOptions, /* connection= */ null);
+ "setFgsActiveLocked: pkg=%s uid=%d notification=%d",
+ packageName, uid, notificationId));
+ mActivityManagerInternal.notifyActiveMediaForegroundService(packageName,
+ sbn.getUser().getIdentifier(), notificationId);
} finally {
Binder.restoreCallingIdentity(token);
}
}
- private void stopFgsIfNoSessionIsLinkedToNotification(
+ @Nullable
+ private StatusBarNotification getLinkedNotification(
+ int uid, MediaSessionRecordImpl record) {
+ synchronized (mLock) {
+ for (StatusBarNotification sbn :
+ mMediaNotifications.getOrDefault(uid, Set.of())) {
+ if (record.isLinkedToNotification(sbn.getNotification())) {
+ return sbn;
+ }
+ }
+ }
+ return null;
+ }
+
+ private void setFgsInactiveIfNoSessionIsLinkedToNotification(
MediaSessionRecordImpl mediaSessionRecord) {
- Log.d(TAG, "stopFgsIfNoSessionIsLinkedToNotification: record=" + mediaSessionRecord);
+ Log.d(TAG, "setFgsIfNoSessionIsLinkedToNotification: record=" + mediaSessionRecord);
if (!Flags.enableNotifyingActivityManagerWithMediaSessionStatusChange()) {
return;
}
+ if (!mediaSessionRecord.hasLinkedNotificationSupport()) {
+ return;
+ }
synchronized (mLock) {
- int uid = mediaSessionRecord.getUid();
- ForegroundServiceDelegationOptions foregroundServiceDelegationOptions =
- mediaSessionRecord.getForegroundServiceDelegationOptions();
- if (foregroundServiceDelegationOptions == null) {
- return;
- }
-
+ final int uid = mediaSessionRecord.getUid();
for (MediaSessionRecordImpl record :
mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) {
- for (Notification mediaNotification :
+ for (StatusBarNotification sbn :
mMediaNotifications.getOrDefault(uid, Set.of())) {
- if (record.isLinkedToNotification(mediaNotification)) {
+ if (record.isLinkedToNotification(sbn.getNotification())) {
// A user engaged session linked with a media notification is found.
// We shouldn't call stop FGS in this case.
return;
}
}
}
-
- stopFgsDelegateLocked(mediaSessionRecord);
+ final StatusBarNotification linkedNotification =
+ getLinkedNotification(uid, mediaSessionRecord);
+ if (linkedNotification != null) {
+ setFgsInactiveLocked(mediaSessionRecord, linkedNotification);
+ }
}
}
@GuardedBy("mLock")
- private void stopFgsDelegateLocked(MediaSessionRecordImpl mediaSessionRecord) {
- ForegroundServiceDelegationOptions foregroundServiceDelegationOptions =
- mediaSessionRecord.getForegroundServiceDelegationOptions();
- if (foregroundServiceDelegationOptions == null) {
- return; // This record doesn't support FGS. Typically a MediaSession2 record.
- }
+ private void setFgsInactiveLocked(MediaSessionRecordImpl mediaSessionRecord,
+ StatusBarNotification sbn) {
if (!mFgsAllowedMediaSessionRecords.remove(mediaSessionRecord)) {
- return; // This record is not FGS-started. No need to stop it.
+ return; // This record is not FGS-active. No need to set inactive.
}
final long token = Binder.clearCallingIdentity();
try {
+ final String packageName = sbn.getPackageName();
+ final int userId = sbn.getUser().getIdentifier();
+ final int uid = sbn.getUid();
+ final int notificationId = sbn.getId();
Log.i(
TAG,
TextUtils.formatSimple(
- "stopFgsDelegate: pkg=%s uid=%d",
- foregroundServiceDelegationOptions.mClientPackageName,
- foregroundServiceDelegationOptions.mClientUid));
- mActivityManagerInternal.stopForegroundServiceDelegate(
- foregroundServiceDelegationOptions);
+ "setFgsInactiveLocked: pkg=%s uid=%d notification=%d",
+ packageName, uid, notificationId));
+ mActivityManagerInternal.notifyInactiveMediaForegroundService(packageName,
+ userId, notificationId);
} finally {
Binder.restoreCallingIdentity(token);
}
@@ -3259,18 +3272,18 @@
@Override
public void onNotificationPosted(StatusBarNotification sbn) {
super.onNotificationPosted(sbn);
- Notification postedNotification = sbn.getNotification();
int uid = sbn.getUid();
+ final Notification postedNotification = sbn.getNotification();
if (!postedNotification.isMediaNotification()) {
return;
}
synchronized (mLock) {
mMediaNotifications.putIfAbsent(uid, new HashSet<>());
- mMediaNotifications.get(uid).add(postedNotification);
+ mMediaNotifications.get(uid).add(sbn);
for (MediaSessionRecordImpl mediaSessionRecord :
mUserEngagedSessionsForFgs.getOrDefault(uid, Set.of())) {
if (mediaSessionRecord.isLinkedToNotification(postedNotification)) {
- startFgsDelegateLocked(mediaSessionRecord);
+ setFgsActiveLocked(mediaSessionRecord, sbn);
return;
}
}
@@ -3286,9 +3299,9 @@
return;
}
synchronized (mLock) {
- Set<Notification> uidMediaNotifications = mMediaNotifications.get(uid);
+ Set<StatusBarNotification> uidMediaNotifications = mMediaNotifications.get(uid);
if (uidMediaNotifications != null) {
- uidMediaNotifications.remove(removedNotification);
+ uidMediaNotifications.remove(sbn);
if (uidMediaNotifications.isEmpty()) {
mMediaNotifications.remove(uid);
}
@@ -3300,8 +3313,7 @@
if (notificationRecord == null) {
return;
}
-
- stopFgsIfNoSessionIsLinkedToNotification(notificationRecord);
+ setFgsInactiveIfNoSessionIsLinkedToNotification(notificationRecord);
}
}
diff --git a/services/core/java/com/android/server/notification/GroupHelper.java b/services/core/java/com/android/server/notification/GroupHelper.java
index f914551..4b41696 100644
--- a/services/core/java/com/android/server/notification/GroupHelper.java
+++ b/services/core/java/com/android/server/notification/GroupHelper.java
@@ -59,6 +59,7 @@
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.function.Predicate;
@@ -243,7 +244,7 @@
if (!sbn.isAppGroup()) {
sbnToBeAutogrouped = maybeGroupWithSections(record, autogroupSummaryExists);
} else {
- maybeUngroupWithSections(record);
+ maybeUngroupOnAppGrouped(record);
}
} else {
final StatusBarNotification sbn = record.getSbn();
@@ -553,11 +554,13 @@
}
/**
- * A non-app grouped notification has been added or updated
+ * A non-app-grouped notification has been added or updated
* Evaluate if:
* (a) an existing autogroup summary needs updated attributes
* (b) a new autogroup summary needs to be added with correct attributes
* (c) other non-app grouped children need to be moved to the autogroup
+ * (d) the notification has been updated from a groupable to a non-groupable section and needs
+ * to trigger a cleanup
*
* This method implements autogrouping with sections support.
*
@@ -567,11 +570,11 @@
boolean autogroupSummaryExists) {
final StatusBarNotification sbn = record.getSbn();
boolean sbnToBeAutogrouped = false;
-
final NotificationSectioner sectioner = getSection(record);
if (sectioner == null) {
+ maybeUngroupOnNonGroupableUpdate(record);
if (DEBUG) {
- Log.i(TAG, "Skipping autogrouping for " + record + " no valid section found.");
+ Slog.i(TAG, "Skipping autogrouping for " + record + " no valid section found.");
}
return false;
}
@@ -584,7 +587,6 @@
if (record.getGroupKey().equals(fullAggregateGroupKey.toString())) {
return false;
}
-
synchronized (mAggregatedNotifications) {
ArrayMap<String, NotificationAttributes> ungrouped =
mUngroupedAbuseNotifications.getOrDefault(fullAggregateGroupKey, new ArrayMap<>());
@@ -601,11 +603,11 @@
if (ungrouped.size() >= mAutoGroupAtCount || autogroupSummaryExists) {
if (DEBUG) {
if (ungrouped.size() >= mAutoGroupAtCount) {
- Log.i(TAG,
+ Slog.i(TAG,
"Found >=" + mAutoGroupAtCount
+ " ungrouped notifications => force grouping");
} else {
- Log.i(TAG, "Found aggregate summary => force grouping");
+ Slog.i(TAG, "Found aggregate summary => force grouping");
}
}
@@ -642,7 +644,24 @@
}
/**
- * A notification was added that's app grouped.
+ * A notification was added that was previously part of a valid section and needs to trigger
+ * GH state cleanup.
+ */
+ private void maybeUngroupOnNonGroupableUpdate(NotificationRecord record) {
+ maybeUngroupWithSections(record, getPreviousValidSectionKey(record));
+ }
+
+ /**
+ * A notification was added that is app-grouped.
+ */
+ private void maybeUngroupOnAppGrouped(NotificationRecord record) {
+ maybeUngroupWithSections(record, getSectionGroupKeyWithFallback(record));
+ }
+
+ /**
+ * Called when a notification is posted and is either app-grouped or was previously part of
+ * a valid section and needs to trigger GH state cleanup.
+ *
* Evaluate whether:
* (a) an existing autogroup summary needs updated attributes
* (b) if we need to remove our autogroup overlay for this notification
@@ -652,13 +671,20 @@
*
* And updates the internal state of un-app-grouped notifications and their flags.
*/
- private void maybeUngroupWithSections(NotificationRecord record) {
+ private void maybeUngroupWithSections(NotificationRecord record,
+ @Nullable FullyQualifiedGroupKey fullAggregateGroupKey) {
+ if (fullAggregateGroupKey == null) {
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Skipping maybeUngroupWithSections for " + record
+ + " no valid section found.");
+ }
+ return;
+ }
+
final StatusBarNotification sbn = record.getSbn();
final String pkgName = sbn.getPackageName();
final int userId = record.getUserId();
- final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(userId,
- pkgName, getSection(record));
-
synchronized (mAggregatedNotifications) {
// if this notification still exists and has an autogroup overlay, but is now
// grouped by the app, clear the overlay
@@ -675,21 +701,22 @@
mAggregatedNotifications.put(fullAggregateGroupKey, aggregatedNotificationsAttrs);
if (DEBUG) {
- Log.i(TAG, "maybeUngroup removeAutoGroup: " + record);
+ Slog.i(TAG, "maybeUngroup removeAutoGroup: " + record);
}
mCallback.removeAutoGroup(sbn.getKey());
if (aggregatedNotificationsAttrs.isEmpty()) {
if (DEBUG) {
- Log.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
+ Slog.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
}
mCallback.removeAutoGroupSummary(userId, pkgName,
fullAggregateGroupKey.toString());
mAggregatedNotifications.remove(fullAggregateGroupKey);
} else {
if (DEBUG) {
- Log.i(TAG, "Aggregate group not empty, updating: " + fullAggregateGroupKey);
+ Slog.i(TAG,
+ "Aggregate group not empty, updating: " + fullAggregateGroupKey);
}
updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0);
}
@@ -860,8 +887,15 @@
final StatusBarNotification sbn = record.getSbn();
final String pkgName = sbn.getPackageName();
final int userId = record.getUserId();
- final FullyQualifiedGroupKey fullAggregateGroupKey = new FullyQualifiedGroupKey(userId,
- pkgName, getSection(record));
+
+ final FullyQualifiedGroupKey fullAggregateGroupKey = getSectionGroupKeyWithFallback(record);
+ if (fullAggregateGroupKey == null) {
+ if (DEBUG) {
+ Slog.i(TAG,
+ "Skipping autogroup cleanup for " + record + " no valid section found.");
+ }
+ return;
+ }
synchronized (mAggregatedNotifications) {
ArrayMap<String, NotificationAttributes> ungrouped =
@@ -879,14 +913,15 @@
if (aggregatedNotificationsAttrs.isEmpty()) {
if (DEBUG) {
- Log.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
+ Slog.i(TAG, "Aggregate group is empty: " + fullAggregateGroupKey);
}
mCallback.removeAutoGroupSummary(userId, pkgName,
fullAggregateGroupKey.toString());
mAggregatedNotifications.remove(fullAggregateGroupKey);
} else {
if (DEBUG) {
- Log.i(TAG, "Aggregate group not empty, updating: " + fullAggregateGroupKey);
+ Slog.i(TAG,
+ "Aggregate group not empty, updating: " + fullAggregateGroupKey);
}
updateAggregateAppGroup(fullAggregateGroupKey, sbn.getKey(), true, 0);
}
@@ -901,6 +936,52 @@
}
/**
+ * Get the section key for a notification. If the section is invalid, ie. notification is not
+ * auto-groupable, then return the previous valid section, if any.
+ * @param record the notification
+ * @return a section group key, null if not found
+ */
+ @Nullable
+ private FullyQualifiedGroupKey getSectionGroupKeyWithFallback(final NotificationRecord record) {
+ final NotificationSectioner sectioner = getSection(record);
+ if (sectioner != null) {
+ return new FullyQualifiedGroupKey(record.getUserId(), record.getSbn().getPackageName(),
+ sectioner);
+ } else {
+ return getPreviousValidSectionKey(record);
+ }
+ }
+
+ /**
+ * Get the previous valid section key of a notification that may have been updated to an invalid
+ * section. This is needed in case a notification is updated as an ungroupable (invalid section)
+ * => auto-groups need to be updated/GH state cleanup.
+ * @param record the notification
+ * @return a section group key or null if not found
+ */
+ @Nullable
+ private FullyQualifiedGroupKey getPreviousValidSectionKey(final NotificationRecord record) {
+ synchronized (mAggregatedNotifications) {
+ final String recordKey = record.getKey();
+ // Search in ungrouped
+ for (Entry<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>>
+ ungroupedSection : mUngroupedAbuseNotifications.entrySet()) {
+ if (ungroupedSection.getValue().containsKey(recordKey)) {
+ return ungroupedSection.getKey();
+ }
+ }
+ // Search in aggregated
+ for (Entry<FullyQualifiedGroupKey, ArrayMap<String, NotificationAttributes>>
+ aggregatedSection : mAggregatedNotifications.entrySet()) {
+ if (aggregatedSection.getValue().containsKey(recordKey)) {
+ return aggregatedSection.getKey();
+ }
+ }
+ }
+ return null;
+ }
+
+ /**
* Called when a child notification is removed, after some delay, so that this helper can
* trigger a forced grouping if the group has become sparse/singleton
* or only the summary is left.
diff --git a/services/core/java/com/android/server/wm/DisplayContent.java b/services/core/java/com/android/server/wm/DisplayContent.java
index bccf6b20..e190963 100644
--- a/services/core/java/com/android/server/wm/DisplayContent.java
+++ b/services/core/java/com/android/server/wm/DisplayContent.java
@@ -791,6 +791,12 @@
private WindowState mLastWakeLockHoldingWindow;
/**
+ * Whether display is allowed to ignore all activity size restrictions.
+ * @see #isDisplayIgnoreActivitySizeRestrictions
+ */
+ private final boolean mIgnoreActivitySizeRestrictions;
+
+ /**
* The helper of policy controller.
*
* @see DisplayWindowPolicyControllerHelper
@@ -1220,6 +1226,8 @@
setWindowingMode(WINDOWING_MODE_FULLSCREEN);
mWmService.mDisplayWindowSettings.applySettingsToDisplayLocked(this);
+ mIgnoreActivitySizeRestrictions =
+ mWmService.mDisplayWindowSettings.isIgnoreActivitySizeRestrictionsLocked(this);
// Sets the initial touch mode state.
mInTouchMode = mWmService.mContext.getResources().getBoolean(
@@ -5810,7 +5818,7 @@
* {@link VirtualDisplayConfig.Builder#setIgnoreActivitySizeRestrictions}.</p>
*/
boolean isDisplayIgnoreActivitySizeRestrictions() {
- return mWmService.mDisplayWindowSettings.isIgnoreActivitySizeRestrictionsLocked(this);
+ return mIgnoreActivitySizeRestrictions;
}
/**
diff --git a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
index 59a9e85..4230cd8 100644
--- a/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
+++ b/services/core/java/com/android/server/wm/ImeInsetsSourceProvider.java
@@ -286,7 +286,12 @@
if (isImeInputTarget(caller)) {
reportImeInputTargetStateToControlTarget(caller, controlTarget, statsToken);
} else {
- // TODO(b/353463205) add ImeTracker?
+ ProtoLog.w(WM_DEBUG_IME,
+ "Tried to update client visibility for non-IME input target %s "
+ + "(current target: %s)",
+ caller, mDisplayContent.getImeInputTarget());
+ ImeTracker.forLogging().onFailed(statsToken,
+ ImeTracker.PHASE_SERVER_UPDATE_CLIENT_VISIBILITY);
}
}
return false;
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index a48fa5e..ac1219c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -9085,7 +9085,9 @@
}
CallerIdentity caller = getCallerIdentity(who);
- Objects.requireNonNull(who, "ComponentName is null");
+ if (!Flags.setAutoTimeEnabledCoexistence()) {
+ Objects.requireNonNull(who, "ComponentName is null");
+ }
Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
|| isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(caller));
@@ -9165,7 +9167,9 @@
CallerIdentity caller = getCallerIdentity(who);
- Objects.requireNonNull(who, "ComponentName is null");
+ if (!Flags.setAutoTimeZoneEnabledCoexistence()) {
+ Objects.requireNonNull(who, "ComponentName is null");
+ }
Preconditions.checkCallAuthorization(isProfileOwnerOnUser0(caller)
|| isProfileOwnerOfOrganizationOwnedDevice(caller) || isDefaultDeviceOwner(
caller));
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
index e4b461f..5d42713 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyCoordinatorTest.kt
@@ -20,6 +20,7 @@
import android.util.DisplayMetrics
import android.view.Display
import android.view.DisplayInfo
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.mockito.ArgumentMatchers.anyFloat
@@ -87,4 +88,21 @@
verify(mockTopology, never()).addDisplay(anyInt(), anyFloat(), anyFloat())
verify(mockTopologyChangedCallback, never()).invoke(any())
}
+
+ @Test
+ fun getTopology_copy() {
+ assertThat(coordinator.topology).isEqualTo(mockTopologyCopy)
+ }
+
+ @Test
+ fun setTopology_normalize() {
+ val topology = mock<DisplayTopology>()
+ val topologyCopy = mock<DisplayTopology>()
+ whenever(topology.copy()).thenReturn(topologyCopy)
+
+ coordinator.topology = topology
+
+ verify(topology).normalize()
+ verify(mockTopologyChangedCallback).invoke(topologyCopy)
+ }
}
\ No newline at end of file
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
index dd278fc..6cb2429 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java
@@ -2338,6 +2338,177 @@
}
@Test
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
+ public void testUpdateToUngroupableSection_cleanupUngrouped() {
+ final String pkg = "package";
+ // Post notification w/o group in a valid section
+ NotificationRecord notification = spy(getNotificationRecord(pkg, 0, "", mUser,
+ "", false, IMPORTANCE_LOW));
+ Notification n = mock(Notification.class);
+ StatusBarNotification sbn = spy(getSbn(pkg, 0, "0", UserHandle.SYSTEM));
+ when(notification.getNotification()).thenReturn(n);
+ when(notification.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(n);
+ when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
+ assertThat(GroupHelper.getSection(notification)).isNotNull();
+ mGroupHelper.onNotificationPosted(notification, false);
+
+ // Update notification to invalid section
+ when(n.isStyle(Notification.CallStyle.class)).thenReturn(true);
+ assertThat(GroupHelper.getSection(notification)).isNull();
+ boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notification, false);
+ assertThat(needsAutogrouping).isFalse();
+
+ // Check that GH internal state (ungrouped list) was cleaned-up
+ // Post AUTOGROUP_AT_COUNT-1 notifications => should not autogroup
+ for (int i = 0; i < AUTOGROUP_AT_COUNT - 1; i++) {
+ int id = 42 + i;
+ notification = getNotificationRecord(pkg, id, "" + id, mUser,
+ null, false, IMPORTANCE_LOW);
+ mGroupHelper.onNotificationPosted(notification, false);
+ }
+
+ verify(mCallback, never()).addAutoGroupSummary(anyInt(), anyString(), anyString(),
+ anyString(), anyInt(), any());
+ verify(mCallback, never()).addAutoGroup(anyString(), anyString(), anyBoolean());
+ }
+
+ @Test
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
+ android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+ public void testUpdateToUngroupableSection_afterAutogroup_isUngrouped() {
+ final String pkg = "package";
+ final List<NotificationRecord> notificationList = new ArrayList<>();
+ // Post notification w/o group in a valid section
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ NotificationRecord notification = spy(getNotificationRecord(pkg, i, "" + i, mUser,
+ "", false, IMPORTANCE_LOW));
+ Notification n = mock(Notification.class);
+ StatusBarNotification sbn = spy(getSbn(pkg, i, "" + i, UserHandle.SYSTEM));
+ when(notification.getNotification()).thenReturn(n);
+ when(notification.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(n);
+ when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
+ assertThat(GroupHelper.getSection(notification)).isNotNull();
+ mGroupHelper.onNotificationPosted(notification, false);
+ notificationList.add(notification);
+ }
+
+ final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(expectedGroupKey), anyInt(), any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+ eq(expectedGroupKey), eq(true));
+
+ // Update a notification to invalid section
+ Mockito.reset(mCallback);
+ final NotificationRecord notifToInvalidate = notificationList.get(0);
+ when(notifToInvalidate.getNotification().isStyle(Notification.CallStyle.class)).thenReturn(
+ true);
+ assertThat(GroupHelper.getSection(notifToInvalidate)).isNull();
+ boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, true);
+ assertThat(needsAutogrouping).isFalse();
+
+ // Check that the updated notification was removed from the autogroup
+ verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
+ eq(expectedGroupKey), any());
+ }
+
+ @Test
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS,
+ android.app.Flags.FLAG_CHECK_AUTOGROUP_BEFORE_POST})
+ public void testUpdateToUngroupableSection_onRemoved_isUngrouped() {
+ final String pkg = "package";
+ final List<NotificationRecord> notificationList = new ArrayList<>();
+ // Post notification w/o group in a valid section
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ NotificationRecord notification = spy(getNotificationRecord(pkg, i, "" + i, mUser,
+ "", false, IMPORTANCE_LOW));
+ Notification n = mock(Notification.class);
+ StatusBarNotification sbn = spy(getSbn(pkg, i, "" + i, UserHandle.SYSTEM));
+ when(notification.getNotification()).thenReturn(n);
+ when(notification.getSbn()).thenReturn(sbn);
+ when(sbn.getNotification()).thenReturn(n);
+ when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
+ assertThat(GroupHelper.getSection(notification)).isNotNull();
+ mGroupHelper.onNotificationPosted(notification, false);
+ notificationList.add(notification);
+ }
+
+ final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(expectedGroupKey), anyInt(), any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT - 1)).addAutoGroup(anyString(),
+ eq(expectedGroupKey), eq(true));
+
+ // Update a notification to invalid section and removed it
+ Mockito.reset(mCallback);
+ final NotificationRecord notifToInvalidate = notificationList.get(0);
+ when(notifToInvalidate.getNotification().isStyle(Notification.CallStyle.class)).thenReturn(
+ true);
+ assertThat(GroupHelper.getSection(notifToInvalidate)).isNull();
+ notificationList.remove(notifToInvalidate);
+ mGroupHelper.onNotificationRemoved(notifToInvalidate, notificationList);
+
+ // Check that the autogroup was updated
+ verify(mCallback, never()).removeAutoGroup(anyString());
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
+ eq(expectedGroupKey), any());
+ }
+
+ @Test
+ @EnableFlags({FLAG_NOTIFICATION_FORCE_GROUPING, FLAG_NOTIFICATION_FORCE_GROUP_SINGLETONS})
+ public void testUpdateToUngroupableSection_afterForceGrouping_isUngrouped() {
+ final String pkg = "package";
+ final String groupName = "testGroup";
+ final List<NotificationRecord> notificationList = new ArrayList<>();
+ final ArrayMap<String, NotificationRecord> summaryByGroup = new ArrayMap<>();
+ // Post valid section summary notifications without children => force group
+ for (int i = 0; i < AUTOGROUP_AT_COUNT; i++) {
+ NotificationRecord notification = spy(getNotificationRecord(mPkg, i, "" + i, mUser,
+ groupName, true, IMPORTANCE_LOW));
+ Notification n = mock(Notification.class);
+ StatusBarNotification sbn = spy(getSbn(pkg, i, "" + i, UserHandle.SYSTEM, groupName));
+ when(notification.getNotification()).thenReturn(n);
+ when(notification.getSbn()).thenReturn(sbn);
+ when(n.getGroup()).thenReturn(groupName);
+ when(sbn.getNotification()).thenReturn(n);
+ when(n.isStyle(Notification.CallStyle.class)).thenReturn(false);
+ assertThat(GroupHelper.getSection(notification)).isNotNull();
+ notificationList.add(notification);
+ mGroupHelper.onNotificationPostedWithDelay(notification, notificationList,
+ summaryByGroup);
+ }
+
+ final String expectedGroupKey = GroupHelper.getFullAggregateGroupKey(pkg,
+ AGGREGATE_GROUP_KEY + "SilentSection", UserHandle.SYSTEM.getIdentifier());
+ verify(mCallback, times(1)).addAutoGroupSummary(anyInt(), eq(pkg), anyString(),
+ eq(expectedGroupKey), anyInt(), any());
+ verify(mCallback, times(AUTOGROUP_AT_COUNT)).addAutoGroup(anyString(),
+ eq(expectedGroupKey), eq(true));
+
+ // Update a notification to invalid section
+ Mockito.reset(mCallback);
+ final NotificationRecord notifToInvalidate = notificationList.get(0);
+ when(notifToInvalidate.getNotification().isStyle(Notification.CallStyle.class)).thenReturn(
+ true);
+ assertThat(GroupHelper.getSection(notifToInvalidate)).isNull();
+ boolean needsAutogrouping = mGroupHelper.onNotificationPosted(notifToInvalidate, true);
+ assertThat(needsAutogrouping).isFalse();
+
+ // Check that GH internal state (ungrouped list) was cleaned-up
+ verify(mCallback, times(1)).removeAutoGroup(eq(notifToInvalidate.getKey()));
+ verify(mCallback, never()).removeAutoGroupSummary(anyInt(), anyString(), anyString());
+ verify(mCallback, times(1)).updateAutogroupSummary(anyInt(), anyString(),
+ eq(expectedGroupKey), any());
+ }
+
+ @Test
@EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
public void testMoveAggregateGroups_updateChannel() {
final String pkg = "package";