summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/hardware/display/DisplayTopology.java24
-rw-r--r--services/core/Android.bp1
-rw-r--r--services/core/java/com/android/server/display/DisplayTopologyStore.java33
-rw-r--r--services/core/java/com/android/server/display/DisplayTopologyXmlStore.java582
-rw-r--r--services/core/xsd/Android.bp8
-rw-r--r--services/core/xsd/display-topology/OWNERS1
-rw-r--r--services/core/xsd/display-topology/display-topology.xsd65
-rw-r--r--services/core/xsd/display-topology/schema/current.txt64
-rw-r--r--services/core/xsd/display-topology/schema/last_current.txt0
-rw-r--r--services/core/xsd/display-topology/schema/last_removed.txt0
-rw-r--r--services/core/xsd/display-topology/schema/removed.txt1
-rw-r--r--services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyXmlStoreTest.java388
12 files changed, 1164 insertions, 3 deletions
diff --git a/core/java/android/hardware/display/DisplayTopology.java b/core/java/android/hardware/display/DisplayTopology.java
index 1d2f133ee759..11eed72480dc 100644
--- a/core/java/android/hardware/display/DisplayTopology.java
+++ b/core/java/android/hardware/display/DisplayTopology.java
@@ -108,7 +108,6 @@ public final class DisplayTopology implements Parcelable {
public DisplayTopology() {}
- @VisibleForTesting
public DisplayTopology(TreeNode root, int primaryDisplayId) {
mRoot = root;
if (mRoot != null) {
@@ -541,6 +540,19 @@ public final class DisplayTopology implements Parcelable {
}
@Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof DisplayTopology)) {
+ return false;
+ }
+ return obj.toString().equals(toString());
+ }
+
+ @Override
+ public int hashCode() {
+ return toString().hashCode();
+ }
+
+ @Override
public String toString() {
StringWriter out = new StringWriter();
PrintWriter writer = new PrintWriter(out);
@@ -610,7 +622,7 @@ public final class DisplayTopology implements Parcelable {
}
@Nullable
- private static TreeNode findDisplay(int displayId, @Nullable TreeNode startingNode) {
+ public static TreeNode findDisplay(int displayId, @Nullable TreeNode startingNode) {
if (startingNode == null) {
return null;
}
@@ -775,16 +787,22 @@ public final class DisplayTopology implements Parcelable {
*/
private float mOffset;
- private final List<TreeNode> mChildren = new ArrayList<>();
+ private final List<TreeNode> mChildren;
@VisibleForTesting
public TreeNode(int displayId, float width, float height, @Position int position,
float offset) {
+ this(displayId, width, height, position, offset, List.of());
+ }
+
+ public TreeNode(int displayId, float width, float height, int position,
+ float offset, List<TreeNode> children) {
mDisplayId = displayId;
mWidth = width;
mHeight = height;
mPosition = position;
mOffset = offset;
+ mChildren = new ArrayList<>(children);
}
public TreeNode(Parcel source) {
diff --git a/services/core/Android.bp b/services/core/Android.bp
index 42385fc5bdb0..fed070a1b8a8 100644
--- a/services/core/Android.bp
+++ b/services/core/Android.bp
@@ -143,6 +143,7 @@ java_library_static {
":platform-compat-overrides",
":display-device-config",
":display-layout-config",
+ ":display-topology",
":device-state-config",
"java/com/android/server/EventLogTags.logtags",
"java/com/android/server/am/EventLogTags.logtags",
diff --git a/services/core/java/com/android/server/display/DisplayTopologyStore.java b/services/core/java/com/android/server/display/DisplayTopologyStore.java
new file mode 100644
index 000000000000..2256c11feee8
--- /dev/null
+++ b/services/core/java/com/android/server/display/DisplayTopologyStore.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import android.annotation.Nullable;
+import android.hardware.display.DisplayTopology;
+
+/**
+ * Allows to save and restore {@link DisplayTopology}.
+ * See implementation: {@link DisplayTopologyXmlStore}
+ */
+interface DisplayTopologyStore {
+ boolean saveTopology(DisplayTopology topology);
+
+ @Nullable
+ DisplayTopology restoreTopology(DisplayTopology topology);
+
+ void reloadTopologies(int userId);
+}
diff --git a/services/core/java/com/android/server/display/DisplayTopologyXmlStore.java b/services/core/java/com/android/server/display/DisplayTopologyXmlStore.java
new file mode 100644
index 000000000000..b7f31b75f6dc
--- /dev/null
+++ b/services/core/java/com/android/server/display/DisplayTopologyXmlStore.java
@@ -0,0 +1,582 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+import static android.view.Display.DEFAULT_DISPLAY;
+import static android.view.Display.INVALID_DISPLAY;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static java.util.Comparator.comparingInt;
+
+import android.annotation.Nullable;
+import android.hardware.display.DisplayTopology;
+import android.os.Environment;
+import android.util.AtomicFile;
+import android.util.AtomicFilePrintWriter;
+import android.util.Slog;
+import android.util.SparseArray;
+
+// automatically generated classes from display-topology.xsd
+import com.android.server.display.topology.Children;
+import com.android.server.display.topology.Display;
+import com.android.server.display.topology.DisplayTopologyState;
+import com.android.server.display.topology.Position;
+import com.android.server.display.topology.Topology;
+import com.android.server.display.topology.XmlParser;
+import com.android.server.display.topology.XmlWriter;
+import com.android.server.display.utils.DebugUtils;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.xml.datatype.DatatypeConfigurationException;
+
+/**
+ * Saves and restores {@link DisplayTopology} to/from xml files with topologies for each
+ * {@link DisplayTopologyXmlStore#mUserId} user.
+ */
+class DisplayTopologyXmlStore implements DisplayTopologyStore {
+ private static final String TAG = "DisplayManager.DisplayTopologyXmlStore";
+ private static final String ETC_DIR = "etc";
+ private static final String DISPLAY_CONFIG_DIR = "displayconfig";
+
+ // To enable these logs, run:
+ // adb shell setprop persist.log.tag.DisplayManager.DisplayTopologyXmlStore DEBUG
+ // adb reboot
+ private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+
+ private static final int PERSISTENT_TOPOLOGY_VERSION = 1;
+ /**
+ * {@link #restoreTopology} needs to reorder topologies to keep the most recently used
+ * topologies order close to 0. In case current topology displays change often, the persistence
+ * of the reordered topologies can become a performance issue. To avoid persistence for small
+ * changes in the order values lets use this constant, serving as the threshold when
+ * to trigger persistence during {@link #restoreTopology}.
+ */
+ private static final int MIN_REORDER_WHICH_TRIGGERS_PERSISTENCE = 10;
+ private static final int MAX_NUMBER_OF_TOPOLOGIES = 100;
+
+ static File getUserTopologyFile(int userId) {
+ return new File(Environment.getDataSystemCeDirectory(userId), "display_topology.xml");
+ }
+
+ private static File getVendorTopologyFile() {
+ return Environment.buildPath(Environment.getVendorDirectory(),
+ ETC_DIR, DISPLAY_CONFIG_DIR, "display_topology.xml");
+ }
+
+ private static File getProductTopologyFile() {
+ return Environment.buildPath(Environment.getProductDirectory(),
+ ETC_DIR, DISPLAY_CONFIG_DIR, "display_topology.xml");
+ }
+
+ private static List<Topology> readTopologiesFromInputStream(
+ @Nullable InputStream iStream)
+ throws DatatypeConfigurationException, XmlPullParserException, IOException {
+ if (null == iStream) {
+ if (DEBUG) {
+ Slog.d(TAG, "iStream is null");
+ }
+ return List.of();
+ }
+ // use parser automatically generated from display-topology.xsd
+ var topologyState = XmlParser.read(iStream);
+ if (topologyState.getVersion() > PERSISTENT_TOPOLOGY_VERSION) {
+ Slog.e(TAG, "Topology version=" + topologyState.getVersion()
+ + " is not supported by DisplayTopologyXmlStore version="
+ + PERSISTENT_TOPOLOGY_VERSION);
+ return List.of();
+ }
+ if (DEBUG) {
+ Slog.d(TAG, "readTopologiesFromInputStream: done");
+ }
+
+ var topologyList = topologyState.getTopology();
+ topologyList.sort(comparingInt(Topology::getOrder));
+ return topologyList;
+ }
+
+ private static int getOrderOrDefault(@Nullable Topology topology, int defaultOrder) {
+ return null != topology ? topology.getOrder() : defaultOrder;
+ }
+
+ private final Injector mInjector;
+ private int mUserId = -1;
+ private final List<Topology> mImmutableTopologies = new ArrayList<>();
+ private final Map<String, Topology> mTopologies = new HashMap<>();
+
+ DisplayTopologyXmlStore(Injector injector) {
+ mInjector = injector;
+ reloadImmutableTopologies();
+ }
+
+ /**
+ * Persists the topology into XML
+ * @param topology the topology to persist
+ * @return true if persisted successfully, false otherwise
+ */
+ @Override
+ public boolean saveTopology(DisplayTopology topology) {
+ String topologyId = getTopologyId(topology);
+ if (DEBUG) {
+ Slog.d(TAG, "saveTopology userId=" + mUserId + ", topologyId=" + topologyId);
+ }
+ if (null == topologyId) {
+ Slog.w(TAG, "saveTopology cancelled: topology id is null for " + topology);
+ return false;
+ }
+
+ Topology topologyToPersist = convertTopologyForPersistence(topology, topologyId);
+ if (null == topologyToPersist) {
+ Slog.w(TAG, "saveTopology cancelled: can't convert topology " + topology);
+ return false;
+ }
+
+ if (!prependTopology(topologyToPersist)) {
+ Slog.w(TAG, "saveTopology cancelled: can't prependTopology");
+ return false;
+ }
+ saveTopologiesToFile();
+ return true;
+ }
+
+ /**
+ * Searches for the topology's id in the store. If topology is found in the store,
+ * then uses the passed topology display width and height, and the persisted topology
+ * structure, position and offset.
+ * @param topology original topology which we would like to restore to a state which was
+ * previously persisted, keeping the current width and height.
+ * @return null if topology is not found, or the new restored topology otherwise.
+ */
+ @Nullable
+ @Override
+ public DisplayTopology restoreTopology(DisplayTopology topology) {
+ String topologyId = getTopologyId(topology);
+ if (DEBUG) {
+ Slog.d(TAG, "restoreTopology userId=" + mUserId + ", topologyId=" + topologyId);
+ }
+ if (null == topologyId) {
+ Slog.w(TAG, "restoreTopology cancelled: topology id is null for " + topology);
+ return null;
+ }
+
+ Topology restoredTopology = mTopologies.get(topologyId);
+ if (null == restoredTopology) {
+ // Topology is not found in persistent storage.
+ if (DEBUG) {
+ Slog.d(TAG, "restoreTopology userId=" + mUserId + ", topologyId=" + topologyId
+ + " is not found");
+ }
+ return null;
+ }
+
+ // Reorder and save to file for significant changes in topologies order.
+ if (restoredTopology.getOrder() >= MIN_REORDER_WHICH_TRIGGERS_PERSISTENCE) {
+ moveTopologyToHead(restoredTopology);
+ saveTopologiesToFile();
+ }
+ return convertPersistentTopologyToDisplayTopology(topology, restoredTopology.getDisplay(),
+ mInjector.getUniqueIdToDisplayIdMapping());
+ }
+
+ @Override
+ public void reloadTopologies(int userId) {
+ if (DEBUG) {
+ Slog.d(TAG, "reloadTopologies mUserId=" + mUserId + "->userId=" + userId);
+ }
+ if (mUserId != userId) {
+ mUserId = userId;
+ resetTopologies();
+ }
+ reloadTopologies();
+ }
+
+ private void resetTopologies() {
+ mTopologies.clear();
+ appendTopologies(mImmutableTopologies);
+ }
+
+ /**
+ * Increases all orders by 1 for those topologies currently below the order of the
+ * passed topology. Sets the order of the passed topology to 0.
+ */
+ private void moveTopologyToHead(Topology topology) {
+ if (topology.getOrder() == 0) {
+ return;
+ }
+ for (var t : mTopologies.values()) {
+ if (t.getOrder() < topology.getOrder()) {
+ t.setOrder(t.getOrder() + 1);
+ }
+ }
+ topology.setOrder(0);
+ }
+
+ private void reloadImmutableTopologies() {
+ mImmutableTopologies.clear();
+ try (InputStream iStream = mInjector.readProductTopologies()) {
+ mImmutableTopologies.addAll(readTopologiesFromInputStream(iStream));
+ } catch (IOException | XmlPullParserException | DatatypeConfigurationException e) {
+ Slog.e(TAG, "reloadImmutableTopologies for product topologies failed", e);
+ }
+ try (InputStream iStream = mInjector.readVendorTopologies()) {
+ mImmutableTopologies.addAll(readTopologiesFromInputStream(iStream));
+ } catch (IOException | XmlPullParserException | DatatypeConfigurationException e) {
+ Slog.e(TAG, "reloadImmutableTopologies for vendor topologies failed", e);
+ }
+ for (var topology : mImmutableTopologies) {
+ topology.setImmutable(true);
+ }
+ }
+
+ private void reloadTopologies() {
+ if (mUserId < 0) {
+ Slog.e(TAG, "Can't reload topologies for userId=" + mUserId);
+ return;
+ }
+ try (InputStream iStream = mInjector.readUserTopologies(mUserId)) {
+ appendTopologies(readTopologiesFromInputStream(iStream));
+ } catch (IOException | XmlPullParserException | DatatypeConfigurationException e) {
+ Slog.e(TAG, "reloadTopologies failed", e);
+ }
+ }
+
+ private void appendTopologies(List<Topology> topologyList) {
+ for (var topology : topologyList) {
+ appendTopology(topology);
+ }
+ }
+
+ private void appendTopology(Topology topology) {
+ Topology restoredTopology = mTopologies.get(topology.getId());
+ if (null != restoredTopology && restoredTopology.getImmutable()) {
+ Slog.w(TAG, "addTopology: can't override immutable topology "
+ + topology.getId());
+ return;
+ }
+
+ // If topology is not found, and we exceed the limit of topologies
+ // (so we can't add more topologies), then skip this topology
+ if (null == restoredTopology && mTopologies.size() >= MAX_NUMBER_OF_TOPOLOGIES) {
+ if (DEBUG) {
+ Slog.d(TAG, "appendTopology: MAX_NUMBER_OF_TOPOLOGIES is reached,"
+ + " can't append topology" + topology.getId());
+ }
+ return;
+ }
+ topology.setOrder(getOrderOrDefault(restoredTopology, mTopologies.size()));
+ mTopologies.put(topology.getId(), topology);
+ }
+
+ private boolean prependTopology(Topology topology) {
+ Topology restoredTopology = mTopologies.get(topology.getId());
+ if (null != restoredTopology && restoredTopology.getImmutable()) {
+ Slog.w(TAG, "prependTopology: can't override immutable topology "
+ + topology.getId());
+ return false;
+ }
+
+ // If topology is not found, and we exceed the limit of topologies
+ // remove the max order mutable topology.
+ if (null == restoredTopology && mTopologies.size() >= MAX_NUMBER_OF_TOPOLOGIES) {
+ Topology topologyToRemove = findMaxOrderMutableTopology();
+ if (topologyToRemove == null) {
+ Slog.w(TAG, "prependTopology: can't find a topology to remove to free up space");
+ return false;
+ }
+ mTopologies.remove(topologyToRemove.getId());
+ if (DEBUG) {
+ Slog.d(TAG, "prependTopology: remove topology " + topologyToRemove.getId());
+ }
+ }
+
+ topology.setOrder(Integer.MAX_VALUE);
+ moveTopologyToHead(topology);
+ mTopologies.put(topology.getId(), topology);
+ return true;
+ }
+
+ /**
+ * Higher order of the topology means lower priority.
+ */
+ @Nullable
+ private Topology findMaxOrderMutableTopology() {
+ Topology res = null;
+ for (var topology : mTopologies.values()) {
+ if (topology.getImmutable()) {
+ continue;
+ }
+ if (res == null || res.getOrder() < topology.getOrder()) {
+ res = topology;
+ }
+ }
+ return res;
+ }
+
+ private void saveTopologiesToFile() {
+ if (mUserId < 0) {
+ Slog.e(TAG, "Can't save topologies for userId=" + mUserId);
+ return;
+ }
+ if (mTopologies.isEmpty()) {
+ if (DEBUG) {
+ Slog.d(TAG, "No topologies to save for userId=" + mUserId);
+ }
+ return;
+ }
+ var topologyState = new DisplayTopologyState();
+ topologyState.setVersion(PERSISTENT_TOPOLOGY_VERSION);
+ for (var topology : mTopologies.values()) {
+ if (!topology.getImmutable()) {
+ topologyState.getTopology().add(topology);
+ }
+ }
+
+ try (var pw = mInjector.getTopologyFilePrintWriter(mUserId)) {
+ // use writer automatically generated from display-topology.xsd
+ XmlWriter.write(new XmlWriter(pw), topologyState);
+ pw.markSuccess();
+ if (DEBUG) Slog.d(TAG, "saveTopologiesToFile " + pw);
+ } catch (IOException e) {
+ Slog.e(TAG, "saveTopologiesToFile failed", e);
+ }
+ }
+
+ private DisplayTopology convertPersistentTopologyToDisplayTopology(
+ DisplayTopology currentDisplayTopology,
+ Display persistentDisplayTopology,
+ Map<String, Integer> uniqueIdToDisplayIdMapping) {
+ var rootNode = convertPersistentDisplayToTreeNode(persistentDisplayTopology,
+ currentDisplayTopology, uniqueIdToDisplayIdMapping);
+ int primaryDisplayId = findPrimaryDisplayId(persistentDisplayTopology,
+ uniqueIdToDisplayIdMapping);
+ if (primaryDisplayId == INVALID_DISPLAY) {
+ Slog.e(TAG, "Primary display id is not found in persistent topology");
+ primaryDisplayId = DEFAULT_DISPLAY;
+ }
+ return new DisplayTopology(rootNode, primaryDisplayId);
+ }
+
+ private DisplayTopology.TreeNode convertPersistentDisplayToTreeNode(
+ Display persistentDisplay,
+ DisplayTopology currentDisplayTopology,
+ Map<String, Integer> uniqueIdToDisplayIdMapping
+ ) {
+ Integer displayId = uniqueIdToDisplayIdMapping.get(persistentDisplay.getId());
+ if (null == displayId) {
+ throw new IllegalStateException("Can't map uniqueId="
+ + persistentDisplay.getId() + " to displayId");
+ }
+
+ var displayNode = DisplayTopology.findDisplay(displayId,
+ currentDisplayTopology.getRoot());
+ if (null == displayNode) {
+ throw new IllegalStateException("Can't find displayId="
+ + displayId + " in current topology");
+ }
+
+ List<DisplayTopology.TreeNode> children = new ArrayList<>();
+ for (var child : persistentDisplay.getChildren().getDisplay()) {
+ children.add(convertPersistentDisplayToTreeNode(child, currentDisplayTopology,
+ uniqueIdToDisplayIdMapping));
+ }
+
+ return new DisplayTopology.TreeNode(
+ displayId, displayNode.getWidth(), displayNode.getHeight(),
+ toDisplayTopologyPosition(persistentDisplay.getPosition()),
+ persistentDisplay.getOffset(), children);
+ }
+
+ private int findPrimaryDisplayId(Display persistentDisplay,
+ Map<String, Integer> uniqueIdToDisplayIdMapping) {
+ if (persistentDisplay.getPrimary()) {
+ var displayId = uniqueIdToDisplayIdMapping.get(persistentDisplay.getId());
+ if (null == displayId) {
+ throw new IllegalStateException("Can't map uniqueId="
+ + persistentDisplay.getId() + " to displayId");
+ }
+ return displayId;
+ }
+ for (var child : persistentDisplay.getChildren().getDisplay()) {
+ var displayId = findPrimaryDisplayId(child, uniqueIdToDisplayIdMapping);
+ if (displayId != INVALID_DISPLAY) {
+ return displayId;
+ }
+ }
+ return INVALID_DISPLAY;
+ }
+
+ @Nullable
+ private Topology convertTopologyForPersistence(DisplayTopology topology, String topologyId) {
+ var rootNode = convertTreeNodeForPersistence(topology.getRoot(),
+ topology.getPrimaryDisplayId(), mInjector.getDisplayIdToUniqueIdMapping());
+ if (null == rootNode) {
+ return null;
+ }
+
+ Topology persistentTopology = new Topology();
+ persistentTopology.setDisplay(rootNode);
+ persistentTopology.setId(topologyId);
+ return persistentTopology;
+ }
+
+ @Nullable
+ private Display convertTreeNodeForPersistence(
+ @Nullable DisplayTopology.TreeNode node,
+ int primaryDisplayId,
+ SparseArray<String> idsToUniqueIds) {
+ if (null == node) {
+ Slog.e(TAG, "Can't convertTreeNodeForPersistence, node == null");
+ return null;
+ }
+ var uniqueId = idsToUniqueIds.get(node.getDisplayId());
+ if (null == uniqueId) {
+ Slog.e(TAG, "Can't convertTreeNodeForPersistence,"
+ + " uniqueId is not found for " + node.getDisplayId());
+ return null;
+ }
+ Children children = new Children();
+ for (var child : node.getChildren()) {
+ var display = convertTreeNodeForPersistence(child, primaryDisplayId, idsToUniqueIds);
+ if (null == display) {
+ return null;
+ }
+ children.getDisplay().add(display);
+ }
+ var root = new Display();
+ root.setPosition(toPersistentPosition(node.getPosition()));
+ root.setId(uniqueId);
+ root.setOffset(node.getOffset());
+ root.setPrimary(node.getDisplayId() == primaryDisplayId);
+ root.setChildren(children);
+ return root;
+ }
+
+ private Position toPersistentPosition(@DisplayTopology.TreeNode.Position int pos) {
+ return switch (pos) {
+ case DisplayTopology.TreeNode.POSITION_LEFT -> Position.left;
+ case DisplayTopology.TreeNode.POSITION_TOP -> Position.top;
+ case DisplayTopology.TreeNode.POSITION_RIGHT -> Position.right;
+ case DisplayTopology.TreeNode.POSITION_BOTTOM -> Position.bottom;
+ default -> throw new IllegalArgumentException("Unknown position=" + pos);
+ };
+ }
+
+ @DisplayTopology.TreeNode.Position
+ private int toDisplayTopologyPosition(Position pos) {
+ return switch (pos) {
+ case left -> DisplayTopology.TreeNode.POSITION_LEFT;
+ case top -> DisplayTopology.TreeNode.POSITION_TOP;
+ case right -> DisplayTopology.TreeNode.POSITION_RIGHT;
+ case bottom -> DisplayTopology.TreeNode.POSITION_BOTTOM;
+ };
+ }
+
+ private List<String> getUniqueIds(@Nullable DisplayTopology.TreeNode node,
+ SparseArray<String> mapping, List<String> uniqueIds) {
+ if (null == node) {
+ return uniqueIds;
+ }
+ uniqueIds.add(mapping.get(node.getDisplayId()));
+ for (var child : node.getChildren()) {
+ getUniqueIds(child, mapping, uniqueIds);
+ }
+ return uniqueIds;
+ }
+
+ @Nullable
+ private String getTopologyId(DisplayTopology topology) {
+ SparseArray<String> mapping = mInjector.getDisplayIdToUniqueIdMapping();
+ return getTopologyId(getUniqueIds(topology.getRoot(), mapping, new ArrayList<>()));
+ }
+
+ @Nullable
+ private String getTopologyId(List<String> uniqueIds) {
+ if (uniqueIds.isEmpty() || uniqueIds.contains(null)) {
+ return null;
+ }
+ Collections.sort(uniqueIds);
+ return String.join("|", uniqueIds);
+ }
+
+ abstract static class Injector {
+ /**
+ * Necessary mapping for conversion of {@link DisplayTopology} which uses
+ * {@link android.view.DisplayInfo#displayId} to {@link DisplayTopologyState}
+ * which uses {@link android.view.DisplayInfo#uniqueId}
+ *
+ * @return mapping from {@link android.view.DisplayInfo#displayId}
+ * to {@link android.view.DisplayInfo#uniqueId}
+ */
+ public abstract SparseArray<String> getDisplayIdToUniqueIdMapping();
+
+ /**
+ * Necessary mapping for conversion opposite to {@link #getDisplayIdToUniqueIdMapping()}
+ *
+ * @return mapping from {@link android.view.DisplayInfo#uniqueId}
+ * to {@link android.view.DisplayInfo#displayId}
+ */
+ public abstract Map<String, Integer> getUniqueIdToDisplayIdMapping();
+
+ /**
+ * Reads vendor topologies, if configured.
+ * @return input stream with vendor-defined topologies, or null if not configured.
+ */
+ @Nullable
+ public InputStream readVendorTopologies() throws FileNotFoundException {
+ return getFileInputStream(getVendorTopologyFile());
+ }
+
+ /**
+ * Reads product topologies, if configured.
+ * @return input stream with product-defined topologies, or null if not configured.
+ */
+ @Nullable
+ public InputStream readProductTopologies() throws FileNotFoundException {
+ return getFileInputStream(getProductTopologyFile());
+ }
+
+ @Nullable
+ InputStream readUserTopologies(int userId) throws FileNotFoundException {
+ return getFileInputStream(getUserTopologyFile(userId));
+ }
+
+ AtomicFilePrintWriter getTopologyFilePrintWriter(int userId) throws IOException {
+ var atomicFile = new AtomicFile(getUserTopologyFile(userId),
+ /*commitTag=*/ "topology-state");
+ return new AtomicFilePrintWriter(atomicFile, UTF_8);
+ }
+
+ @Nullable
+ private FileInputStream getFileInputStream(File file) throws FileNotFoundException {
+ if (DEBUG) {
+ Slog.d(TAG, "File: " + file + " exists=" + file.exists());
+ }
+ return !file.exists() ? null : new FileInputStream(file);
+ }
+ }
+}
diff --git a/services/core/xsd/Android.bp b/services/core/xsd/Android.bp
index 6a50d3834355..8b7bdf5cece2 100644
--- a/services/core/xsd/Android.bp
+++ b/services/core/xsd/Android.bp
@@ -30,6 +30,14 @@ xsd_config {
}
xsd_config {
+ name: "display-topology",
+ srcs: ["display-topology/display-topology.xsd"],
+ api_dir: "display-topology/schema",
+ package_name: "com.android.server.display.topology",
+ gen_writer: true,
+}
+
+xsd_config {
name: "display-device-config",
srcs: ["display-device-config/display-device-config.xsd"],
api_dir: "display-device-config/schema",
diff --git a/services/core/xsd/display-topology/OWNERS b/services/core/xsd/display-topology/OWNERS
new file mode 100644
index 000000000000..6ce1ee4d3de2
--- /dev/null
+++ b/services/core/xsd/display-topology/OWNERS
@@ -0,0 +1 @@
+include /services/core/java/com/android/server/display/OWNERS
diff --git a/services/core/xsd/display-topology/display-topology.xsd b/services/core/xsd/display-topology/display-topology.xsd
new file mode 100644
index 000000000000..00f766fe018c
--- /dev/null
+++ b/services/core/xsd/display-topology/display-topology.xsd
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ 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.
+-->
+
+<!--
+ This defines the format of the XML file used to define how displays are arranged
+ in topologies.
+ It is parsed in com/android/server/display/PersistentTopologyStore.java
+ More information on display topology can be found in DisplayTopology.java
+-->
+<xs:schema version="2.0"
+ elementFormDefault="qualified"
+ xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:simpleType name="position">
+ <xs:restriction base="xs:string">
+ <xs:enumeration value="left"/>
+ <xs:enumeration value="top"/>
+ <xs:enumeration value="right"/>
+ <xs:enumeration value="bottom"/>
+ </xs:restriction>
+ </xs:simpleType>
+ <xs:complexType name="children">
+ <xs:sequence>
+ <xs:element type="display" name="display" maxOccurs="unbounded" minOccurs="0"/>
+ </xs:sequence>
+ </xs:complexType>
+ <xs:complexType name="display">
+ <xs:sequence>
+ <xs:element type="position" name="position" />
+ <xs:element type="xs:float" name="offset"/>
+ <xs:element type="children" name="children" />
+ </xs:sequence>
+ <xs:attribute type="xs:string" name="id" use="required"/>
+ <xs:attribute type="xs:boolean" name="primary"/>
+ </xs:complexType>
+ <xs:complexType name="topology">
+ <xs:sequence>
+ <xs:element type="display" name="display"/>
+ </xs:sequence>
+ <xs:attribute type="xs:string" name="id" use="required"/>
+ <xs:attribute type="xs:int" name="order" use="required"/>
+ <xs:attribute type="xs:boolean" name="immutable"/>
+ </xs:complexType>
+ <xs:element name="displayTopologyState">
+ <xs:complexType>
+ <xs:sequence>
+ <xs:element type="topology" name="topology" maxOccurs="1000" minOccurs="0"/>
+ </xs:sequence>
+ <xs:attribute type="xs:int" name="version" use="required"/>
+ </xs:complexType>
+ </xs:element>
+</xs:schema>
diff --git a/services/core/xsd/display-topology/schema/current.txt b/services/core/xsd/display-topology/schema/current.txt
new file mode 100644
index 000000000000..eb59e9ec5f7b
--- /dev/null
+++ b/services/core/xsd/display-topology/schema/current.txt
@@ -0,0 +1,64 @@
+// Signature format: 2.0
+package com.android.server.display.topology {
+
+ public class Children {
+ ctor public Children();
+ method public java.util.List<com.android.server.display.topology.Display> getDisplay();
+ }
+
+ public class Display {
+ ctor public Display();
+ method public com.android.server.display.topology.Children getChildren();
+ method public String getId();
+ method public float getOffset();
+ method public com.android.server.display.topology.Position getPosition();
+ method public boolean getPrimary();
+ method public void setChildren(com.android.server.display.topology.Children);
+ method public void setId(String);
+ method public void setOffset(float);
+ method public void setPosition(com.android.server.display.topology.Position);
+ method public void setPrimary(boolean);
+ }
+
+ public class DisplayTopologyState {
+ ctor public DisplayTopologyState();
+ method public java.util.List<com.android.server.display.topology.Topology> getTopology();
+ method public int getVersion();
+ method public void setVersion(int);
+ }
+
+ public enum Position {
+ method public String getRawName();
+ enum_constant public static final com.android.server.display.topology.Position bottom;
+ enum_constant public static final com.android.server.display.topology.Position left;
+ enum_constant public static final com.android.server.display.topology.Position right;
+ enum_constant public static final com.android.server.display.topology.Position top;
+ }
+
+ public class Topology {
+ ctor public Topology();
+ method public com.android.server.display.topology.Display getDisplay();
+ method public String getId();
+ method public boolean getImmutable();
+ method public int getOrder();
+ method public void setDisplay(com.android.server.display.topology.Display);
+ method public void setId(String);
+ method public void setImmutable(boolean);
+ method public void setOrder(int);
+ }
+
+ public class XmlParser {
+ ctor public XmlParser();
+ method public static com.android.server.display.topology.DisplayTopologyState read(java.io.InputStream) throws javax.xml.datatype.DatatypeConfigurationException, java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+ method public static String readText(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+ method public static void skip(org.xmlpull.v1.XmlPullParser) throws java.io.IOException, org.xmlpull.v1.XmlPullParserException;
+ }
+
+ public class XmlWriter implements java.io.Closeable {
+ ctor public XmlWriter(java.io.PrintWriter);
+ method public void close();
+ method public static void write(com.android.server.display.topology.XmlWriter, com.android.server.display.topology.DisplayTopologyState) throws java.io.IOException;
+ }
+
+}
+
diff --git a/services/core/xsd/display-topology/schema/last_current.txt b/services/core/xsd/display-topology/schema/last_current.txt
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/services/core/xsd/display-topology/schema/last_current.txt
diff --git a/services/core/xsd/display-topology/schema/last_removed.txt b/services/core/xsd/display-topology/schema/last_removed.txt
new file mode 100644
index 000000000000..e69de29bb2d1
--- /dev/null
+++ b/services/core/xsd/display-topology/schema/last_removed.txt
diff --git a/services/core/xsd/display-topology/schema/removed.txt b/services/core/xsd/display-topology/schema/removed.txt
new file mode 100644
index 000000000000..d802177e249b
--- /dev/null
+++ b/services/core/xsd/display-topology/schema/removed.txt
@@ -0,0 +1 @@
+// Signature format: 2.0
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyXmlStoreTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyXmlStoreTest.java
new file mode 100644
index 000000000000..48822821ba01
--- /dev/null
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayTopologyXmlStoreTest.java
@@ -0,0 +1,388 @@
+/*
+ * Copyright 2025 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display;
+
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import android.graphics.PointF;
+import android.hardware.display.DisplayTopology;
+import android.util.AtomicFilePrintWriter;
+import android.util.SparseArray;
+
+import androidx.test.filters.SmallTest;
+
+import com.google.common.io.CharSource;
+import com.google.testing.junit.testparameterinjector.TestParameterInjector;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Tests for {@link DisplayTopologyXmlStore}
+ * Run: atest PersistentTopologyStoreTest
+ */
+@SmallTest
+@RunWith(TestParameterInjector.class)
+public class DisplayTopologyXmlStoreTest {
+ private static final String NO_TOPOLOGIES = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <displayTopologyState version="1"/>
+ """;
+
+ private static final String SIMPLE_TOPOLOGY = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <displayTopologyState version="1">
+ <topology id="uniqueid0|uniqueid1" order="0">
+ <display id="uniqueid0" primary="true">
+ <position>left</position>
+ <offset>0.0</offset>
+ <children>
+ <display id="uniqueid1" primary="false">
+ <position>top</position>
+ <offset>-560.0</offset>
+ <children>
+ </children>
+ </display>
+ </children>
+ </display>
+ </topology>
+ </displayTopologyState>
+ """;
+
+ private static final String IMMUTABLE_TOPOLOGY = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <displayTopologyState version="1">
+ <topology id="uniqueid10|uniqueid11" order="0" immutable="true">
+ <display id="uniqueid10" primary="true">
+ <position>left</position>
+ <offset>0.0</offset>
+ <children>
+ <display id="uniqueid11" primary="false">
+ <position>top</position>
+ <offset>-560.0</offset>
+ <children>
+ </children>
+ </display>
+ </children>
+ </display>
+ </topology>
+ </displayTopologyState>
+ """;
+
+ private static final String MULTIPLE_TOPOLOGIES = """
+ <?xml version="1.0" encoding="utf-8"?>
+ <displayTopologyState version="1">
+ <topology id="uniqueid0|uniqueid1" order="0">
+ <display id="uniqueid0" primary="true">
+ <position>left</position>
+ <offset>0.0</offset>
+ <children>
+ <display id="uniqueid1" primary="false">
+ <position>top</position>
+ <offset>-560.0</offset>
+ <children>
+ </children>
+ </display>
+ </children>
+ </display>
+ </topology>
+ <topology id="uniqueid0|uniqueid1|uniqueid2|uniqueid3" order="0">
+ <display id="uniqueid1" primary="false">
+ <position>left</position>
+ <offset>0.0</offset>
+ <children>
+ <display id="uniqueid0" primary="false">
+ <position>top</position>
+ <offset>-50</offset>
+ <children>
+ </children>
+ </display>
+ <display id="uniqueid2" primary="true">
+ <position>right</position>
+ <offset>-100</offset>
+ <children>
+ <display id="uniqueid3" primary="false">
+ <position>bottom</position>
+ <offset>-300</offset>
+ <children>
+ </children>
+ </display>
+ </children>
+ </display>
+ </children>
+ </display>
+ </topology>
+ </displayTopologyState>
+ """;
+
+ private static InputStream asInputStream(String value) throws IOException {
+ return CharSource.wrap(value).asByteSource(UTF_8).openStream();
+ }
+
+ private static SparseArray<String> generateIdToUniqueId() {
+ var res = new SparseArray<String>();
+ for (int i = 0; i < 200; i++) {
+ res.put(i, "uniqueid" + i);
+ }
+ return res;
+ }
+
+ private static Map<String, Integer> generateUniqueIdToId() {
+ var res = new HashMap<String, Integer>();
+ for (int i = 0; i < 200; i++) {
+ res.put("uniqueid" + i, i);
+ }
+ return res;
+ }
+
+ private static DisplayTopology generateTopology(int displayId1, int displayId2) {
+ var topology = new DisplayTopology();
+ topology.addDisplay(displayId1, 800f, 600f);
+ topology.addDisplay(displayId2, 1920f, 1080f);
+ return topology;
+ }
+
+ @Mock
+ private DisplayTopologyXmlStore.Injector mInjector;
+ @Mock
+ private AtomicFilePrintWriter mPrintWriter0;
+ @Mock
+ private AtomicFilePrintWriter mPrintWriter1;
+
+ private DisplayTopology mTopology;
+
+ /** Setup tests. */
+ @Before
+ public void setup() throws IOException {
+ MockitoAnnotations.initMocks(this);
+ configureTopologyFile(/*userId=*/ 0, NO_TOPOLOGIES, mPrintWriter0);
+ configureTopologyFile(/*userId=*/ 1, SIMPLE_TOPOLOGY, mPrintWriter1);
+ configureTopologyFile(/*userId=*/ 2, MULTIPLE_TOPOLOGIES, mPrintWriter1);
+
+ when(mInjector.getDisplayIdToUniqueIdMapping()).thenReturn(generateIdToUniqueId());
+ when(mInjector.getUniqueIdToDisplayIdMapping()).thenReturn(generateUniqueIdToId());
+
+ mTopology = generateTopology(0, 1);
+ }
+
+ @Test
+ public void testSaveAndRestoreTopologyWithoutFileStreams() throws IOException {
+ final float initialOffset = -560f;
+ final float newOffset = -300f;
+
+ var store = new DisplayTopologyXmlStore(mInjector);
+ assertThat(store.saveTopology(mTopology)).isTrue();
+ assertThat(mTopology.getRoot().getChildren().getFirst().getOffset())
+ .isEqualTo(initialOffset);
+ assertThat(mTopology.getRoot().getWidth()).isEqualTo(800f);
+ assertThat(mTopology.getRoot().getHeight()).isEqualTo(600f);
+
+ // Change display size
+ assertThat(mTopology.updateDisplay(0, 640f, 480f)).isTrue();
+ assertThat(mTopology.getRoot().getWidth()).isEqualTo(640f);
+ assertThat(mTopology.getRoot().getHeight()).isEqualTo(480f);
+
+ // Move display#1.
+ mTopology.rearrange(Map.of(0, new PointF(0, 0),
+ 1, new PointF(newOffset, -1080f)));
+ assertThat(mTopology.getRoot().getChildren().getFirst().getOffset()).isEqualTo(newOffset);
+
+ // Restore the topology, should apply saved offset, while keeping the current display sizes
+ mTopology = store.restoreTopology(mTopology);
+
+ // Offset is taken from the persisted topology.
+ assertThat(mTopology.getRoot().getChildren().getFirst().getOffset())
+ .isEqualTo(initialOffset);
+
+ // Size is taken from the current topology
+ assertThat(mTopology.getRoot().getWidth()).isEqualTo(640f);
+ assertThat(mTopology.getRoot().getHeight()).isEqualTo(480f);
+
+ // reloadTopologies was never called so, no file operations should have been performed.
+ verify(mInjector, never()).readUserTopologies(anyInt());
+ verify(mInjector, never()).getTopologyFilePrintWriter(anyInt());
+ }
+
+ @Test
+ public void testSaveTopologyInPrintWriter() throws IOException {
+ var store = new DisplayTopologyXmlStore(mInjector);
+ store.reloadTopologies(/*userId=*/ 0);
+ assertThat(store.saveTopology(mTopology)).isTrue();
+ verify(mPrintWriter0).print(eq(SIMPLE_TOPOLOGY));
+ verify(mPrintWriter0).markSuccess();
+ verify(mPrintWriter0).close();
+ }
+
+ @Test
+ public void testRestoreTopology() {
+ var store = new DisplayTopologyXmlStore(mInjector);
+ store.reloadTopologies(/*userId=*/ 0);
+ var restoredTopology = store.restoreTopology(mTopology);
+
+ // Should return null because there was nothing persisted before.
+ assertThat(restoredTopology).isNull();
+
+ // Persist topology
+ assertThat(store.saveTopology(mTopology)).isTrue();
+
+ // Should return new instance (restored), but equal.
+ var restoredTopologyAfterSave = store.restoreTopology(mTopology);
+ assertThat(restoredTopologyAfterSave).isNotSameInstanceAs(mTopology);
+ assertThat(restoredTopologyAfterSave).isEqualTo(mTopology);
+ }
+
+ @Test
+ public void testChangeUser() {
+ var store = new DisplayTopologyXmlStore(mInjector);
+ // Move display#1.
+ mTopology.rearrange(Map.of(0, new PointF(0, 0),
+ 1, new PointF(-10f, -1080f)));
+ assertThat(mTopology.getRoot().getChildren().getFirst().getOffset()).isEqualTo(-10f);
+
+ store.reloadTopologies(/*userId=*/ 1);
+ // Should return new instance (restored), with new offset.
+ var restoredTopology = store.restoreTopology(mTopology);
+ assertThat(restoredTopology).isNotSameInstanceAs(mTopology);
+ assertThat(restoredTopology.getRoot().getChildren().getFirst().getOffset())
+ .isEqualTo(-560f);
+
+ // Change user.
+ store.reloadTopologies(/*userId=*/ 0);
+ // Should return null because the topology is not found for user 0.
+ assertThat(store.restoreTopology(mTopology)).isNull();
+ }
+
+ @Test
+ public void testMultipleUserTopologies() {
+ var store = new DisplayTopologyXmlStore(mInjector);
+ store.reloadTopologies(/*userId=*/ 2);
+
+ var topology4Displays = new DisplayTopology();
+ topology4Displays.addDisplay(0, 800f, 600f);
+ topology4Displays.addDisplay(1, 1920f, 1080f);
+ topology4Displays.addDisplay(2, 480f, 640f);
+ topology4Displays.addDisplay(3, 768f, 1024f);
+
+ var restored = store.restoreTopology(topology4Displays);
+ assertThat(restored).isNotNull();
+ assertThat(restored.getPrimaryDisplayId()).isEqualTo(2);
+ assertThat(restored.getRoot()).isNotNull();
+ assertThat(restored.getRoot().getDisplayId()).isEqualTo(1);
+ assertThat(restored.getRoot().getWidth()).isEqualTo(1920f);
+ assertThat(restored.getRoot().getHeight()).isEqualTo(1080f);
+ assertThat(restored.getRoot().getOffset()).isEqualTo(0);
+ assertThat(restored.getRoot().getChildren().size()).isEqualTo(2);
+ assertThat(restored.getRoot().getChildren().getFirst().getDisplayId()).isEqualTo(0);
+ assertThat(restored.getRoot().getChildren().getFirst().getWidth()).isEqualTo(800f);
+ assertThat(restored.getRoot().getChildren().getFirst().getHeight()).isEqualTo(600f);
+ assertThat(restored.getRoot().getChildren().getFirst().getOffset()).isEqualTo(-50);
+ assertThat(restored.getRoot().getChildren().getFirst().getChildren().size())
+ .isEqualTo(0);
+ assertThat(restored.getRoot().getChildren().getLast().getDisplayId()).isEqualTo(2);
+ assertThat(restored.getRoot().getChildren().getLast().getWidth()).isEqualTo(480f);
+ assertThat(restored.getRoot().getChildren().getLast().getHeight()).isEqualTo(640f);
+ assertThat(restored.getRoot().getChildren().getLast().getOffset()).isEqualTo(-100);
+ assertThat(restored.getRoot().getChildren().getLast().getChildren().size())
+ .isEqualTo(1);
+
+ assertThat(restored.getRoot().getChildren().getLast().getChildren().getFirst()
+ .getDisplayId()).isEqualTo(3);
+ assertThat(restored.getRoot().getChildren().getLast().getChildren().getFirst()
+ .getWidth()).isEqualTo(768f);
+ assertThat(restored.getRoot().getChildren().getLast().getChildren().getFirst()
+ .getHeight()).isEqualTo(1024f);
+ assertThat(restored.getRoot().getChildren().getLast().getChildren().getFirst()
+ .getOffset()).isEqualTo(-300);
+ }
+
+ @Test
+ public void testLimitNumberOfTopologies() {
+ var store = new DisplayTopologyXmlStore(mInjector);
+ store.reloadTopologies(/*userId=*/ 0);
+ for (int i = 0; i < 110; i++) {
+ assertThat(store.saveTopology(generateTopology(i, i + 1))).isTrue();
+ }
+
+ assertThat(store.restoreTopology(generateTopology(110, 111))).isNull();
+ assertThat(store.restoreTopology(generateTopology(109, 110))).isNotNull();
+ assertThat(store.restoreTopology(generateTopology(10, 11))).isNotNull();
+ assertThat(store.restoreTopology(generateTopology(9, 10))).isNull();
+ for (int i = 0; i < 100; i++) {
+ assertThat(store.restoreTopology(generateTopology(i + 10, i + 11))).isNotNull();
+ }
+ }
+
+ @Test
+ public void testVendorTopology() throws IOException {
+ configureVendorTopologyFile(IMMUTABLE_TOPOLOGY);
+ var store = new DisplayTopologyXmlStore(mInjector);
+ store.reloadTopologies(/*userId=*/ 0);
+ var restored = store.restoreTopology(generateTopology(10, 11));
+ assertThat(restored).isNotNull();
+ assertThat(store.saveTopology(restored)).isFalse();
+
+ var userTopology = generateTopology(0, 1);
+ assertThat(store.restoreTopology(userTopology)).isNull();
+ assertThat(store.saveTopology(userTopology)).isTrue();
+ }
+
+ @Test
+ public void testProductTopology() throws IOException {
+ configureProductTopologyFile(IMMUTABLE_TOPOLOGY);
+ var store = new DisplayTopologyXmlStore(mInjector);
+ store.reloadTopologies(/*userId=*/ 0);
+ var restored = store.restoreTopology(generateTopology(10, 11));
+ assertThat(restored).isNotNull();
+ assertThat(store.saveTopology(restored)).isFalse();
+
+ var userTopology = generateTopology(0, 1);
+ assertThat(store.restoreTopology(userTopology)).isNull();
+ assertThat(store.saveTopology(userTopology)).isTrue();
+ }
+
+ private void configureTopologyFile(int userId, String initialFileContent,
+ AtomicFilePrintWriter printWriter) throws IOException {
+ doReturn(asInputStream(initialFileContent)).when(mInjector).readUserTopologies(eq(userId));
+ doReturn(printWriter).when(mInjector).getTopologyFilePrintWriter(eq(userId));
+ }
+
+ private void configureVendorTopologyFile(String initialFileContent) throws IOException {
+ doReturn(asInputStream(initialFileContent)).when(mInjector).readVendorTopologies();
+ }
+
+ private void configureProductTopologyFile(String initialFileContent) throws IOException {
+ doReturn(asInputStream(initialFileContent)).when(mInjector).readProductTopologies();
+ }
+}