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>
419 lines
13 KiB
Java
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;
|
|
}
|
|
}
|