/**
* Copyright (c) 2017-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
package com.facebook.litho;
import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import android.graphics.Color;
import android.graphics.Rect;
import android.support.annotation.ColorInt;
import android.support.annotation.Nullable;
import android.support.v4.util.ArrayMap;
import android.support.v4.util.SimpleArrayMap;
import android.view.View;
import com.facebook.litho.annotations.Prop;
import com.facebook.litho.annotations.ResType;
import com.facebook.litho.annotations.State;
import com.facebook.yoga.YogaAlign;
import com.facebook.yoga.YogaConstants;
import com.facebook.yoga.YogaDirection;
import com.facebook.yoga.YogaEdge;
import com.facebook.yoga.YogaFlexDirection;
import com.facebook.yoga.YogaJustify;
import com.facebook.yoga.YogaNode;
import com.facebook.yoga.YogaPositionType;
import com.facebook.yoga.YogaUnit;
import com.facebook.yoga.YogaValue;
import static com.facebook.yoga.YogaUnit.PERCENT;
import static com.facebook.yoga.YogaUnit.POINT;
/**
* A DebugComponent represents a node in Litho's component hierarchy. DebugComponent removes the
* need to worry about implementation details of whether a node is represented by a
* {@link Component} or a {@link ComponentLayout}. The purpose of this class is for tools such as
* Stetho's UI inspector to be able to easily visualize a component hierarchy without worrying about
* implementation details of Litho.
*/
public final class DebugComponent {
private static final YogaValue YOGA_VALUE_UNDEFINED =
new YogaValue(YogaConstants.UNDEFINED, YogaUnit.UNDEFINED);
private static final YogaValue YOGA_VALUE_AUTO =
new YogaValue(YogaConstants.UNDEFINED, YogaUnit.AUTO);
private final static YogaEdge[] edges = YogaEdge.values();
private final static SimpleArrayMap<String, DebugComponent> mDebugNodes = new SimpleArrayMap<>();
private String mKey;
private WeakReference<InternalNode> mNode;
private int mComponentIndex;
private final SimpleArrayMap<String, SimpleArrayMap<String, String>> mStyleOverrides =
new SimpleArrayMap<>();
private final SimpleArrayMap<String, SimpleArrayMap<String, String>> mPropOverrides =
new SimpleArrayMap<>();
private final SimpleArrayMap<String, SimpleArrayMap<String, String>> mStateOverrides =
new SimpleArrayMap<>();
private DebugComponent() {}
static DebugComponent getInstance(InternalNode node, int componentIndex) {
final String globalKey = createKey(node, componentIndex);
DebugComponent debugComponent = mDebugNodes.get(globalKey);
if (debugComponent == null) {
debugComponent = new DebugComponent();
mDebugNodes.put(globalKey, debugComponent);
}
debugComponent.mKey = globalKey;
debugComponent.mNode = new WeakReference<>(node);
debugComponent.mComponentIndex = componentIndex;
return debugComponent;
}
/**
* @return The root {@link DebugComponent} of a LithoView. This should be the start of your
* traversal.
*/
public static DebugComponent getRootInstance(LithoView view) {
final ComponentTree component = view.getComponentTree();
final LayoutState layoutState = component == null ? null : component.getMainThreadLayoutState();
final InternalNode root = layoutState == null ? null : layoutState.getLayoutRoot();
if (root != null) {
final int outerWrapperComponentIndex = Math.max(0, root.getComponents().size() - 1);
return DebugComponent.getInstance(root, outerWrapperComponentIndex);
}
return null;
}
/**
* @return A conanical name for this component. Suitable to present to the user.
*/
public String getName() {
final InternalNode node = mNode.get();
if (node == null) {
return null;
}
if (node.getComponents().isEmpty()) {
switch (node.mYogaNode.getFlexDirection()) {
case COLUMN: return Column.class.getName();
case COLUMN_REVERSE: return ColumnReverse.class.getName();
case ROW: return Row.class.getName();
case ROW_REVERSE: return RowReverse.class.getName();
}
}
return node
.getComponents()
.get(mComponentIndex)
.getLifecycle()
.getClass()
.getName();
}
/**
* Get the list of components composed by this component. This will not include any {@link View}s
* that are mounted by this component as those are not components.
* Use {@link this#getMountedViews} for that.
*
* @return A list of child components.
*/
public List<DebugComponent> getChildComponents() {
final InternalNode node = mNode.get();
if (node == null) {
return Collections.EMPTY_LIST;
}
if (mComponentIndex > 0) {
final int wrappedComponentIndex = mComponentIndex - 1;
return Arrays.asList(getInstance(node, wrappedComponentIndex));
}
final ArrayList<DebugComponent> children = new ArrayList<>();
for (int i = 0, count = node.getChildCount(); i < count; i++) {
final InternalNode childNode = node.getChildAt(i);
final int outerWrapperComponentIndex = Math.max(0, childNode.getComponents().size() - 1);
children.add(getInstance(childNode, outerWrapperComponentIndex));
}
if (node.hasNestedTree()) {
final InternalNode nestedTree = node.getNestedTree();
for (int i = 0, count = nestedTree.getChildCount(); i < count; i++) {
final InternalNode childNode = nestedTree.getChildAt(i);
children.add(getInstance(childNode, Math.max(0, childNode.getComponents().size() - 1)));
}
}
return children;
}
/**
* @return A list of mounted views.
*/
public List<View> getMountedViews() {
if (mComponentIndex > 0) {
return Collections.EMPTY_LIST;
}
final InternalNode node = mNode.get();
final ComponentContext context = node == null ? null : node.getContext();
final ComponentTree tree = context == null ? null : context.getComponentTree();
final LithoView view = tree == null ? null : tree.getLithoView();
final MountState mountState = view == null ? null : view.getMountState();
final ArrayList<View> children = new ArrayList<>();
if (mountState != null) {
for (int i = 0, count = mountState.getItemCount(); i < count; i++) {
final MountItem mountItem = mountState.getItemAt(i);
final Component component = mountItem == null ? null : mountItem.getComponent();
if (component != null &&
component == node.getRootComponent() &&
Component.isMountViewSpec(component)) {
children.add((View) mountItem.getContent());
}
}
}
return children;
}
/**
* @return The litho view hosting this component.
*/
public LithoView getLithoView() {
final InternalNode node = mNode.get();
final ComponentContext c = node == null ? null : node.getContext();
final ComponentTree tree = c == null ? null : c.getComponentTree();
return tree == null ? null : tree.getLithoView();
}
/**
* @return The bounds of this component relative to its hosting {@link LithoView}.
*/
public Rect getBoundsInLithoView() {
final InternalNode node = mNode.get();
if (node == null) {
return new Rect();
}
final int x = getXFromRoot(node);
final int y = getYFromRoot(node);
return new Rect(x, y, x + node.getWidth(), y + node.getHeight());
}
/**
* @return The bounds of this component relative to its parent.
*/
public Rect getBounds() {
final InternalNode node = mNode.get();
if (node == null) {
return new Rect();
}
final int x = node.getX();
final int y = node.getY();
return new Rect(x, y, x + node.getWidth(), y + node.getHeight());
}
/**
* @return Key-value mapping of this components layout styles.
*/
public Map<String, String> getStyles() {
final InternalNode node = mNode.get();
if (node == null || !isLayoutNode()) {
return Collections.EMPTY_MAP;
}
final Map<String, String> styles = new ArrayMap<>();
final YogaNode yogaNode = node.mYogaNode;
final YogaNode defaults = ComponentsPools.acquireYogaNode(node.getContext());
styles.put("background", "<drawable>");
styles.put("foreground", "<drawable>");
styles.put("direction", toCSSString(yogaNode.getStyleDirection()));
styles.put("flex-direction", toCSSString(yogaNode.getFlexDirection()));
styles.put("justify-content", toCSSString(yogaNode.getJustifyContent()));
styles.put("align-items", toCSSString(yogaNode.getAlignItems()));
styles.put("align-self", toCSSString(yogaNode.getAlignSelf()));
styles.put("align-content", toCSSString(yogaNode.getAlignContent()));
styles.put("position", toCSSString(yogaNode.getPositionType()));
styles.put("flex-grow", Float.toString(yogaNode.getFlexGrow()));
styles.put("flex-shrink", Float.toString(yogaNode.getFlexShrink()));
styles.put("flex-basis", yogaNode.getFlexBasis().toString());
styles.put("width", yogaNode.getWidth().toString());
styles.put("min-width", yogaNode.getMinWidth().toString());
styles.put("max-width", yogaNode.getMaxWidth().toString());
styles.put("height", yogaNode.getHeight().toString());
styles.put("min-height", yogaNode.getMinHeight().toString());
styles.put("max-height", yogaNode.getMaxHeight().toString());
for (YogaEdge edge : edges) {
final String key = "margin-" + toCSSString(edge);
styles.put(key, yogaNode.getMargin(edge).toString());
}
for (YogaEdge edge : edges) {
final String key = "padding-" + toCSSString(edge);
styles.put(key, yogaNode.getPadding(edge).toString());
}
for (YogaEdge edge : edges) {
final String key = "position-" + toCSSString(edge);
styles.put(key, yogaNode.getPosition(edge).toString());
}
for (YogaEdge edge : edges) {
final String key = "border-" + toCSSString(edge);
styles.put(key, Float.toString(yogaNode.getBorder(edge)));
}
ComponentsPools.release(defaults);
return styles;
}
/**
* @return Key-value mapping of this components props.
*/
public Map<String, String> getProps() {
final InternalNode node = mNode.get();
final Component component = node == null || node.getComponents().isEmpty()
? null
: node.getComponents().get(mComponentIndex);
if (component == null) {
return Collections.EMPTY_MAP;
}
final Map<String, String> props = new ArrayMap<>();
final ComponentLifecycle.StateContainer stateContainer = component.getStateContainer();
for (Field field : component.getClass().getDeclaredFields()) {
try {
field.setAccessible(true);
final Prop propAnnotation = field.getAnnotation(Prop.class);
if (isPrimitiveField(field) && propAnnotation != null) {
final Object value = field.get(component);
if (value != stateContainer && !(value instanceof ComponentLifecycle)) {
if (value == null) {
props.put(field.getName(), "null");
} else if (propAnnotation.resType() == ResType.COLOR) {
final int i = (Integer) value;
props.put(
field.getName(),
("#" +
Integer.toHexString(((i & 0xF0000000) >> 28) & 0xf) +
Integer.toHexString(((i & 0x0F000000) >> 24) & 0xf) +
Integer.toHexString(((i & 0x00F00000) >> 20) & 0xf) +
Integer.toHexString(((i & 0x000F0000) >> 16) & 0xf) +
Integer.toHexString(((i & 0x0000F000) >> 12) & 0xf) +
Integer.toHexString(((i & 0x00000F00) >> 8) & 0xf) +
Integer.toHexString(((i & 0x000000F0) >> 4) & 0xf) +
Integer.toHexString((i & 0x0000000F) & 0xf)
).toUpperCase());
} else {
props.put(field.getName(), value.toString());
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return props;
}
/**
* @return Key-value mapping of this components state.
*/
public Map<String, String> getState() {
final InternalNode node = mNode.get();
final Component component = node == null || node.getComponents().isEmpty()
? null
: node.getComponents().get(mComponentIndex);
if (component == null) {
return Collections.EMPTY_MAP;
}
final ComponentLifecycle.StateContainer stateContainer = component.getStateContainer();
if (stateContainer == null) {
return Collections.EMPTY_MAP;
}
final Map<String, String> state = new ArrayMap<>();
for (Field field : stateContainer.getClass().getDeclaredFields()) {
try {
field.setAccessible(true);
if (isPrimitiveField(field) && field.getAnnotation(State.class) != null) {
final Object value = field.get(stateContainer);
if (!(value instanceof ComponentLifecycle)) {
state.put(field.getName(), value == null ? "null" : value.toString());
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
return state;
}
/**
* @return Registed an override for a style key with a certain value. This override will be used
* The next time this component is rendered.
*/
public void setStyleOverride(String key, String value) {
SimpleArrayMap<String, String> styles = mStyleOverrides.get(mKey);
if (styles == null) {
styles = new SimpleArrayMap<>();
mStyleOverrides.put(mKey, styles);
}
styles.put(key, value);
getLithoView().forceRelayout();
}
/**
* @return Registed an override for a prop key with a certain value. This override will be used
* The next time this component is rendered.
*/
public void setPropOverride(String key, String value) {
SimpleArrayMap<String, String> props = mPropOverrides.get(mKey);
if (props == null) {
props = new SimpleArrayMap<>();
mPropOverrides.put(mKey, props);
}
props.put(key, value);
getLithoView().forceRelayout();
}
/**
* @return Registed an override for a state key with a certain value. This override will be used
* The next time this component is rendered.
*/
public void setStateOverride(String key, String value) {
SimpleArrayMap<String, String> props = mStateOverrides.get(mKey);
if (props == null) {
props = new SimpleArrayMap<>();
mStateOverrides.put(mKey, props);
}
props.put(key, value);
getLithoView().forceRelayout();
}
/**
* @return the {@link ComponentContext} for this component.
*/
public ComponentContext getContext() {
return mNode.get().getContext();
}
/**
* @return True if this not has layout information attached to it (backed by a Yoga node)
*/
public boolean isLayoutNode() {
return mNode.get().getComponents().isEmpty() || mComponentIndex == 0;
}
/**
* @return This component's testKey or null if none is set.
*/
public String getTestKey() {
return isLayoutNode() ? mNode.get().getTestKey() : null;
}
/**
* @return This component's key or null if none is set.
*/
public String getKey() {
final InternalNode node = mNode.get();
if (node != null && !node.getComponents().isEmpty()) {
final Component component = node.getComponents().get(mComponentIndex);
return component == null ? null : component.getKey();
}
return null;
}
void applyOverrides() {
final InternalNode node = mNode.get();
if (node == null) {
return;
}
if (mStyleOverrides.containsKey(mKey)) {
final SimpleArrayMap<String, String> styles = mStyleOverrides.get(mKey);
for (int i = 0, size = styles.size(); i < size; i++) {
final String key = styles.keyAt(i);
final String value = styles.get(key);
try {
if (key.equals("background")) {
node.backgroundColor(parseColor(value));
}
if (key.equals("foreground")) {
node.foregroundColor(parseColor(value));
}
if (key.equals("direction")) {
node.layoutDirection(YogaDirection.valueOf(toEnumString(value)));
}
if (key.equals("flex-direction")) {
node.flexDirection(YogaFlexDirection.valueOf(toEnumString(value)));
}
if (key.equals("justify-content")) {
node.justifyContent(YogaJustify.valueOf(toEnumString(value)));
}
if (key.equals("align-items")) {
node.alignItems(YogaAlign.valueOf(toEnumString(value)));
}
if (key.equals("align-self")) {
node.alignSelf(YogaAlign.valueOf(toEnumString(value)));
}
if (key.equals("align-content")) {
node.alignContent(YogaAlign.valueOf(toEnumString(value)));
}
if (key.equals("position")) {
node.positionType(YogaPositionType.valueOf(toEnumString(value)));
}
if (key.equals("flex-grow")) {
node.flexGrow(parseFloat(value));
}
if (key.equals("flex-shrink")) {
node.flexShrink(parseFloat(value));
}
} catch (IllegalArgumentException ignored) {
// ignore errors when the user suplied an invalid enum value
}
if (key.equals("flex-basis")) {
final YogaValue flexBasis = yogaValueFromString(value);
if (flexBasis == null) {
continue;
}
switch (flexBasis.unit) {
case AUTO:
node.flexBasisAuto();
break;
case UNDEFINED:
case POINT:
node.flexBasisPx(FastMath.round(flexBasis.value));
break;
case PERCENT:
node.flexBasisPercent(FastMath.round(flexBasis.value));
break;
}
}
if (key.equals("width")) {
final YogaValue width = yogaValueFromString(value);
if (width == null) {
continue;
}
switch (width.unit) {
case AUTO:
node.widthAuto();
break;
case UNDEFINED:
case POINT:
node.widthPx(FastMath.round(width.value));
break;
case PERCENT:
node.widthPercent(FastMath.round(width.value));
break;
}
}
if (key.equals("min-width")) {
final YogaValue minWidth = yogaValueFromString(value);
if (minWidth == null) {
continue;
}
switch (minWidth.unit) {
case UNDEFINED:
case POINT:
node.minWidthPx(FastMath.round(minWidth.value));
break;
case PERCENT:
node.minWidthPercent(FastMath.round(minWidth.value));
break;
}
}
if (key.equals("max-width")) {
final YogaValue maxWidth = yogaValueFromString(value);
if (maxWidth == null) {
continue;
}
switch (maxWidth.unit) {
case UNDEFINED:
case POINT:
node.maxWidthPx(FastMath.round(maxWidth.value));
break;
case PERCENT:
node.maxWidthPercent(FastMath.round(maxWidth.value));
break;
}
}
if (key.equals("height")) {
final YogaValue height = yogaValueFromString(value);
if (height == null) {
continue;
}
switch (height.unit) {
case AUTO:
node.heightAuto();
break;
case UNDEFINED:
case POINT:
node.heightPx(FastMath.round(height.value));
break;
case PERCENT:
node.heightPercent(FastMath.round(height.value));
break;
}
}
if (key.equals("min-height")) {
final YogaValue minHeight = yogaValueFromString(value);
if (minHeight == null) {
continue;
}
switch (minHeight.unit) {
case UNDEFINED:
case POINT:
node.minHeightPx(FastMath.round(minHeight.value));
break;
case PERCENT:
node.minHeightPercent(FastMath.round(minHeight.value));
break;
}
}
if (key.equals("max-height")) {
final YogaValue maxHeight = yogaValueFromString(value);
if (maxHeight == null) {
continue;
}
switch (maxHeight.unit) {
case UNDEFINED:
case POINT:
node.maxHeightPx(FastMath.round(maxHeight.value));
break;
case PERCENT:
node.maxHeightPercent(FastMath.round(maxHeight.value));
break;
}
}
for (YogaEdge edge : edges) {
if (key.equals("margin-" + toCSSString(edge))) {
final YogaValue margin = yogaValueFromString(value);
if (margin == null) {
continue;
}
switch (margin.unit) {
case UNDEFINED:
case POINT:
node.marginPx(edge, FastMath.round(margin.value));
break;
case AUTO:
node.marginAuto(edge);
break;
case PERCENT:
node.marginPercent(edge, FastMath.round(margin.value));
break;
}
}
}
for (YogaEdge edge : edges) {
if (key.equals("padding-" + toCSSString(edge))) {
final YogaValue padding = yogaValueFromString(value);
if (padding == null) {
continue;
}
switch (padding.unit) {
case UNDEFINED:
case POINT:
node.paddingPx(edge, FastMath.round(padding.value));
break;
case PERCENT:
node.paddingPercent(edge, FastMath.round(padding.value));
break;
}
}
}
for (YogaEdge edge : edges) {
if (key.equals("position-" + toCSSString(edge))) {
final YogaValue position = yogaValueFromString(value);
if (position == null) {
continue;
}
switch (position.unit) {
case UNDEFINED:
case POINT:
node.positionPx(edge, FastMath.round(position.value));
break;
case PERCENT:
node.positionPercent(edge, FastMath.round(position.value));
break;
}
}
}
for (YogaEdge edge : edges) {
if (key.equals("border-" + toCSSString(edge))) {
final float border = parseFloat(value);
node.borderWidthPx(edge, FastMath.round(border));
}
}
}
}
if (mPropOverrides.containsKey(mKey)) {
final Component component = node.getRootComponent();
if (component != null) {
final SimpleArrayMap<String, String> props = mPropOverrides.get(mKey);
for (int i = 0, size = props.size(); i < size; i++) {
final String key = props.keyAt(i);
applyReflectiveOverride(component, key, props.get(key));
}
}
}
if (mStateOverrides.containsKey(mKey)) {
final Component component = node.getRootComponent();
final ComponentLifecycle.StateContainer stateContainer =
component == null ? null : component.getStateContainer();
if (stateContainer != null) {
final SimpleArrayMap<String, String> state = mStateOverrides.get(mKey);
for (int i = 0, size = state.size(); i < size; i++) {
final String key = state.keyAt(i);
applyReflectiveOverride(stateContainer, key, state.get(key));
}
}
}
}
private InternalNode parent(InternalNode node) {
final InternalNode parent = node.getParent();
return parent != null ? parent : node.getNestedTreeHolder();
}
private int getXFromRoot(InternalNode node) {
if (node == null) {
return 0;
}
return node.getX() + getXFromRoot(parent(node));
}
private int getYFromRoot(InternalNode node) {
if (node == null) {
return 0;
}
return node.getY() + getYFromRoot(parent(node));
}
private static String toCSSString(Object obj) {
final String str = obj.toString();
final StringBuilder builder = new StringBuilder(str.length());
builder.append(str);
for (int i = 0, length = builder.length(); i < length; ++i) {
final char oldChar = builder.charAt(i);
final char lowerChar = Character.toLowerCase(oldChar);
final char newChar = lowerChar == '_' ? '-' : lowerChar;
builder.setCharAt(i, newChar);
}
return builder.toString();
}
private static boolean isPrimitiveField(Field field) {
return field.getType().isPrimitive() ||
CharSequence.class.isAssignableFrom(field.getType());
}
private static String toEnumString(String str) {
final StringBuilder builder = new StringBuilder(str.length());
builder.append(str);
for (int i = 0, length = builder.length(); i < length; ++i) {
final char oldChar = builder.charAt(i);
final char upperChar = Character.toUpperCase(oldChar);
final char newChar = upperChar == '-' ? '_' : upperChar;
builder.setCharAt(i, newChar);
}
return builder.toString();
}
private static float parseFloat(@Nullable String s) {
if (s == null) {
return 0;
}
try {
return Float.parseFloat(s);
} catch (NumberFormatException e) {
return 0;
}
}
@ColorInt
private static int parseColor(String color) {
if (color == null || color.length() == 0) {
return Color.TRANSPARENT;
}
// Color.parse does not handle hax code with 3 ints e.g. #123
if (color.length() == 4) {
final char r = color.charAt(1);
final char g = color.charAt(2);
final char b = color.charAt(3);
color = "#" + r + r + g + g + b + b;
}
return Color.parseColor(color);
}
private void applyReflectiveOverride(Object o, String key, String value) {
try {
final Field field = o.getClass().getDeclaredField(key);
final Class type = field.getType();
final Prop prop = field.getAnnotation(Prop.class);
field.setAccessible(true);
if (type.equals(short.class)) {
field.set(o, Short.parseShort(value));
} else if (type.equals(int.class)) {
if (prop != null && prop.resType() == ResType.COLOR) {
field.set(o, parseColor(value));
} else {
field.set(o, Integer.parseInt(value));
}
} else if (type.equals(long.class)) {
field.set(o, Long.parseLong(value));
} else if (type.equals(float.class)) {
field.set(o, Float.parseFloat(value));
} else if (type.equals(double.class)) {
field.set(o, Double.parseDouble(value));
} else if (type.equals(boolean.class)) {
field.set(o, Boolean.parseBoolean(value));
} else if (type.equals(byte.class)) {
field.set(o, Byte.parseByte(value));
} else if (type.equals(char.class)) {
field.set(o, value.charAt(0));
} else if (CharSequence.class.isAssignableFrom(type)) {
field.set(o, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private static YogaValue yogaValueFromString(String s) {
if (s == null) {
return null;
}
if ("undefined".equals(s)) {
return YOGA_VALUE_UNDEFINED;
}
if ("auto".equals(s)) {
return YOGA_VALUE_AUTO;
}
if (s.endsWith("%")) {
return new YogaValue(parseFloat(s.substring(0, s.length() - 1)), PERCENT);
}
return new YogaValue(parseFloat(s), POINT);
}
private static String createKey(InternalNode node, int componentIndex) {
final InternalNode parent = node.getParent();
final InternalNode nestedTreeHolder = node.getNestedTreeHolder();
String key;
if (parent != null) {
key = createKey(parent, 0) + "." + parent.getChildIndex(node);
} else if (nestedTreeHolder != null) {
key = createKey(nestedTreeHolder, 0) + ".nested";
} else {
final ComponentContext c = node.getContext();
final ComponentTree tree = c.getComponentTree();
key = Integer.toString(System.identityHashCode(tree));
}
return key + "(" + componentIndex + ")";
}
public String getId() {
return mKey;
}
}