diff --git a/android/src/main/java/com/facebook/flipper/plugins/sections/ChangesetDebug.java b/android/src/main/java/com/facebook/flipper/plugins/sections/ChangesetDebug.java index 3ee80dee1..06ebafa8a 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/sections/ChangesetDebug.java +++ b/android/src/main/java/com/facebook/flipper/plugins/sections/ChangesetDebug.java @@ -6,6 +6,7 @@ */ package com.facebook.flipper.plugins.sections; +import android.annotation.SuppressLint; import com.facebook.flipper.core.FlipperArray; import com.facebook.flipper.core.FlipperObject; import com.facebook.litho.sections.Change; @@ -15,8 +16,11 @@ import com.facebook.litho.sections.ChangesetDebugConfiguration.ChangesetDebugLis import com.facebook.litho.sections.Section; import com.facebook.litho.sections.SectionsLogEventUtils; import com.facebook.litho.sections.SectionsLogEventUtils.ApplyNewChangeSet; +import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; public class ChangesetDebug implements ChangesetDebugListener { @@ -49,7 +53,7 @@ public class ChangesetDebug implements ChangesetDebugListener { @Override public void onChangesetApplied( Section rootSection, - Section oldSection, + Section oldRootSection, ChangesInfo changesInfo, String surfaceId, @ApplyNewChangeSet int attribution, @@ -58,9 +62,13 @@ public class ChangesetDebug implements ChangesetDebugListener { final FlipperObject.Builder changesetData = new FlipperObject.Builder(); final String sourceName = SectionsLogEventUtils.applyNewChangeSetSourceToString(attribution); + extractSidePanelChangesetData(changesInfo, changesetData, rootSection.getGlobalKey()); - createSectionTree(rootSection, "", tree, oldSection); + createSectionTree(rootSection, tree, oldRootSection); + + List prevData = getDataFromPreviousTree(oldRootSection); + applyChangesInfoOnPreviousData(prevData, changesInfo, tree); sSectionsFlipperPlugin.onChangesetApplied( sourceName + " " + extra, @@ -154,7 +162,235 @@ public class ChangesetDebug implements ChangesetDebugListener { return null; } - static void createSectionTree( + /** Represents a data model node in a DiffSection. */ + private static class DataModelChangeInfo { + static final int UNCHANGED = -100; + Object model; + int operation = UNCHANGED; + String sectionKey; + } + + /** + * Skips all nodes which have been removed and finds the item on which the Change operation with + * the given index is applied on. + */ + private static int getPositionWithChangesApplied( + List dataInfos, int operationIndex) { + int count = -1; + int i = 0; + int size = dataInfos.size(); + for (i = 0; i < size; i++) { + if (dataInfos.get(i).operation != Change.DELETE + && dataInfos.get(i).operation != Change.DELETE_RANGE) { + count++; + } + + if (count == operationIndex) { + break; + } + } + + return i; + } + + /** For a given DiffSectionSpec section, returns the list of data on which it performs diffing. */ + private static List getDataFromPreviousSection(Section previousSection) { + List data = new ArrayList(); + + if (previousSection == null) { + return data; + } + + Class clasz = previousSection.getClass(); + final String diffSectionType = clasz.getSimpleName(); + + try { + if (diffSectionType.equals("DataDiffSection")) { + Field field = clasz.getDeclaredField("data"); + field.setAccessible(true); + data = (List) field.get(previousSection); + } else if (diffSectionType.equals("SingleComponentSection")) { + Field field = clasz.getDeclaredField("component"); + field.setAccessible(true); + data.add(field.get(previousSection)); + } + } catch (@SuppressLint("NewApi") NoSuchFieldException | IllegalAccessException e) { + e.printStackTrace(); + } + + return data; + } + + private static void addRemovedSectionNodes( + Section previousRoot, Section currentRoot, final FlipperArray.Builder tree) { + final Map prevSections = serializeChildren(previousRoot); + final Map currSections = serializeChildren(currentRoot); + + for (String prevSectionKey : prevSections.keySet()) { + if (!currSections.containsKey(prevSectionKey)) { + final FlipperObject.Builder nodeBuilder = new FlipperObject.Builder(); + final Section section = prevSections.get(prevSectionKey); + nodeBuilder.put("identifier", prevSectionKey); + nodeBuilder.put("name", section.getSimpleName()); + nodeBuilder.put( + "parent", section.getParent() == null ? null : section.getParent().getGlobalKey()); + nodeBuilder.put("removed", true); + tree.put(nodeBuilder.build()); + } + } + } + + private static Map serializeChildren(Section root) { + final Map children = new HashMap<>(); + serializeChildrenRecursive(root, children); + + return children; + } + + private static void serializeChildrenRecursive(Section root, Map children) { + if (root == null) { + return; + } + + children.put(root.getGlobalKey(), root); + + if (root.getChildren() == null) { + return; + } + + for (Section child : root.getChildren()) { + serializeChildrenRecursive(child, children); + } + } + + private static List getDataFromPreviousTree(Section previousRoot) { + final List dataModelChangeInfos = new ArrayList<>(); + + getDataFromPreviousTreeRecursive(previousRoot, dataModelChangeInfos); + + return dataModelChangeInfos; + } + + private static void getDataFromPreviousTreeRecursive( + Section previousRoot, List data) { + if (previousRoot == null) { + return; + } + + if (previousRoot.isDiffSectionSpec()) { + List models = getDataFromPreviousSection(previousRoot); + for (int i = 0; i < models.size(); i++) { + DataModelChangeInfo dataModelChangeInfo = new DataModelChangeInfo(); + dataModelChangeInfo.model = models.get(i); + dataModelChangeInfo.sectionKey = previousRoot.getGlobalKey(); + data.add(dataModelChangeInfo); + } + } else if (previousRoot.getChildren() != null) { + for (Section child : previousRoot.getChildren()) { + getDataFromPreviousTreeRecursive(child, data); + } + } + } + + private static void applyChangesInfoOnPreviousData( + List dataInfos, ChangesInfo changesInfo, FlipperArray.Builder tree) { + final List changes = changesInfo.getAllChanges(); + + for (int i = 0; i < changes.size(); i++) { + final Change change = changes.get(i); + int index = change.getIndex(); + + switch (change.getType()) { + case Change.INSERT: + { + DataModelChangeInfo dataInfo = new DataModelChangeInfo(); + if (change.getNextData() != null) { + dataInfo.model = change.getNextData().get(0); + } + dataInfo.sectionKey = + (String) change.getRenderInfo().getDebugInfo("section_global_key"); + + dataInfo.operation = Change.INSERT; + + int addToPosition = getPositionWithChangesApplied(dataInfos, index); + dataInfos.add(addToPosition, dataInfo); + break; + } + case Change.INSERT_RANGE: + { + int addToPosition = getPositionWithChangesApplied(dataInfos, index); + for (int item = 0; item < change.getCount(); item++) { + DataModelChangeInfo dataInfo = new DataModelChangeInfo(); + if (change.getNextData() != null) { + dataInfo.model = change.getNextData().get(item); + } + dataInfo.operation = Change.INSERT_RANGE; + dataInfo.sectionKey = + (String) change.getRenderInfos().get(item).getDebugInfo("section_global_key"); + dataInfos.add(addToPosition + item, dataInfo); + } + break; + } + case Change.DELETE: + { + int addToPosition = getPositionWithChangesApplied(dataInfos, index); + DataModelChangeInfo dataInfo = dataInfos.get(addToPosition); + dataInfo.operation = Change.DELETE; + break; + } + case Change.DELETE_RANGE: + { + int addToPosition = getPositionWithChangesApplied(dataInfos, index); + for (int del = addToPosition; del < addToPosition + change.getCount(); del++) { + dataInfos.get(del).operation = Change.DELETE_RANGE; + } + break; + } + case Change.UPDATE: + { + int getPosition = getPositionWithChangesApplied(dataInfos, index); + dataInfos.get(getPosition).operation = Change.UPDATE; + break; + } + case Change.UPDATE_RANGE: + { + for (int updateIndex = index; updateIndex < index + change.getCount(); updateIndex++) { + int getPosition = getPositionWithChangesApplied(dataInfos, updateIndex); + dataInfos.get(getPosition).operation = Change.UPDATE_RANGE; + } + break; + } + default: + break; + } + } + + for (int i = 0; i < dataInfos.size(); i++) { + + DataModelChangeInfo dataInfo = dataInfos.get(i); + final FlipperObject.Builder dataObject = new FlipperObject.Builder(); + + String name = dataInfo.model == null ? "N/A" : dataInfo.model.toString(); + int operation = dataInfo.operation; + + dataObject.put("identifier", name); + dataObject.put("name", name); + dataObject.put("parent", dataInfo.sectionKey); + dataObject.put("unchanged", operation == DataModelChangeInfo.UNCHANGED); + dataObject.put("inserted", operation == Change.INSERT || operation == Change.INSERT_RANGE); + dataObject.put("removed", operation == Change.DELETE || operation == Change.DELETE_RANGE); + dataObject.put("updated", operation == Change.UPDATE || operation == Change.UPDATE_RANGE); + tree.put(dataObject.build()); + } + } + + private static void createSectionTree( + Section rootSection, FlipperArray.Builder tree, Section oldRootSection) { + createSectionTreeRecursive(rootSection, "", tree, oldRootSection); + addRemovedSectionNodes(oldRootSection, rootSection, tree); + } + + private static void createSectionTreeRecursive( Section rootSection, String parentKey, FlipperArray.Builder tree, Section oldRootSection) { if (rootSection == null) { return; @@ -179,7 +415,7 @@ public class ChangesetDebug implements ChangesetDebugListener { } for (int i = 0; i < rootSection.getChildren().size(); i++) { - createSectionTree(rootSection.getChildren().get(i), globalKey, tree, oldRootSection); + createSectionTreeRecursive(rootSection.getChildren().get(i), globalKey, tree, oldRootSection); } } }