Files
flipper/android/plugins/inspector/InspectorSonarPlugin.java
Daniel Büchele fbbf8cf16b 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>
2018-06-01 11:03:58 +01:00

419 lines
13 KiB
Java

/*
* 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;
import android.app.Application;
import android.content.Context;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import com.facebook.sonar.core.ErrorReportingRunnable;
import com.facebook.sonar.core.SonarArray;
import com.facebook.sonar.core.SonarConnection;
import com.facebook.sonar.core.SonarDynamic;
import com.facebook.sonar.core.SonarObject;
import com.facebook.sonar.core.SonarPlugin;
import com.facebook.sonar.core.SonarReceiver;
import com.facebook.sonar.core.SonarResponder;
import com.facebook.sonar.plugins.common.MainThreadSonarReceiver;
import com.facebook.sonar.plugins.console.iface.ConsoleCommandReceiver;
import com.facebook.sonar.plugins.console.iface.NullScriptingEnvironment;
import com.facebook.sonar.plugins.console.iface.ScriptingEnvironment;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Nullable;
public class InspectorSonarPlugin implements SonarPlugin {
private ApplicationWrapper mApplication;
private DescriptorMapping mDescriptorMapping;
private ObjectTracker mObjectTracker;
private ScriptingEnvironment mScriptingEnvironment;
private String mHighlightedId;
private TouchOverlayView mTouchOverlay;
private SonarConnection mConnection;
public InspectorSonarPlugin(
Context context,
DescriptorMapping descriptorMapping,
ScriptingEnvironment scriptingEnvironment) {
this(
new ApplicationWrapper((Application) context.getApplicationContext()),
descriptorMapping,
scriptingEnvironment);
}
public InspectorSonarPlugin(Context context, DescriptorMapping descriptorMapping) {
this(context, descriptorMapping, new NullScriptingEnvironment());
}
// Package visible for testing
InspectorSonarPlugin(
ApplicationWrapper wrapper,
DescriptorMapping descriptorMapping,
ScriptingEnvironment scriptingEnvironment) {
mDescriptorMapping = descriptorMapping;
mObjectTracker = new ObjectTracker();
mApplication = wrapper;
mScriptingEnvironment = scriptingEnvironment;
}
@Override
public String getId() {
return "Inspector";
}
@Override
public void onConnect(SonarConnection connection) throws Exception {
mConnection = connection;
mDescriptorMapping.onConnect(connection);
ConsoleCommandReceiver.listenForCommands(
connection,
mScriptingEnvironment,
new ConsoleCommandReceiver.ContextProvider() {
@Override
@Nullable
public Object getObjectForId(String id) {
return mObjectTracker.get(id);
}
});
connection.receive("getRoot", mGetRoot);
connection.receive("getNodes", mGetNodes);
connection.receive("setData", mSetData);
connection.receive("setHighlighted", mSetHighlighted);
connection.receive("setSearchActive", mSetSearchActive);
connection.receive("getSearchResults", mGetSearchResults);
}
@Override
public void onDisconnect() throws Exception {
if (mHighlightedId != null) {
setHighlighted(mHighlightedId, false);
mHighlightedId = null;
}
mObjectTracker.clear();
mDescriptorMapping.onDisconnect();
mConnection = null;
}
final SonarReceiver mGetRoot =
new MainThreadSonarReceiver(mConnection) {
@Override
public void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
throws Exception {
responder.success(getNode(trackObject(mApplication)));
}
};
final SonarReceiver mGetNodes =
new MainThreadSonarReceiver(mConnection) {
@Override
public void onReceiveOnMainThread(final SonarObject params, final SonarResponder responder)
throws Exception {
final SonarArray ids = params.getArray("ids");
final SonarArray.Builder result = new SonarArray.Builder();
for (int i = 0, count = ids.length(); i < count; i++) {
final String id = ids.getString(i);
final SonarObject node = getNode(id);
if (node != null) {
result.put(node);
} else {
responder.error(
new SonarObject.Builder()
.put("message", "No node with given id")
.put("id", id)
.build());
return;
}
}
responder.success(new SonarObject.Builder().put("elements", result).build());
}
};
final SonarReceiver mSetData =
new MainThreadSonarReceiver(mConnection) {
@Override
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
throws Exception {
final String nodeId = params.getString("id");
final SonarArray keyPath = params.getArray("path");
final SonarDynamic value = params.getDynamic("value");
final Object obj = mObjectTracker.get(nodeId);
if (obj == null) {
return;
}
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
if (descriptor == null) {
return;
}
final int count = keyPath.length();
final String[] path = new String[count];
for (int i = 0; i < count; i++) {
path[i] = keyPath.getString(i);
}
descriptor.setValue(obj, path, value);
}
};
final SonarReceiver mSetHighlighted =
new MainThreadSonarReceiver(mConnection) {
@Override
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
throws Exception {
final String nodeId = params.getString("id");
if (mHighlightedId != null) {
setHighlighted(mHighlightedId, false);
}
if (nodeId != null) {
setHighlighted(nodeId, true);
}
mHighlightedId = nodeId;
}
};
final SonarReceiver mSetSearchActive =
new MainThreadSonarReceiver(mConnection) {
@Override
public void onReceiveOnMainThread(final SonarObject params, SonarResponder responder)
throws Exception {
final boolean active = params.getBoolean("active");
final List<View> roots = mApplication.getViewRoots();
ViewGroup root = null;
for (int i = roots.size() - 1; i >= 0; i--) {
if (roots.get(i) instanceof ViewGroup) {
root = (ViewGroup) roots.get(i);
break;
}
}
if (root != null) {
if (active) {
mTouchOverlay = new TouchOverlayView(root.getContext());
root.addView(mTouchOverlay);
root.bringChildToFront(mTouchOverlay);
} else {
root.removeView(mTouchOverlay);
mTouchOverlay = null;
}
}
}
};
final SonarReceiver mGetSearchResults =
new MainThreadSonarReceiver(mConnection) {
@Override
public void onReceiveOnMainThread(SonarObject params, SonarResponder responder)
throws Exception {
final String query = params.getString("query");
final SearchResultNode matchTree = searchTree(query.toLowerCase(), mApplication);
final SonarObject results = matchTree == null ? null : matchTree.toSonarObject();
final SonarObject response =
new SonarObject.Builder().put("results", results).put("query", query).build();
responder.success(response);
}
};
class TouchOverlayView extends View implements HiddenNode {
public TouchOverlayView(Context context) {
super(context);
setBackgroundColor(BoundsDrawable.COLOR_HIGHLIGHT_CONTENT);
}
@Override
public boolean onTouchEvent(final MotionEvent event) {
if (event.getAction() != MotionEvent.ACTION_UP) {
return true;
}
new ErrorReportingRunnable(mConnection) {
@Override
public void runOrThrow() throws Exception {
hitTest((int) event.getX(), (int) event.getY());
}
}.run();
return true;
}
}
void hitTest(final int touchX, final int touchY) throws Exception {
final SonarArray.Builder path = new SonarArray.Builder();
path.put(trackObject(mApplication));
final Touch touch =
new Touch() {
int x = touchX;
int y = touchY;
Object node = mApplication;
@Override
public void finish() {
mConnection.send("select", new SonarObject.Builder().put("path", path).build());
}
@Override
public void continueWithOffset(
final int childIndex, final int offsetX, final int offsetY) {
final Touch touch = this;
new ErrorReportingRunnable(mConnection) {
@Override
protected void runOrThrow() throws Exception {
x -= offsetX;
y -= offsetY;
node = assertNotNull(descriptorForObject(node).getChildAt(node, childIndex));
path.put(trackObject(node));
final NodeDescriptor<Object> descriptor = descriptorForObject(node);
descriptor.hitTest(node, touch);
}
}.run();
}
@Override
public boolean containedIn(int l, int t, int r, int b) {
return x >= l && x <= r && y >= t && y <= b;
}
};
final NodeDescriptor<Object> descriptor = descriptorForObject(mApplication);
descriptor.hitTest(mApplication, touch);
}
private void setHighlighted(final String id, final boolean highlighted) throws Exception {
final Object obj = mObjectTracker.get(id);
if (obj == null) {
return;
}
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
if (descriptor == null) {
return;
}
descriptor.setHighlighted(obj, highlighted);
}
public SearchResultNode searchTree(String query, Object obj) throws Exception {
final NodeDescriptor descriptor = descriptorForObject(obj);
List<SearchResultNode> childTrees = null;
boolean isMatch = descriptor.matches(query, obj);
for (int i = 0; i < descriptor.getChildCount(obj); i++) {
Object child = descriptor.getChildAt(obj, i);
SearchResultNode childNode = searchTree(query, child);
if (childNode != null) {
if (childTrees == null) {
childTrees = new ArrayList<>();
}
childTrees.add(childNode);
}
}
if (isMatch || childTrees != null) {
final String id = trackObject(obj);
return new SearchResultNode(id, isMatch, getNode(id), childTrees);
}
return null;
}
private @Nullable SonarObject getNode(String id) throws Exception {
final Object obj = mObjectTracker.get(id);
if (obj == null) {
return null;
}
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
if (descriptor == null) {
return null;
}
final SonarArray.Builder children = new SonarArray.Builder();
new ErrorReportingRunnable(mConnection) {
@Override
protected void runOrThrow() throws Exception {
for (int i = 0, count = descriptor.getChildCount(obj); i < count; i++) {
final Object child = assertNotNull(descriptor.getChildAt(obj, i));
children.put(trackObject(child));
}
}
}.run();
final SonarObject.Builder data = new SonarObject.Builder();
new ErrorReportingRunnable(mConnection) {
@Override
protected void runOrThrow() throws Exception {
for (Named<SonarObject> props : descriptor.getData(obj)) {
data.put(props.getName(), props.getValue());
}
}
}.run();
final SonarArray.Builder attributes = new SonarArray.Builder();
new ErrorReportingRunnable(mConnection) {
@Override
protected void runOrThrow() throws Exception {
for (Named<String> attribute : descriptor.getAttributes(obj)) {
attributes.put(
new SonarObject.Builder()
.put("name", attribute.getName())
.put("value", attribute.getValue())
.build());
}
}
}.run();
return new SonarObject.Builder()
.put("id", descriptor.getId(obj))
.put("name", descriptor.getName(obj))
.put("data", data)
.put("children", children)
.put("attributes", attributes)
.put("decoration", descriptor.getDecoration(obj))
.build();
}
private String trackObject(Object obj) throws Exception {
final NodeDescriptor<Object> descriptor = descriptorForObject(obj);
final String id = descriptor.getId(obj);
final Object curr = mObjectTracker.get(id);
if (obj != curr) {
mObjectTracker.put(id, obj);
descriptor.init(obj);
}
return id;
}
private NodeDescriptor<Object> descriptorForObject(Object obj) {
final Class c = assertNotNull(obj).getClass();
return (NodeDescriptor<Object>) mDescriptorMapping.descriptorForClass(c);
}
private static Object assertNotNull(@Nullable Object o) {
if (o == null) {
throw new AssertionError("Unexpected null value");
}
return o;
}
}