Initial commit 🎉

fbshipit-source-id: b6fc29740c6875d2e78953b8a7123890a67930f2
Co-authored-by: Sebastian McKenzie <sebmck@fb.com>
Co-authored-by: John Knox <jknox@fb.com>
Co-authored-by: Emil Sjölander <emilsj@fb.com>
Co-authored-by: Pritesh Nandgaonkar <prit91@fb.com>
This commit is contained in:
Daniel Büchele
2018-04-13 08:38:06 -07:00
committed by Daniel Buchele
commit fbbf8cf16b
659 changed files with 87130 additions and 0 deletions

View File

@@ -0,0 +1,708 @@
/*
* Copyright (c) 2018-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the LICENSE
* file in the root directory of this source tree.
*
*/
package com.facebook.sonar.plugins.inspector.descriptors;
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Color;
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.support.v4.view.MarginLayoutParamsCompat;
import android.support.v4.view.ViewCompat;
import android.util.SparseArray;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewGroup.MarginLayoutParams;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import com.facebook.sonar.core.SonarDynamic;
import com.facebook.sonar.core.SonarObject;
import com.facebook.sonar.plugins.inspector.HighlightedOverlay;
import com.facebook.sonar.plugins.inspector.InspectorValue;
import com.facebook.sonar.plugins.inspector.Named;
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
import com.facebook.sonar.plugins.inspector.Touch;
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityUtil;
import com.facebook.sonar.plugins.inspector.descriptors.utils.EnumMapping;
import com.facebook.stetho.common.android.ResourcesUtil;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.annotation.Nullable;
public class ViewDescriptor extends NodeDescriptor<View> {
private static Field sKeyedTagsField;
private static Field sListenerInfoField;
private static Field sOnClickListenerField;
static {
try {
sKeyedTagsField = View.class.getDeclaredField("mKeyedTags");
sKeyedTagsField.setAccessible(true);
sListenerInfoField = View.class.getDeclaredField("mListenerInfo");
sListenerInfoField.setAccessible(true);
final String viewInfoClassName = View.class.getName() + "$ListenerInfo";
sOnClickListenerField = Class.forName(viewInfoClassName).getDeclaredField("mOnClickListener");
sOnClickListenerField.setAccessible(true);
} catch (Exception ignored) {
}
}
@Override
public void init(final View node) {}
@Override
public String getId(View node) {
return Integer.toString(System.identityHashCode(node));
}
@Override
public String getName(View node) {
return node.getClass().getSimpleName();
}
@Override
public int getChildCount(View node) {
return 0;
}
@Override
public @Nullable Object getChildAt(View node, int index) {
return null;
}
@Override
public List<Named<SonarObject>> getData(View node) {
final SonarObject.Builder viewProps =
new SonarObject.Builder()
.put("height", InspectorValue.mutable(node.getHeight()))
.put("width", InspectorValue.mutable(node.getWidth()))
.put("alpha", InspectorValue.mutable(node.getAlpha()))
.put("visibility", sVisibilityMapping.get(node.getVisibility()))
.put("background", fromDrawable(node.getBackground()))
.put("tag", InspectorValue.mutable(node.getTag()))
.put("keyedTags", getTags(node))
.put("layoutParams", getLayoutParams(node))
.put(
"state",
new SonarObject.Builder()
.put("enabled", InspectorValue.mutable(node.isEnabled()))
.put("activated", InspectorValue.mutable(node.isActivated()))
.put("focused", node.isFocused())
.put("selected", InspectorValue.mutable(node.isSelected())))
.put(
"bounds",
new SonarObject.Builder()
.put("left", InspectorValue.mutable(node.getLeft()))
.put("right", InspectorValue.mutable(node.getRight()))
.put("top", InspectorValue.mutable(node.getTop()))
.put("bottom", InspectorValue.mutable(node.getBottom())))
.put(
"padding",
new SonarObject.Builder()
.put("left", InspectorValue.mutable(node.getPaddingLeft()))
.put("top", InspectorValue.mutable(node.getPaddingTop()))
.put("right", InspectorValue.mutable(node.getPaddingRight()))
.put("bottom", InspectorValue.mutable(node.getPaddingBottom())))
.put(
"rotation",
new SonarObject.Builder()
.put("x", InspectorValue.mutable(node.getRotationX()))
.put("y", InspectorValue.mutable(node.getRotationY()))
.put("z", InspectorValue.mutable(node.getRotation())))
.put(
"scale",
new SonarObject.Builder()
.put("x", InspectorValue.mutable(node.getScaleX()))
.put("y", InspectorValue.mutable(node.getScaleY())))
.put(
"pivot",
new SonarObject.Builder()
.put("x", InspectorValue.mutable(node.getPivotX()))
.put("y", InspectorValue.mutable(node.getPivotY())));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
viewProps
.put("layoutDirection", sLayoutDirectionMapping.get(node.getLayoutDirection()))
.put("textDirection", sTextDirectionMapping.get(node.getTextDirection()))
.put("textAlignment", sTextAlignmentMapping.get(node.getTextAlignment()));
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
viewProps.put("elevation", InspectorValue.mutable(node.getElevation()));
}
SonarObject.Builder translation =
new SonarObject.Builder()
.put("x", InspectorValue.mutable(node.getTranslationX()))
.put("y", InspectorValue.mutable(node.getTranslationY()));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
translation.put("z", InspectorValue.mutable(node.getTranslationZ()));
}
viewProps.put("translation", translation);
SonarObject.Builder position =
new SonarObject.Builder()
.put("x", InspectorValue.mutable(node.getX()))
.put("y", InspectorValue.mutable(node.getY()));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) {
position.put("z", InspectorValue.mutable(node.getZ()));
}
viewProps.put("position", position);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
viewProps.put("foreground", fromDrawable(node.getForeground()));
}
return Arrays.asList(
new Named<>("View", viewProps.build()),
new Named<>("Accessibility", getAccessibilityData(node)));
}
private static SonarObject getAccessibilityData(View view) {
final SonarObject.Builder accessibilityProps = new SonarObject.Builder();
// This needs to be an empty string to be mutable. See t20470623.
CharSequence contentDescription =
view.getContentDescription() != null ? view.getContentDescription() : "";
accessibilityProps.put("content-description", InspectorValue.mutable(contentDescription));
accessibilityProps.put("focusable", InspectorValue.mutable(view.isFocusable()));
accessibilityProps.put("node-info", AccessibilityUtil.getAccessibilityNodeInfoProperties(view));
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
accessibilityProps.put(
"important-for-accessibility",
AccessibilityUtil.sImportantForAccessibilityMapping.get(
view.getImportantForAccessibility()));
}
AccessibilityUtil.addTalkbackProperties(accessibilityProps, view);
return accessibilityProps.build();
}
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
@Override
public void setValue(View node, String[] path, SonarDynamic value) {
if (path[0].equals("Accessibility")) {
setAccessibilityValue(node, path, value);
}
if (!path[0].equals("View")) {
return;
}
switch (path[1]) {
case "elevation":
node.setElevation(value.asFloat());
break;
case "alpha":
node.setAlpha(value.asFloat());
break;
case "visibility":
node.setVisibility(sVisibilityMapping.get(value.asString()));
break;
case "layoutParams":
setLayoutParams(node, Arrays.copyOfRange(path, 1, path.length), value);
break;
case "layoutDirection":
node.setLayoutDirection(sLayoutDirectionMapping.get(value.asString()));
break;
case "textDirection":
node.setTextDirection(sTextDirectionMapping.get(value.asString()));
break;
case "textAlignment":
node.setTextAlignment(sTextAlignmentMapping.get(value.asString()));
break;
case "background":
node.setBackground(new ColorDrawable(value.asInt()));
break;
case "foreground":
node.setForeground(new ColorDrawable(value.asInt()));
break;
case "state":
switch (path[2]) {
case "enabled":
node.setEnabled(value.asBoolean());
break;
case "activated":
node.setActivated(value.asBoolean());
break;
case "selected":
node.setSelected(value.asBoolean());
break;
}
break;
case "bounds":
switch (path[2]) {
case "left":
node.setLeft(value.asInt());
break;
case "top":
node.setTop(value.asInt());
break;
case "right":
node.setRight(value.asInt());
break;
case "bottom":
node.setBottom(value.asInt());
break;
}
break;
case "padding":
switch (path[2]) {
case "left":
node.setPadding(
value.asInt(),
node.getPaddingTop(),
node.getPaddingRight(),
node.getPaddingBottom());
break;
case "top":
node.setPadding(
node.getPaddingLeft(),
value.asInt(),
node.getPaddingRight(),
node.getPaddingBottom());
break;
case "right":
node.setPadding(
node.getPaddingLeft(),
node.getPaddingTop(),
value.asInt(),
node.getPaddingBottom());
break;
case "bottom":
node.setPadding(
node.getPaddingLeft(), node.getPaddingTop(), node.getPaddingRight(), value.asInt());
break;
}
break;
case "rotation":
switch (path[2]) {
case "x":
node.setRotationX(value.asFloat());
break;
case "y":
node.setRotationY(value.asFloat());
break;
case "z":
node.setRotation(value.asFloat());
break;
}
break;
case "translation":
switch (path[2]) {
case "x":
node.setTranslationX(value.asFloat());
break;
case "y":
node.setTranslationY(value.asFloat());
break;
case "z":
node.setTranslationZ(value.asFloat());
break;
}
break;
case "position":
switch (path[2]) {
case "x":
node.setX(value.asFloat());
break;
case "y":
node.setY(value.asFloat());
break;
case "z":
node.setZ(value.asFloat());
break;
}
break;
case "scale":
switch (path[2]) {
case "x":
node.setScaleX(value.asFloat());
break;
case "y":
node.setScaleY(value.asFloat());
break;
}
break;
case "pivot":
switch (path[2]) {
case "x":
node.setPivotY(value.asFloat());
break;
case "y":
node.setPivotX(value.asFloat());
break;
}
break;
case "width":
LayoutParams lpw = node.getLayoutParams();
lpw.width = value.asInt();
node.setLayoutParams(lpw);
break;
case "height":
LayoutParams lph = node.getLayoutParams();
lph.height = value.asInt();
node.setLayoutParams(lph);
break;
}
invalidate(node);
}
private void setAccessibilityValue(View node, String[] path, SonarDynamic value) {
switch (path[1]) {
case "focusable":
node.setFocusable(value.asBoolean());
break;
case "important-for-accessibility":
node.setImportantForAccessibility(
AccessibilityUtil.sImportantForAccessibilityMapping.get(value.asString()));
break;
case "content-description":
node.setContentDescription(value.asString());
break;
}
invalidate(node);
}
@Override
public List<Named<String>> getAttributes(View node) throws Exception {
final List<Named<String>> attributes = new ArrayList<>();
final String resourceId = getResourceId(node);
if (resourceId != null) {
attributes.add(new Named<>("id", resourceId));
}
if (sListenerInfoField != null && sOnClickListenerField != null) {
final Object listenerInfo = sListenerInfoField.get(node);
if (listenerInfo != null) {
final OnClickListener clickListener =
(OnClickListener) sOnClickListenerField.get(listenerInfo);
if (clickListener != null) {
attributes.add(new Named<>("onClick", clickListener.getClass().getName()));
}
}
}
return attributes;
}
@Nullable
private static String getResourceId(View node) {
final int id = node.getId();
if (id == View.NO_ID) {
return null;
}
return ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id);
}
@Override
public void setHighlighted(View node, boolean selected) {
// We need to figure out whether the given View has a parent View since margins are not
// included within a View's bounds. So, in order to display the margin values for a particular
// view, we need to apply an overlay on its parent rather than itself.
final View targetView;
final ViewParent parent = node.getParent();
if (parent instanceof View) {
targetView = (View) parent;
} else {
targetView = node;
}
if (!selected) {
HighlightedOverlay.removeHighlight(targetView);
return;
}
final Rect padding =
new Rect(
ViewCompat.getPaddingStart(node),
node.getPaddingTop(),
ViewCompat.getPaddingEnd(node),
node.getPaddingBottom());
final Rect margin;
final ViewGroup.LayoutParams params = node.getLayoutParams();
if (params instanceof ViewGroup.MarginLayoutParams) {
final ViewGroup.MarginLayoutParams marginParams = (ViewGroup.MarginLayoutParams) params;
margin =
new Rect(
MarginLayoutParamsCompat.getMarginStart(marginParams),
marginParams.topMargin,
MarginLayoutParamsCompat.getMarginEnd(marginParams),
marginParams.bottomMargin);
} else {
margin = new Rect();
}
final int left = node.getLeft();
final int top = node.getTop();
final Rect contentBounds = new Rect(left, top, left + node.getWidth(), top + node.getHeight());
if (targetView == node) {
// If the View doesn't have a parent View that we're applying the overlay to, then
// we need to ensure that it is aligned to 0, 0, rather than its relative location to its
// parent
contentBounds.offset(-left, -top);
}
HighlightedOverlay.setHighlighted(targetView, margin, padding, contentBounds);
}
@Override
public void hitTest(View node, Touch touch) {
touch.finish();
}
@Override
public @Nullable String getDecoration(View obj) {
return null;
}
@Override
public boolean matches(String query, View node) throws Exception {
final String resourceId = getResourceId(node);
if (resourceId != null && resourceId.toLowerCase().contains(query)) {
return true;
}
final NodeDescriptor objectDescriptor = descriptorForClass(Object.class);
return objectDescriptor.matches(query, node);
}
private SonarObject getTags(final View node) {
final SonarObject.Builder tags = new SonarObject.Builder();
if (sKeyedTagsField == null) {
return tags.build();
}
new ErrorReportingRunnable() {
@Override
protected void runOrThrow() throws Exception {
final SparseArray keyedTags = (SparseArray) sKeyedTagsField.get(node);
if (keyedTags != null) {
for (int i = 0, count = keyedTags.size(); i < count; i++) {
final String id =
ResourcesUtil.getIdStringQuietly(
node.getContext(), node.getResources(), keyedTags.keyAt(i));
tags.put(id, keyedTags.valueAt(i));
}
}
}
}.run();
return tags.build();
}
private static InspectorValue fromDrawable(Drawable d) {
if (d instanceof ColorDrawable) {
return InspectorValue.mutable(Color, ((ColorDrawable) d).getColor());
}
return InspectorValue.mutable(Color, 0);
}
private static SonarObject getLayoutParams(View node) {
final LayoutParams layoutParams = node.getLayoutParams();
final SonarObject.Builder params = new SonarObject.Builder();
params.put("width", fromSize(layoutParams.width));
params.put("height", fromSize(layoutParams.height));
if (layoutParams instanceof MarginLayoutParams) {
final MarginLayoutParams marginLayoutParams = (MarginLayoutParams) layoutParams;
params.put(
"margin",
new SonarObject.Builder()
.put("left", InspectorValue.mutable(marginLayoutParams.leftMargin))
.put("top", InspectorValue.mutable(marginLayoutParams.topMargin))
.put("right", InspectorValue.mutable(marginLayoutParams.rightMargin))
.put("bottom", InspectorValue.mutable(marginLayoutParams.bottomMargin)));
}
if (layoutParams instanceof FrameLayout.LayoutParams) {
final FrameLayout.LayoutParams frameLayoutParams = (FrameLayout.LayoutParams) layoutParams;
params.put("gravity", sGravityMapping.get(frameLayoutParams.gravity));
}
if (layoutParams instanceof LinearLayout.LayoutParams) {
final LinearLayout.LayoutParams linearLayoutParams = (LinearLayout.LayoutParams) layoutParams;
params
.put("weight", InspectorValue.mutable(linearLayoutParams.weight))
.put("gravity", sGravityMapping.get(linearLayoutParams.gravity));
}
return params.build();
}
private void setLayoutParams(View node, String[] path, SonarDynamic value) {
final LayoutParams params = node.getLayoutParams();
switch (path[0]) {
case "width":
params.width = toSize(value.asString());
break;
case "height":
params.height = toSize(value.asString());
break;
case "weight":
final LinearLayout.LayoutParams linearParams = (LinearLayout.LayoutParams) params;
linearParams.weight = value.asFloat();
break;
}
if (params instanceof MarginLayoutParams) {
final MarginLayoutParams marginParams = (MarginLayoutParams) params;
switch (path[0]) {
case "margin":
switch (path[1]) {
case "left":
marginParams.leftMargin = value.asInt();
break;
case "top":
marginParams.topMargin = value.asInt();
break;
case "right":
marginParams.rightMargin = value.asInt();
break;
case "bottom":
marginParams.bottomMargin = value.asInt();
break;
}
break;
}
}
if (params instanceof FrameLayout.LayoutParams) {
final FrameLayout.LayoutParams frameLayoutParams = (FrameLayout.LayoutParams) params;
switch (path[0]) {
case "gravity":
frameLayoutParams.gravity = sGravityMapping.get(value.asString());
break;
}
}
if (params instanceof LinearLayout.LayoutParams) {
final LinearLayout.LayoutParams linearParams = (LinearLayout.LayoutParams) params;
switch (path[0]) {
case "weight":
linearParams.weight = value.asFloat();
break;
case "gravity":
linearParams.gravity = sGravityMapping.get(value.asString());
break;
}
}
node.setLayoutParams(params);
}
private static InspectorValue fromSize(int size) {
switch (size) {
case LayoutParams.WRAP_CONTENT:
return InspectorValue.mutable(Enum, "WRAP_CONTENT");
case LayoutParams.MATCH_PARENT:
return InspectorValue.mutable(Enum, "MATCH_PARENT");
default:
return InspectorValue.mutable(Enum, Integer.toString(size));
}
}
private static int toSize(String size) {
switch (size) {
case "WRAP_CONTENT":
return LayoutParams.WRAP_CONTENT;
case "MATCH_PARENT":
return LayoutParams.MATCH_PARENT;
default:
return Integer.parseInt(size);
}
}
private static final EnumMapping sVisibilityMapping =
new EnumMapping("VISIBLE") {
{
put("VISIBLE", View.VISIBLE);
put("INVISIBLE", View.INVISIBLE);
put("GONE", View.GONE);
}
};
private static final EnumMapping sLayoutDirectionMapping =
new EnumMapping("LAYOUT_DIRECTION_INHERIT") {
{
put("LAYOUT_DIRECTION_INHERIT", View.LAYOUT_DIRECTION_INHERIT);
put("LAYOUT_DIRECTION_LOCALE", View.LAYOUT_DIRECTION_LOCALE);
put("LAYOUT_DIRECTION_LTR", View.LAYOUT_DIRECTION_LTR);
put("LAYOUT_DIRECTION_RTL", View.LAYOUT_DIRECTION_RTL);
}
};
private static final EnumMapping sTextDirectionMapping =
new EnumMapping("TEXT_DIRECTION_INHERIT") {
{
put("TEXT_DIRECTION_INHERIT", View.TEXT_DIRECTION_INHERIT);
put("TEXT_DIRECTION_FIRST_STRONG", View.TEXT_DIRECTION_FIRST_STRONG);
put("TEXT_DIRECTION_ANY_RTL", View.TEXT_DIRECTION_ANY_RTL);
put("TEXT_DIRECTION_LTR", View.TEXT_DIRECTION_LTR);
put("TEXT_DIRECTION_RTL", View.TEXT_DIRECTION_RTL);
put("TEXT_DIRECTION_LOCALE", View.TEXT_DIRECTION_LOCALE);
put("TEXT_DIRECTION_FIRST_STRONG_LTR", View.TEXT_DIRECTION_FIRST_STRONG_LTR);
put("TEXT_DIRECTION_FIRST_STRONG_RTL", View.TEXT_DIRECTION_FIRST_STRONG_RTL);
}
};
private static final EnumMapping sTextAlignmentMapping =
new EnumMapping("TEXT_ALIGNMENT_INHERIT") {
{
put("TEXT_ALIGNMENT_INHERIT", View.TEXT_ALIGNMENT_INHERIT);
put("TEXT_ALIGNMENT_GRAVITY", View.TEXT_ALIGNMENT_GRAVITY);
put("TEXT_ALIGNMENT_TEXT_START", View.TEXT_ALIGNMENT_TEXT_START);
put("TEXT_ALIGNMENT_TEXT_END", View.TEXT_ALIGNMENT_TEXT_END);
put("TEXT_ALIGNMENT_CENTER", View.TEXT_ALIGNMENT_CENTER);
put("TEXT_ALIGNMENT_VIEW_START", View.TEXT_ALIGNMENT_VIEW_START);
put("TEXT_ALIGNMENT_VIEW_END", View.TEXT_ALIGNMENT_VIEW_END);
}
};
private static final EnumMapping sGravityMapping =
new EnumMapping("NO_GRAVITY") {
{
put("NO_GRAVITY", Gravity.NO_GRAVITY);
put("LEFT", Gravity.LEFT);
put("TOP", Gravity.TOP);
put("RIGHT", Gravity.RIGHT);
put("BOTTOM", Gravity.BOTTOM);
put("CENTER", Gravity.CENTER);
put("CENTER_VERTICAL", Gravity.CENTER_VERTICAL);
put("FILL_VERTICAL", Gravity.FILL_VERTICAL);
put("CENTER_HORIZONTAL", Gravity.CENTER_HORIZONTAL);
put("FILL_HORIZONTAL", Gravity.FILL_HORIZONTAL);
}
};
}