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:
129
android/plugins/inspector/descriptors/ActivityDescriptor.java
Normal file
129
android/plugins/inspector/descriptors/ActivityDescriptor.java
Normal file
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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 android.app.Activity;
|
||||
import android.view.Window;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.stetho.common.android.FragmentActivityAccessor;
|
||||
import com.facebook.stetho.common.android.FragmentCompat;
|
||||
import com.facebook.stetho.common.android.FragmentManagerAccessor;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ActivityDescriptor extends NodeDescriptor<Activity> {
|
||||
|
||||
@Override
|
||||
public void init(Activity node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Activity node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Activity node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Activity node) {
|
||||
return (node.getWindow() != null ? 1 : 0)
|
||||
+ getDialogFragments(FragmentCompat.getSupportLibInstance(), node).size()
|
||||
+ getDialogFragments(FragmentCompat.getFrameworkInstance(), node).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(Activity node, int index) {
|
||||
if (node.getWindow() != null) {
|
||||
if (index == 0) {
|
||||
return node.getWindow();
|
||||
} else {
|
||||
index--;
|
||||
}
|
||||
}
|
||||
|
||||
final List dialogs = getDialogFragments(FragmentCompat.getSupportLibInstance(), node);
|
||||
if (index < dialogs.size()) {
|
||||
return dialogs.get(index);
|
||||
} else {
|
||||
final List supportDialogs = getDialogFragments(FragmentCompat.getFrameworkInstance(), node);
|
||||
return supportDialogs.get(index - dialogs.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Activity node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Activity node, String[] path, SonarDynamic value) throws Exception {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Activity node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Activity node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Window.class);
|
||||
descriptor.setHighlighted(node.getWindow(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Activity node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Activity obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Activity node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
|
||||
private static List<Object> getDialogFragments(FragmentCompat compat, Activity activity) {
|
||||
if (compat == null || !compat.getFragmentActivityClass().isInstance(activity)) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
FragmentActivityAccessor activityAccessor = compat.forFragmentActivity();
|
||||
Object fragmentManager = activityAccessor.getFragmentManager(activity);
|
||||
if (fragmentManager == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
FragmentManagerAccessor fragmentManagerAccessor = compat.forFragmentManager();
|
||||
List<Object> addedFragments = fragmentManagerAccessor.getAddedFragments(fragmentManager);
|
||||
if (addedFragments == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
final List<Object> dialogFragments = new ArrayList<>();
|
||||
for (int i = 0, N = addedFragments.size(); i < N; ++i) {
|
||||
final Object fragment = addedFragments.get(i);
|
||||
if (compat.getDialogFragmentClass().isInstance(fragment)) {
|
||||
dialogFragments.add(fragment);
|
||||
}
|
||||
}
|
||||
|
||||
return dialogFragments;
|
||||
}
|
||||
}
|
||||
161
android/plugins/inspector/descriptors/ApplicationDescriptor.java
Normal file
161
android/plugins/inspector/descriptors/ApplicationDescriptor.java
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* 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 android.app.Activity;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.ApplicationWrapper;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ApplicationDescriptor extends NodeDescriptor<ApplicationWrapper> {
|
||||
|
||||
private class NodeKey {
|
||||
private int[] mKey;
|
||||
|
||||
boolean set(ApplicationWrapper node) {
|
||||
final List<View> roots = node.getViewRoots();
|
||||
final int childCount = roots.size();
|
||||
final int[] key = new int[childCount];
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = roots.get(i);
|
||||
key[i] = System.identityHashCode(child);
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
if (mKey == null) {
|
||||
changed = true;
|
||||
} else if (mKey.length != key.length) {
|
||||
changed = true;
|
||||
} else {
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
if (mKey[i] != key[i]) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mKey = key;
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final ApplicationWrapper node) {
|
||||
node.setListener(
|
||||
new ApplicationWrapper.ActivityStackChangedListener() {
|
||||
@Override
|
||||
public void onActivityStackChanged(List<Activity> stack) {
|
||||
invalidate(node);
|
||||
}
|
||||
});
|
||||
|
||||
final NodeKey key = new NodeKey();
|
||||
final Runnable maybeInvalidate =
|
||||
new NodeDescriptor.ErrorReportingRunnable() {
|
||||
@Override
|
||||
public void runOrThrow() throws Exception {
|
||||
if (connected()) {
|
||||
if (key.set(node)) {
|
||||
invalidate(node);
|
||||
}
|
||||
node.postDelayed(this, 1000);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
node.postDelayed(maybeInvalidate, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(ApplicationWrapper node) {
|
||||
return node.getApplication().getPackageName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(ApplicationWrapper node) {
|
||||
return node.getApplication().getPackageName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(ApplicationWrapper node) {
|
||||
return node.getViewRoots().size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(ApplicationWrapper node, int index) {
|
||||
final View view = node.getViewRoots().get(index);
|
||||
|
||||
for (Activity activity : node.getActivityStack()) {
|
||||
if (activity.getWindow().getDecorView() == view) {
|
||||
return activity;
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(ApplicationWrapper node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(ApplicationWrapper node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(ApplicationWrapper node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(ApplicationWrapper node, boolean selected) throws Exception {
|
||||
final int childCount = getChildCount(node);
|
||||
if (childCount > 0) {
|
||||
final Object topChild = getChildAt(node, childCount - 1);
|
||||
final NodeDescriptor descriptor = descriptorForClass(topChild.getClass());
|
||||
descriptor.setHighlighted(topChild, selected);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(ApplicationWrapper node, Touch touch) {
|
||||
final int childCount = getChildCount(node);
|
||||
|
||||
for (int i = childCount - 1; i >= 0; i--) {
|
||||
final Object child = getChildAt(node, i);
|
||||
if (child instanceof Activity || child instanceof ViewGroup) {
|
||||
touch.continueWithOffset(i, 0, 0);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(ApplicationWrapper obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, ApplicationWrapper node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
81
android/plugins/inspector/descriptors/DialogDescriptor.java
Normal file
81
android/plugins/inspector/descriptors/DialogDescriptor.java
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 android.app.Dialog;
|
||||
import android.view.Window;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DialogDescriptor extends NodeDescriptor<Dialog> {
|
||||
|
||||
@Override
|
||||
public void init(Dialog node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Dialog node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Dialog node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Dialog node) {
|
||||
return node.getWindow() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(Dialog node, int index) {
|
||||
return node.getWindow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Dialog node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Dialog node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Dialog node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Dialog node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Window.class);
|
||||
descriptor.setHighlighted(node.getWindow(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Dialog node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Dialog obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Dialog node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 android.app.Dialog;
|
||||
import android.app.DialogFragment;
|
||||
import android.app.Fragment;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DialogFragmentDescriptor extends NodeDescriptor<DialogFragment> {
|
||||
|
||||
@Override
|
||||
public void init(DialogFragment node) {}
|
||||
|
||||
@Override
|
||||
public String getId(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(DialogFragment node) {
|
||||
return node.getDialog() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(DialogFragment node, int index) {
|
||||
return node.getDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getData(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(DialogFragment node, String[] path, SonarDynamic value) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(DialogFragment node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Dialog.class);
|
||||
if (node.getDialog() != null) {
|
||||
descriptor.setHighlighted(node.getDialog(), selected);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(DialogFragment node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(DialogFragment obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, DialogFragment node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
146
android/plugins/inspector/descriptors/DrawableDescriptor.java
Normal file
146
android/plugins/inspector/descriptors/DrawableDescriptor.java
Normal file
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* 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 android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
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 java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class DrawableDescriptor extends NodeDescriptor<Drawable> {
|
||||
|
||||
@Override
|
||||
public void init(Drawable node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Drawable node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Drawable node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Drawable node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(Drawable node, int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Drawable node) {
|
||||
final SonarObject.Builder props = new SonarObject.Builder();
|
||||
final Rect bounds = node.getBounds();
|
||||
|
||||
props.put("left", InspectorValue.mutable(bounds.left));
|
||||
props.put("top", InspectorValue.mutable(bounds.top));
|
||||
props.put("right", InspectorValue.mutable(bounds.right));
|
||||
props.put("bottom", InspectorValue.mutable(bounds.bottom));
|
||||
|
||||
if (hasAlphaSupport()) {
|
||||
props.put("alpha", InspectorValue.mutable(node.getAlpha()));
|
||||
}
|
||||
|
||||
return Arrays.asList(new Named<>("Drawable", props.build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Drawable node, String[] path, SonarDynamic value) {
|
||||
final Rect bounds = node.getBounds();
|
||||
|
||||
switch (path[0]) {
|
||||
case "Drawable":
|
||||
switch (path[1]) {
|
||||
case "left":
|
||||
bounds.left = value.asInt();
|
||||
node.setBounds(bounds);
|
||||
break;
|
||||
case "top":
|
||||
bounds.top = value.asInt();
|
||||
node.setBounds(bounds);
|
||||
break;
|
||||
case "right":
|
||||
bounds.right = value.asInt();
|
||||
node.setBounds(bounds);
|
||||
break;
|
||||
case "bottom":
|
||||
bounds.bottom = value.asInt();
|
||||
node.setBounds(bounds);
|
||||
break;
|
||||
case "alpha":
|
||||
node.setAlpha(value.asInt());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean hasAlphaSupport() {
|
||||
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Drawable node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Drawable node, boolean selected) {
|
||||
// Ensure we handle wrapping drawable
|
||||
Drawable.Callback callbacks = node.getCallback();
|
||||
while (callbacks instanceof Drawable) {
|
||||
callbacks = ((Drawable) callbacks).getCallback();
|
||||
}
|
||||
|
||||
if (!(callbacks instanceof View)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final View callbackView = (View) callbacks;
|
||||
if (selected) {
|
||||
final Rect zero = new Rect();
|
||||
final Rect bounds = node.getBounds();
|
||||
HighlightedOverlay.setHighlighted(callbackView, zero, zero, bounds);
|
||||
} else {
|
||||
HighlightedOverlay.removeHighlight(callbackView);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Drawable node, Touch touch) {
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Drawable obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Drawable node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
124
android/plugins/inspector/descriptors/FragmentDescriptor.java
Normal file
124
android/plugins/inspector/descriptors/FragmentDescriptor.java
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
* 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 android.app.Fragment;
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.stetho.common.android.ResourcesUtil;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class FragmentDescriptor extends NodeDescriptor<Fragment> {
|
||||
|
||||
@Override
|
||||
public void init(Fragment node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Fragment node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Fragment node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Fragment node) {
|
||||
return node.getView() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(Fragment node, int index) {
|
||||
return node.getView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Fragment node) {
|
||||
final Bundle args = node.getArguments();
|
||||
if (args == null || args.isEmpty()) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
final SonarObject.Builder bundle = new SonarObject.Builder();
|
||||
|
||||
for (String key : args.keySet()) {
|
||||
bundle.put(key, args.get(key));
|
||||
}
|
||||
|
||||
return Arrays.asList(new Named<>("Arguments", bundle.build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Fragment node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Fragment node) {
|
||||
final String resourceId = getResourceId(node);
|
||||
|
||||
if (resourceId == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
return Arrays.asList(new Named<>("id", resourceId));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getResourceId(Fragment node) {
|
||||
final int id = node.getId();
|
||||
|
||||
if (id == View.NO_ID || node.getHost() == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Fragment node, boolean selected) throws Exception {
|
||||
if (node.getView() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node.getView(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Fragment node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Fragment obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Fragment node) throws Exception {
|
||||
final String resourceId = getResourceId(node);
|
||||
|
||||
if (resourceId != null) {
|
||||
if (resourceId.toLowerCase().contains(query)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
final NodeDescriptor objectDescriptor = descriptorForClass(Object.class);
|
||||
return objectDescriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
85
android/plugins/inspector/descriptors/ObjectDescriptor.java
Normal file
85
android/plugins/inspector/descriptors/ObjectDescriptor.java
Normal file
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* 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 com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ObjectDescriptor extends NodeDescriptor<Object> {
|
||||
|
||||
@Override
|
||||
public void init(Object node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Object node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Object node) {
|
||||
return node.getClass().getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Object node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(Object node, int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Object node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Object node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Object node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Object node, boolean selected) {}
|
||||
|
||||
@Override
|
||||
public void hitTest(Object node, Touch touch) {
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Object obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Object node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(node.getClass());
|
||||
final List<Named<String>> attributes = descriptor.getAttributes(node);
|
||||
for (Named<String> namedString : attributes) {
|
||||
if (namedString.getName().equals("id")) {
|
||||
if (namedString.getValue().toLowerCase().contains(query)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return descriptor.getName(node).toLowerCase().contains(query);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* 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 android.app.Dialog;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.support.v4.app.Fragment;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class SupportDialogFragmentDescriptor extends NodeDescriptor<DialogFragment> {
|
||||
|
||||
@Override
|
||||
public void init(DialogFragment node) {}
|
||||
|
||||
@Override
|
||||
public String getId(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(DialogFragment node) {
|
||||
return node.getDialog() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(DialogFragment node, int index) {
|
||||
return node.getDialog();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getData(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(DialogFragment node, String[] path, SonarDynamic value) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Fragment.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(DialogFragment node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Dialog.class);
|
||||
if (node.getDialog() != null) {
|
||||
descriptor.setHighlighted(node.getDialog(), selected);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(DialogFragment node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(DialogFragment obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, DialogFragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* 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 android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.View;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import com.facebook.stetho.common.android.ResourcesUtil;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class SupportFragmentDescriptor extends NodeDescriptor<Fragment> {
|
||||
|
||||
@Override
|
||||
public void init(Fragment node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Fragment node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Fragment node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Fragment node) {
|
||||
return node.getView() == null ? 0 : 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(Fragment node, int index) {
|
||||
return node.getView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Fragment node) {
|
||||
final Bundle args = node.getArguments();
|
||||
if (args == null || args.isEmpty()) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
final SonarObject.Builder bundle = new SonarObject.Builder();
|
||||
|
||||
for (String key : args.keySet()) {
|
||||
bundle.put(key, args.get(key));
|
||||
}
|
||||
|
||||
return Arrays.asList(new Named<>("Arguments", bundle.build()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Fragment node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Fragment node) {
|
||||
final int id = node.getId();
|
||||
if (id == View.NO_ID || node.getHost() == null) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
return Arrays.asList(
|
||||
new Named<>(
|
||||
"id", ResourcesUtil.getIdStringQuietly(node.getContext(), node.getResources(), id)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Fragment node, boolean selected) throws Exception {
|
||||
if (node.getView() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node.getView(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Fragment node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Fragment obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Fragment node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
132
android/plugins/inspector/descriptors/TextViewDescriptor.java
Normal file
132
android/plugins/inspector/descriptors/TextViewDescriptor.java
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* 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.Number;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Text;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class TextViewDescriptor extends NodeDescriptor<TextView> {
|
||||
|
||||
@Override
|
||||
public void init(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.init(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(TextView node) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(TextView node, int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(TextView node) throws Exception {
|
||||
final List<Named<SonarObject>> props = new ArrayList<>();
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
|
||||
props.add(
|
||||
0,
|
||||
new Named<>(
|
||||
"TextView",
|
||||
new SonarObject.Builder()
|
||||
.put("text", InspectorValue.mutable(Text, node.getText().toString()))
|
||||
.put(
|
||||
"textColor",
|
||||
InspectorValue.mutable(Color, node.getTextColors().getDefaultColor()))
|
||||
.put("textSize", InspectorValue.mutable(Number, node.getTextSize()))
|
||||
.build()));
|
||||
|
||||
props.addAll(descriptor.getData(node));
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(TextView node, String[] path, SonarDynamic value) throws Exception {
|
||||
switch (path[0]) {
|
||||
case "TextView":
|
||||
switch (path[1]) {
|
||||
case "text":
|
||||
node.setText(value.asString());
|
||||
break;
|
||||
case "textColor":
|
||||
node.setTextColor(value.asInt());
|
||||
break;
|
||||
case "textSize":
|
||||
node.setTextSize(value.asInt());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
break;
|
||||
}
|
||||
invalidate(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(TextView node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node, selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(TextView node, Touch touch) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.hitTest(node, touch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getDecoration(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, TextView node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
708
android/plugins/inspector/descriptors/ViewDescriptor.java
Normal file
708
android/plugins/inspector/descriptors/ViewDescriptor.java
Normal 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);
|
||||
}
|
||||
};
|
||||
}
|
||||
273
android/plugins/inspector/descriptors/ViewGroupDescriptor.java
Normal file
273
android/plugins/inspector/descriptors/ViewGroupDescriptor.java
Normal file
@@ -0,0 +1,273 @@
|
||||
/*
|
||||
* 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 android.support.v4.view.ViewGroupCompat.LAYOUT_MODE_CLIP_BOUNDS;
|
||||
import static android.support.v4.view.ViewGroupCompat.LAYOUT_MODE_OPTICAL_BOUNDS;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Boolean;
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import com.facebook.sonar.R;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.HiddenNode;
|
||||
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.stetho.common.android.FragmentCompatUtil;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class ViewGroupDescriptor extends NodeDescriptor<ViewGroup> {
|
||||
|
||||
private class NodeKey {
|
||||
private int[] mKey;
|
||||
|
||||
boolean set(ViewGroup node) {
|
||||
final int childCount = node.getChildCount();
|
||||
final int[] key = new int[childCount];
|
||||
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
final View child = node.getChildAt(i);
|
||||
key[i] = System.identityHashCode(child);
|
||||
}
|
||||
|
||||
boolean changed = false;
|
||||
if (mKey == null) {
|
||||
changed = true;
|
||||
} else if (mKey.length != key.length) {
|
||||
changed = true;
|
||||
} else {
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
if (mKey[i] != key[i]) {
|
||||
changed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mKey = key;
|
||||
return changed;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void init(final ViewGroup node) {
|
||||
final NodeKey key = new NodeKey();
|
||||
|
||||
final Runnable maybeInvalidate =
|
||||
new ErrorReportingRunnable() {
|
||||
@Override
|
||||
public void runOrThrow() throws Exception {
|
||||
if (connected()) {
|
||||
if (key.set(node)) {
|
||||
invalidate(node);
|
||||
}
|
||||
|
||||
final boolean hasAttachedToWindow =
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
|
||||
if (!hasAttachedToWindow || node.isAttachedToWindow()) {
|
||||
node.postDelayed(this, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
node.postDelayed(maybeInvalidate, 1000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getId(ViewGroup node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getId(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(ViewGroup node) throws Exception {
|
||||
NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getName(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(ViewGroup node) {
|
||||
int childCount = 0;
|
||||
for (int i = 0, count = node.getChildCount(); i < count; i++) {
|
||||
if (!(node.getChildAt(i) instanceof HiddenNode)) {
|
||||
childCount++;
|
||||
}
|
||||
}
|
||||
return childCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object getChildAt(ViewGroup node, int index) {
|
||||
for (int i = 0, count = node.getChildCount(); i < count; i++) {
|
||||
final View child = node.getChildAt(i);
|
||||
if (child instanceof HiddenNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i >= index) {
|
||||
final Object fragment = getAttachedFragmentForView(child);
|
||||
if (fragment != null && !FragmentCompatUtil.isDialogFragment(fragment)) {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(ViewGroup node) throws Exception {
|
||||
final List<Named<SonarObject>> props = new ArrayList<>();
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
|
||||
final SonarObject.Builder vgProps = new SonarObject.Builder();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) {
|
||||
vgProps
|
||||
.put(
|
||||
"layoutMode",
|
||||
InspectorValue.mutable(
|
||||
Enum,
|
||||
node.getLayoutMode() == LAYOUT_MODE_CLIP_BOUNDS
|
||||
? "LAYOUT_MODE_CLIP_BOUNDS"
|
||||
: "LAYOUT_MODE_OPTICAL_BOUNDS"))
|
||||
.put("clipChildren", InspectorValue.mutable(Boolean, node.getClipChildren()));
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
vgProps.put("clipToPadding", InspectorValue.mutable(Boolean, node.getClipToPadding()));
|
||||
}
|
||||
|
||||
props.add(0, new Named<>("ViewGroup", vgProps.build()));
|
||||
|
||||
props.addAll(descriptor.getData(node));
|
||||
|
||||
return props;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(ViewGroup node, String[] path, SonarDynamic value) throws Exception {
|
||||
switch (path[0]) {
|
||||
case "ViewGroup":
|
||||
switch (path[1]) {
|
||||
case "layoutMode":
|
||||
switch (value.asString()) {
|
||||
case "LAYOUT_MODE_CLIP_BOUNDS":
|
||||
node.setLayoutMode(LAYOUT_MODE_CLIP_BOUNDS);
|
||||
break;
|
||||
case "LAYOUT_MODE_OPTICAL_BOUNDS":
|
||||
node.setLayoutMode(LAYOUT_MODE_OPTICAL_BOUNDS);
|
||||
break;
|
||||
default:
|
||||
node.setLayoutMode(-1);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "clipChildren":
|
||||
node.setClipChildren(value.asBoolean());
|
||||
break;
|
||||
case "clipToPadding":
|
||||
node.setClipToPadding(value.asBoolean());
|
||||
break;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setValue(node, path, value);
|
||||
break;
|
||||
}
|
||||
invalidate(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(ViewGroup node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
return descriptor.getAttributes(node);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(ViewGroup node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node, selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(ViewGroup node, Touch touch) {
|
||||
for (int i = node.getChildCount() - 1; i >= 0; i--) {
|
||||
final View child = node.getChildAt(i);
|
||||
if (child instanceof HiddenNode
|
||||
|| child.getVisibility() != View.VISIBLE
|
||||
|| shouldSkip(child)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final int scrollX = node.getScrollX();
|
||||
final int scrollY = node.getScrollY();
|
||||
|
||||
final int left = (child.getLeft() + (int) child.getTranslationX()) - scrollX;
|
||||
final int top = (child.getTop() + (int) child.getTranslationY()) - scrollY;
|
||||
final int right = (child.getRight() + (int) child.getTranslationX()) - scrollX;
|
||||
final int bottom = (child.getBottom() + (int) child.getTranslationY()) - scrollY;
|
||||
|
||||
final boolean hit = touch.containedIn(left, top, right, bottom);
|
||||
|
||||
if (hit) {
|
||||
touch.continueWithOffset(i, left, top);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
touch.finish();
|
||||
}
|
||||
|
||||
private static boolean shouldSkip(View view) {
|
||||
Object tag = view.getTag(R.id.sonar_skip_view_traversal);
|
||||
if (!(tag instanceof Boolean)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (Boolean) tag;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(ViewGroup obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, ViewGroup node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
|
||||
private static Object getAttachedFragmentForView(View v) {
|
||||
try {
|
||||
final Object fragment = FragmentCompatUtil.findFragmentForView(v);
|
||||
boolean added = false;
|
||||
if (fragment instanceof android.app.Fragment) {
|
||||
added = ((android.app.Fragment) fragment).isAdded();
|
||||
} else if (fragment instanceof android.support.v4.app.Fragment) {
|
||||
added = ((android.support.v4.app.Fragment) fragment).isAdded();
|
||||
}
|
||||
|
||||
return added ? fragment : null;
|
||||
} catch (RuntimeException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
81
android/plugins/inspector/descriptors/WindowDescriptor.java
Normal file
81
android/plugins/inspector/descriptors/WindowDescriptor.java
Normal file
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* 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 android.view.View;
|
||||
import android.view.Window;
|
||||
import com.facebook.sonar.core.SonarDynamic;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import com.facebook.sonar.plugins.inspector.Named;
|
||||
import com.facebook.sonar.plugins.inspector.NodeDescriptor;
|
||||
import com.facebook.sonar.plugins.inspector.Touch;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class WindowDescriptor extends NodeDescriptor<Window> {
|
||||
|
||||
@Override
|
||||
public void init(Window node) {}
|
||||
|
||||
@Override
|
||||
public String getId(Window node) {
|
||||
return Integer.toString(System.identityHashCode(node));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName(Window node) {
|
||||
return node.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getChildCount(Window node) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getChildAt(Window node, int index) {
|
||||
return node.getDecorView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Named<SonarObject>> getData(Window node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setValue(Window node, String[] path, SonarDynamic value) {}
|
||||
|
||||
@Override
|
||||
public List<Named<String>> getAttributes(Window node) {
|
||||
return Collections.EMPTY_LIST;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHighlighted(Window node, boolean selected) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(View.class);
|
||||
descriptor.setHighlighted(node.getDecorView(), selected);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void hitTest(Window node, Touch touch) {
|
||||
touch.continueWithOffset(0, 0, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getDecoration(Window obj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean matches(String query, Window node) throws Exception {
|
||||
final NodeDescriptor descriptor = descriptorForClass(Object.class);
|
||||
return descriptor.matches(query, node);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
* 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.utils;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import com.facebook.sonar.plugins.inspector.descriptors.utils.AccessibilityRoleUtil.AccessibilityRole;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class provides utility methods for determining certain accessibility properties of {@link
|
||||
* View}s and {@link AccessibilityNodeInfoCompat}s. It is porting some of the checks from {@link
|
||||
* com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils}, but has stripped many features which
|
||||
* are unnecessary here.
|
||||
*/
|
||||
public class AccessibilityEvaluationUtil {
|
||||
|
||||
private AccessibilityEvaluationUtil() {}
|
||||
|
||||
/**
|
||||
* Returns whether the specified node has text or a content description.
|
||||
*
|
||||
* @param node The node to check.
|
||||
* @return {@code true} if the node has text.
|
||||
*/
|
||||
public static boolean hasText(@Nullable AccessibilityNodeInfoCompat node) {
|
||||
return node != null
|
||||
&& node.getCollectionInfo() == null
|
||||
&& (!TextUtils.isEmpty(node.getText()) || !TextUtils.isEmpty(node.getContentDescription()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the supplied {@link View} and {@link AccessibilityNodeInfoCompat} would produce
|
||||
* spoken feedback if it were accessibility focused. NOTE: not all speaking nodes are focusable.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if it meets the criterion for producing spoken feedback
|
||||
*/
|
||||
public static boolean isSpeakingNode(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
if (node == null || view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int important = ViewCompat.getImportantForAccessibility(view);
|
||||
if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS
|
||||
|| (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO && node.getChildCount() <= 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return node.isCheckable() || hasText(node) || hasNonActionableSpeakingDescendants(node, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the supplied {@link View} and {@link AccessibilityNodeInfoCompat} has any
|
||||
* children which are not independently accessibility focusable and also have a spoken
|
||||
* description.
|
||||
*
|
||||
* <p>NOTE: Accessibility services will include these children's descriptions in the closest
|
||||
* focusable ancestor.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if it has any non-actionable speaking descendants within its subtree
|
||||
*/
|
||||
public static boolean hasNonActionableSpeakingDescendants(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
|
||||
if (node == null || view == null || !(view instanceof ViewGroup)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ViewGroup viewGroup = (ViewGroup) view;
|
||||
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
|
||||
final View childView = viewGroup.getChildAt(i);
|
||||
|
||||
if (childView == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat childNode = AccessibilityNodeInfoCompat.obtain();
|
||||
try {
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(childView, childNode);
|
||||
|
||||
if (!node.isVisibleToUser()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isAccessibilityFocusable(childNode, childView)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isSpeakingNode(childNode, childView)) {
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
if (childNode != null) {
|
||||
childNode.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the provided {@link View} and {@link AccessibilityNodeInfoCompat} meet the
|
||||
* criteria for gaining accessibility focus.
|
||||
*
|
||||
* <p>Note: this is evaluating general focusability by accessibility services, and does not mean
|
||||
* this view will be guaranteed to be focused by specific services such as Talkback. For Talkback
|
||||
* focusability, see {@link #isTalkbackFocusable(View)}
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if it is possible to gain accessibility focus
|
||||
*/
|
||||
public static boolean isAccessibilityFocusable(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
if (node == null || view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Never focus invisible nodes.
|
||||
if (!node.isVisibleToUser()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Always focus "actionable" nodes.
|
||||
if (isActionableForAccessibility(node)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// only focus top-level list items with non-actionable speaking children.
|
||||
return isTopLevelScrollItem(node, view) && isSpeakingNode(node, view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether the provided {@link View} and {@link AccessibilityNodeInfoCompat} is a
|
||||
* top-level item in a scrollable container.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if it is a top-level item in a scrollable container.
|
||||
*/
|
||||
public static boolean isTopLevelScrollItem(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
if (node == null || view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final View parent = (View) ViewCompat.getParentForAccessibility(view);
|
||||
if (parent == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.isScrollable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final List actionList = node.getActionList();
|
||||
if (actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD)
|
||||
|| actionList.contains(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Top-level items in a scrolling pager are actually two levels down since the first
|
||||
// level items in pagers are the pages themselves.
|
||||
View grandparent = (View) ViewCompat.getParentForAccessibility(parent);
|
||||
if (grandparent != null
|
||||
&& AccessibilityRoleUtil.getRole(grandparent) == AccessibilityRole.PAGER) {
|
||||
return true;
|
||||
}
|
||||
|
||||
AccessibilityRole parentRole = AccessibilityRoleUtil.getRole(parent);
|
||||
return parentRole == AccessibilityRole.LIST
|
||||
|| parentRole == AccessibilityRole.GRID
|
||||
|| parentRole == AccessibilityRole.SCROLL_VIEW
|
||||
|| parentRole == AccessibilityRole.HORIZONTAL_SCROLL_VIEW;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a node is actionable. That is, the node supports one of {@link
|
||||
* AccessibilityNodeInfoCompat#isClickable()}, {@link AccessibilityNodeInfoCompat#isFocusable()},
|
||||
* or {@link AccessibilityNodeInfoCompat#isLongClickable()}.
|
||||
*
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if node is actionable.
|
||||
*/
|
||||
public static boolean isActionableForAccessibility(@Nullable AccessibilityNodeInfoCompat node) {
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.isClickable() || node.isLongClickable() || node.isFocusable()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final List actionList = node.getActionList();
|
||||
return actionList.contains(AccessibilityNodeInfoCompat.ACTION_CLICK)
|
||||
|| actionList.contains(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK)
|
||||
|| actionList.contains(AccessibilityNodeInfoCompat.ACTION_FOCUS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if any of the provided {@link View}'s and {@link AccessibilityNodeInfoCompat}'s
|
||||
* ancestors can receive accessibility focus
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param node The {@link AccessibilityNodeInfoCompat} to evaluate
|
||||
* @return {@code true} if an ancestor of may receive accessibility focus
|
||||
*/
|
||||
public static boolean hasFocusableAncestor(
|
||||
@Nullable AccessibilityNodeInfoCompat node, @Nullable View view) {
|
||||
if (node == null || view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final ViewParent parentView = ViewCompat.getParentForAccessibility(view);
|
||||
if (!(parentView instanceof View)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat parentNode = AccessibilityNodeInfoCompat.obtain();
|
||||
try {
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo((View) parentView, parentNode);
|
||||
if (parentNode == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hasEqualBoundsToViewRoot(parentNode, (View) parentView)
|
||||
&& parentNode.getChildCount() > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAccessibilityFocusable(parentNode, (View) parentView)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hasFocusableAncestor(parentNode, (View) parentView)) {
|
||||
return true;
|
||||
}
|
||||
} finally {
|
||||
parentNode.recycle();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a one given view is a descendant of another.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @param potentialAncestor The potential ancestor {@link View}
|
||||
* @return {@code true} if view is a descendant of potentialAncestor
|
||||
*/
|
||||
private static boolean viewIsDescendant(View view, View potentialAncestor) {
|
||||
ViewParent parent = view.getParent();
|
||||
while (parent != null) {
|
||||
if (parent == potentialAncestor) {
|
||||
return true;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a View has the same size and position as its View Root.
|
||||
*
|
||||
* @param view The {@link View} to evaluate
|
||||
* @return {@code true} if view has equal bounds
|
||||
*/
|
||||
public static boolean hasEqualBoundsToViewRoot(AccessibilityNodeInfoCompat node, View view) {
|
||||
AndroidRootResolver rootResolver = new AndroidRootResolver();
|
||||
List<AndroidRootResolver.Root> roots = rootResolver.listActiveRoots();
|
||||
if (roots != null) {
|
||||
for (AndroidRootResolver.Root root : roots) {
|
||||
if (view == root.view) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (viewIsDescendant(view, root.view)) {
|
||||
Rect nodeBounds = new Rect();
|
||||
node.getBoundsInScreen(nodeBounds);
|
||||
|
||||
Rect viewRootBounds = new Rect();
|
||||
viewRootBounds.set(
|
||||
root.param.x,
|
||||
root.param.y,
|
||||
root.param.width + root.param.x,
|
||||
root.param.height + root.param.y);
|
||||
|
||||
return nodeBounds.equals(viewRootBounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether a given {@link View} will be focusable by Google's TalkBack screen reader.
|
||||
*
|
||||
* @param view The {@link View} to evaluate.
|
||||
* @return {@code boolean} if the view will be ignored by TalkBack.
|
||||
*/
|
||||
public static boolean isTalkbackFocusable(View view) {
|
||||
if (view == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final int important = ViewCompat.getImportantForAccessibility(view);
|
||||
if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO
|
||||
|| important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Go all the way up the tree to make sure no parent has hidden its descendants
|
||||
ViewParent parent = view.getParent();
|
||||
while (parent instanceof View) {
|
||||
if (ViewCompat.getImportantForAccessibility((View) parent)
|
||||
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
return false;
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (node == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Non-leaf nodes identical in size to their View Root should not be focusable.
|
||||
if (hasEqualBoundsToViewRoot(node, view) && node.getChildCount() > 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
if (!node.isVisibleToUser()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAccessibilityFocusable(node, view)) {
|
||||
if (node.getChildCount() <= 0) {
|
||||
// Leaves that are accessibility focusable are never ignored, even if they don't have a
|
||||
// speakable description
|
||||
return true;
|
||||
} else if (isSpeakingNode(node, view)) {
|
||||
// Node is focusable and has something to speak
|
||||
return true;
|
||||
}
|
||||
|
||||
// Node is focusable and has nothing to speak
|
||||
return false;
|
||||
}
|
||||
|
||||
// if view is not accessibility focusable, it needs to have text and no focusable ancestors.
|
||||
if (!hasText(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!hasFocusableAncestor(node, view)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
} finally {
|
||||
node.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* 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.utils;
|
||||
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.view.View;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Utility class that handles the addition of a "role" for accessibility to either a View or
|
||||
* AccessibilityNodeInfo.
|
||||
*/
|
||||
public class AccessibilityRoleUtil {
|
||||
|
||||
/**
|
||||
* These roles are defined by Google's TalkBack screen reader, and this list should be kept up to
|
||||
* date with their implementation. Details can be seen in their source code here:
|
||||
*
|
||||
* <p>https://github.com/google/talkback/blob/master/utils/src/main/java/Role.java
|
||||
*/
|
||||
public enum AccessibilityRole {
|
||||
NONE(null),
|
||||
BUTTON("android.widget.Button"),
|
||||
CHECK_BOX("android.widget.CompoundButton"),
|
||||
DROP_DOWN_LIST("android.widget.Spinner"),
|
||||
EDIT_TEXT("android.widget.EditText"),
|
||||
GRID("android.widget.GridView"),
|
||||
IMAGE("android.widget.ImageView"),
|
||||
IMAGE_BUTTON("android.widget.ImageView"),
|
||||
LIST("android.widget.AbsListView"),
|
||||
PAGER("android.support.v4.view.ViewPager"),
|
||||
RADIO_BUTTON("android.widget.RadioButton"),
|
||||
SEEK_CONTROL("android.widget.SeekBar"),
|
||||
SWITCH("android.widget.Switch"),
|
||||
TAB_BAR("android.widget.TabWidget"),
|
||||
TOGGLE_BUTTON("android.widget.ToggleButton"),
|
||||
VIEW_GROUP("android.view.ViewGroup"),
|
||||
WEB_VIEW("android.webkit.WebView"),
|
||||
CHECKED_TEXT_VIEW("android.widget.CheckedTextView"),
|
||||
PROGRESS_BAR("android.widget.ProgressBar"),
|
||||
ACTION_BAR_TAB("android.app.ActionBar$Tab"),
|
||||
DRAWER_LAYOUT("android.support.v4.widget.DrawerLayout"),
|
||||
SLIDING_DRAWER("android.widget.SlidingDrawer"),
|
||||
ICON_MENU("com.android.internal.view.menu.IconMenuView"),
|
||||
TOAST("android.widget.Toast$TN"),
|
||||
DATE_PICKER_DIALOG("android.app.DatePickerDialog"),
|
||||
TIME_PICKER_DIALOG("android.app.TimePickerDialog"),
|
||||
DATE_PICKER("android.widget.DatePicker"),
|
||||
TIME_PICKER("android.widget.TimePicker"),
|
||||
NUMBER_PICKER("android.widget.NumberPicker"),
|
||||
SCROLL_VIEW("android.widget.ScrollView"),
|
||||
HORIZONTAL_SCROLL_VIEW("android.widget.HorizontalScrollView"),
|
||||
KEYBOARD_KEY("android.inputmethodservice.Keyboard$Key");
|
||||
|
||||
@Nullable private final String mValue;
|
||||
|
||||
AccessibilityRole(String type) {
|
||||
mValue = type;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getValue() {
|
||||
return mValue;
|
||||
}
|
||||
|
||||
public static AccessibilityRole fromValue(String value) {
|
||||
for (AccessibilityRole role : AccessibilityRole.values()) {
|
||||
if (role.getValue() != null && role.getValue().equals(value)) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
return AccessibilityRole.NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private AccessibilityRoleUtil() {
|
||||
// No instances
|
||||
}
|
||||
|
||||
public static AccessibilityRole getRole(View view) {
|
||||
AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(view, nodeInfo);
|
||||
AccessibilityRole role = getRole(nodeInfo);
|
||||
nodeInfo.recycle();
|
||||
return role;
|
||||
}
|
||||
|
||||
public static AccessibilityRole getRole(AccessibilityNodeInfo nodeInfo) {
|
||||
return getRole(new AccessibilityNodeInfoCompat(nodeInfo));
|
||||
}
|
||||
|
||||
public static AccessibilityRole getRole(AccessibilityNodeInfoCompat nodeInfo) {
|
||||
AccessibilityRole role = AccessibilityRole.fromValue((String) nodeInfo.getClassName());
|
||||
if (role.equals(AccessibilityRole.IMAGE_BUTTON) || role.equals(AccessibilityRole.IMAGE)) {
|
||||
return nodeInfo.isClickable() ? AccessibilityRole.IMAGE_BUTTON : AccessibilityRole.IMAGE;
|
||||
}
|
||||
|
||||
if (role.equals(AccessibilityRole.NONE)) {
|
||||
AccessibilityNodeInfoCompat.CollectionInfoCompat collection = nodeInfo.getCollectionInfo();
|
||||
if (collection != null) {
|
||||
// RecyclerView will be classified as a list or grid.
|
||||
if (collection.getRowCount() > 1 && collection.getColumnCount() > 1) {
|
||||
return AccessibilityRole.GRID;
|
||||
} else {
|
||||
return AccessibilityRole.LIST;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return role;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
/*
|
||||
* 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.utils;
|
||||
|
||||
import static android.content.Context.ACCESSIBILITY_SERVICE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.text.TextUtils;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewParent;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.widget.EditText;
|
||||
import com.facebook.sonar.core.SonarArray;
|
||||
import com.facebook.sonar.core.SonarObject;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* This class provides utility methods for determining certain accessibility properties of {@link
|
||||
* View}s and {@link AccessibilityNodeInfoCompat}s. It is porting some of the checks from {@link
|
||||
* com.googlecode.eyesfree.utils.AccessibilityNodeInfoUtils}, but has stripped many features which
|
||||
* are unnecessary here.
|
||||
*/
|
||||
public final class AccessibilityUtil {
|
||||
private AccessibilityUtil() {}
|
||||
|
||||
public static final EnumMapping sAccessibilityActionMapping =
|
||||
new EnumMapping("UNKNOWN") {
|
||||
{
|
||||
put("FOCUS", AccessibilityNodeInfoCompat.ACTION_FOCUS);
|
||||
put("CLEAR_FOCUS", AccessibilityNodeInfoCompat.ACTION_CLEAR_FOCUS);
|
||||
put("SELECT", AccessibilityNodeInfoCompat.ACTION_SELECT);
|
||||
put("CLEAR_SELECTION", AccessibilityNodeInfoCompat.ACTION_CLEAR_SELECTION);
|
||||
put("CLICK", AccessibilityNodeInfoCompat.ACTION_CLICK);
|
||||
put("LONG_CLICK", AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
|
||||
put("ACCESSIBILITY_FOCUS", AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS);
|
||||
put(
|
||||
"CLEAR_ACCESSIBILITY_FOCUS",
|
||||
AccessibilityNodeInfoCompat.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
|
||||
put(
|
||||
"NEXT_AT_MOVEMENT_GRANULARITY",
|
||||
AccessibilityNodeInfoCompat.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
|
||||
put(
|
||||
"PREVIOUS_AT_MOVEMENT_GRANULARITY",
|
||||
AccessibilityNodeInfoCompat.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
|
||||
put("NEXT_HTML_ELEMENT", AccessibilityNodeInfoCompat.ACTION_NEXT_HTML_ELEMENT);
|
||||
put("PREVIOUS_HTML_ELEMENT", AccessibilityNodeInfoCompat.ACTION_PREVIOUS_HTML_ELEMENT);
|
||||
put("SCROLL_FORWARD", AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD);
|
||||
put("SCROLL_BACKWARD", AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD);
|
||||
put("CUT", AccessibilityNodeInfoCompat.ACTION_CUT);
|
||||
put("COPY", AccessibilityNodeInfoCompat.ACTION_COPY);
|
||||
put("PASTE", AccessibilityNodeInfoCompat.ACTION_PASTE);
|
||||
put("SET_SELECTION", AccessibilityNodeInfoCompat.ACTION_SET_SELECTION);
|
||||
put("SET_SELECTION", AccessibilityNodeInfoCompat.ACTION_SET_SELECTION);
|
||||
put("EXPAND", AccessibilityNodeInfoCompat.ACTION_EXPAND);
|
||||
put("COLLAPSE", AccessibilityNodeInfoCompat.ACTION_COLLAPSE);
|
||||
put("DISMISS", AccessibilityNodeInfoCompat.ACTION_DISMISS);
|
||||
put("SET_TEXT", AccessibilityNodeInfoCompat.ACTION_SET_TEXT);
|
||||
}
|
||||
};
|
||||
|
||||
public static final EnumMapping sImportantForAccessibilityMapping =
|
||||
new EnumMapping("AUTO") {
|
||||
{
|
||||
put("AUTO", View.IMPORTANT_FOR_ACCESSIBILITY_AUTO);
|
||||
put("NO", View.IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
put("YES", View.IMPORTANT_FOR_ACCESSIBILITY_YES);
|
||||
put("NO_HIDE_DESCENDANTS", View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Given a {@link Context}, determine if any accessibility service is running.
|
||||
*
|
||||
* @param context The {@link Context} used to get the {@link AccessibilityManager}.
|
||||
* @return {@code true} if an accessibility service is currently running.
|
||||
*/
|
||||
public static boolean isAccessibilityEnabled(Context context) {
|
||||
return ((AccessibilityManager) context.getSystemService(ACCESSIBILITY_SERVICE)).isEnabled();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sentence describing why a given {@link View} will be ignored by Google's TalkBack
|
||||
* screen reader.
|
||||
*
|
||||
* @param view The {@link View} to evaluate.
|
||||
* @return {@code String} describing why a {@link View} is ignored.
|
||||
*/
|
||||
public static String getTalkbackIgnoredReasons(View view) {
|
||||
final int important = ViewCompat.getImportantForAccessibility(view);
|
||||
|
||||
if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO) {
|
||||
return "View has importantForAccessibility set to 'NO'.";
|
||||
}
|
||||
|
||||
if (important == ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
return "View has importantForAccessibility set to 'NO_HIDE_DESCENDANTS'.";
|
||||
}
|
||||
|
||||
ViewParent parent = view.getParent();
|
||||
while (parent instanceof View) {
|
||||
if (ViewCompat.getImportantForAccessibility((View) parent)
|
||||
== ViewCompat.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS) {
|
||||
return "An ancestor View has importantForAccessibility set to 'NO_HIDE_DESCENDANTS'.";
|
||||
}
|
||||
parent = parent.getParent();
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (node == null) {
|
||||
return "AccessibilityNodeInfo cannot be found.";
|
||||
}
|
||||
|
||||
try {
|
||||
if (AccessibilityEvaluationUtil.hasEqualBoundsToViewRoot(node, view)) {
|
||||
return "View has the same dimensions as the View Root.";
|
||||
}
|
||||
|
||||
if (!node.isVisibleToUser()) {
|
||||
return "View is not visible.";
|
||||
}
|
||||
|
||||
if (AccessibilityEvaluationUtil.isAccessibilityFocusable(node, view)) {
|
||||
return "View is actionable, but has no description.";
|
||||
}
|
||||
|
||||
if (AccessibilityEvaluationUtil.hasText(node)) {
|
||||
return "View is not actionable, and an ancestor View has co-opted its description.";
|
||||
}
|
||||
|
||||
return "View is not actionable and has no description.";
|
||||
} finally {
|
||||
node.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a sentence describing why a given {@link View} will be focusable by Google's TalkBack
|
||||
* screen reader.
|
||||
*
|
||||
* @param view The {@link View} to evaluate.
|
||||
* @return {@code String} describing why a {@link View} is focusable.
|
||||
*/
|
||||
@Nullable
|
||||
public static String getTalkbackFocusableReasons(View view) {
|
||||
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final boolean hasText = AccessibilityEvaluationUtil.hasText(node);
|
||||
final boolean isCheckable = node.isCheckable();
|
||||
final boolean hasNonActionableSpeakingDescendants =
|
||||
AccessibilityEvaluationUtil.hasNonActionableSpeakingDescendants(node, view);
|
||||
|
||||
if (AccessibilityEvaluationUtil.isActionableForAccessibility(node)) {
|
||||
if (node.getChildCount() <= 0) {
|
||||
return "View is actionable and has no children.";
|
||||
} else if (hasText) {
|
||||
return "View is actionable and has a description.";
|
||||
} else if (isCheckable) {
|
||||
return "View is actionable and checkable.";
|
||||
} else if (hasNonActionableSpeakingDescendants) {
|
||||
return "View is actionable and has non-actionable descendants with descriptions.";
|
||||
}
|
||||
}
|
||||
|
||||
if (AccessibilityEvaluationUtil.isTopLevelScrollItem(node, view)) {
|
||||
if (hasText) {
|
||||
return "View is a direct child of a scrollable container and has a description.";
|
||||
} else if (isCheckable) {
|
||||
return "View is a direct child of a scrollable container and is checkable.";
|
||||
} else if (hasNonActionableSpeakingDescendants) {
|
||||
return "View is a direct child of a scrollable container and has non-actionable "
|
||||
+ "descendants with descriptions.";
|
||||
}
|
||||
}
|
||||
|
||||
if (hasText) {
|
||||
return "View has a description and is not actionable, but has no actionable ancestor.";
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
node.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the text that Gogole's TalkBack screen reader will read aloud for a given {@link View}.
|
||||
* This may be any combination of the {@link View}'s {@code text}, {@code contentDescription}, and
|
||||
* the {@code text} and {@code contentDescription} of any ancestor {@link View}.
|
||||
*
|
||||
* <p>Note: This string does not include any additional semantic information that Talkback will
|
||||
* read, such as "Button", or "disabled".
|
||||
*
|
||||
* @param view The {@link View} to evaluate.
|
||||
* @return {@code String} describing why a {@link View} is focusable.
|
||||
*/
|
||||
@Nullable
|
||||
public static CharSequence getTalkbackDescription(View view) {
|
||||
final AccessibilityNodeInfoCompat node = ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (node == null) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final CharSequence contentDescription = node.getContentDescription();
|
||||
final CharSequence nodeText = node.getText();
|
||||
|
||||
final boolean hasNodeText = !TextUtils.isEmpty(nodeText);
|
||||
final boolean isEditText = view instanceof EditText;
|
||||
|
||||
// EditText's prioritize their own text content over a contentDescription
|
||||
if (!TextUtils.isEmpty(contentDescription) && (!isEditText || !hasNodeText)) {
|
||||
return contentDescription;
|
||||
}
|
||||
|
||||
if (hasNodeText) {
|
||||
return nodeText;
|
||||
}
|
||||
|
||||
// If there are child views and no contentDescription the text of all non-focusable children,
|
||||
// comma separated, becomes the description.
|
||||
if (view instanceof ViewGroup) {
|
||||
final StringBuilder concatChildDescription = new StringBuilder();
|
||||
final String separator = ", ";
|
||||
final ViewGroup viewGroup = (ViewGroup) view;
|
||||
|
||||
for (int i = 0, count = viewGroup.getChildCount(); i < count; i++) {
|
||||
final View child = viewGroup.getChildAt(i);
|
||||
|
||||
final AccessibilityNodeInfoCompat childNodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(child, childNodeInfo);
|
||||
|
||||
CharSequence childNodeDescription = null;
|
||||
if (AccessibilityEvaluationUtil.isSpeakingNode(childNodeInfo, child)
|
||||
&& !AccessibilityEvaluationUtil.isAccessibilityFocusable(childNodeInfo, child)) {
|
||||
childNodeDescription = getTalkbackDescription(child);
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(childNodeDescription)) {
|
||||
if (concatChildDescription.length() > 0) {
|
||||
concatChildDescription.append(separator);
|
||||
}
|
||||
concatChildDescription.append(childNodeDescription);
|
||||
}
|
||||
childNodeInfo.recycle();
|
||||
}
|
||||
|
||||
return concatChildDescription.length() > 0 ? concatChildDescription.toString() : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
} finally {
|
||||
node.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link SonarObject} of useful properties of AccessibilityNodeInfo, to be shown in the
|
||||
* Sonar Layout Inspector. All properties are immutable since they are all derived from various
|
||||
* {@link View} properties.
|
||||
*
|
||||
* @param view The {@link View} to derive the AccessibilityNodeInfo properties from.
|
||||
* @return {@link SonarObject} containing the properties.
|
||||
*/
|
||||
@Nullable
|
||||
public static SonarObject getAccessibilityNodeInfoProperties(View view) {
|
||||
final AccessibilityNodeInfoCompat nodeInfo =
|
||||
ViewAccessibilityHelper.createNodeInfoFromView(view);
|
||||
if (nodeInfo == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final SonarObject.Builder nodeInfoProps = new SonarObject.Builder();
|
||||
final Rect bounds = new Rect();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
final SonarArray.Builder actionsArrayBuilder = new SonarArray.Builder();
|
||||
for (AccessibilityNodeInfoCompat.AccessibilityActionCompat action :
|
||||
nodeInfo.getActionList()) {
|
||||
final String actionLabel = (String) action.getLabel();
|
||||
if (actionLabel != null) {
|
||||
actionsArrayBuilder.put(actionLabel);
|
||||
} else {
|
||||
actionsArrayBuilder.put(
|
||||
AccessibilityUtil.sAccessibilityActionMapping.get(action.getId(), false));
|
||||
}
|
||||
}
|
||||
nodeInfoProps.put("actions", actionsArrayBuilder.build());
|
||||
}
|
||||
|
||||
nodeInfoProps
|
||||
.put("clickable", nodeInfo.isClickable())
|
||||
.put("content-description", nodeInfo.getContentDescription())
|
||||
.put("text", nodeInfo.getText())
|
||||
.put("focused", nodeInfo.isAccessibilityFocused())
|
||||
.put("long-clickable", nodeInfo.isLongClickable())
|
||||
.put("focusable", nodeInfo.isFocusable());
|
||||
|
||||
nodeInfo.getBoundsInParent(bounds);
|
||||
nodeInfoProps.put(
|
||||
"parent-bounds",
|
||||
new SonarObject.Builder()
|
||||
.put("width", bounds.width())
|
||||
.put("height", bounds.height())
|
||||
.put("top", bounds.top)
|
||||
.put("left", bounds.left)
|
||||
.put("bottom", bounds.bottom)
|
||||
.put("right", bounds.right));
|
||||
|
||||
nodeInfo.getBoundsInScreen(bounds);
|
||||
nodeInfoProps.put(
|
||||
"screen-bounds",
|
||||
new SonarObject.Builder()
|
||||
.put("width", bounds.width())
|
||||
.put("height", bounds.height())
|
||||
.put("top", bounds.top)
|
||||
.put("left", bounds.left)
|
||||
.put("bottom", bounds.bottom)
|
||||
.put("right", bounds.right));
|
||||
|
||||
nodeInfo.recycle();
|
||||
|
||||
return nodeInfoProps.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies a {@link SonarObject.Builder} to add Talkback-specific Accessibiltiy properties to be
|
||||
* shown in the Sonar Layout Inspector.
|
||||
*
|
||||
* @param props The {@link SonarObject.Builder} to add the properties to.
|
||||
* @param view The {@link View} to derive the properties from.
|
||||
*/
|
||||
public static void addTalkbackProperties(SonarObject.Builder props, View view) {
|
||||
if (!AccessibilityEvaluationUtil.isTalkbackFocusable(view)) {
|
||||
props
|
||||
.put("talkback-ignored", true)
|
||||
.put("talkback-ignored-reasons", getTalkbackIgnoredReasons(view));
|
||||
} else {
|
||||
props
|
||||
.put("talkback-focusable", true)
|
||||
.put("talkback-focusable-reasons", getTalkbackFocusableReasons(view))
|
||||
.put("talkback-description", getTalkbackDescription(view));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* 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.utils;
|
||||
|
||||
import static android.view.WindowManager.LayoutParams;
|
||||
|
||||
import android.os.Build;
|
||||
import android.view.View;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public final class AndroidRootResolver {
|
||||
|
||||
private static final String WINDOW_MANAGER_IMPL_CLAZZ = "android.view.WindowManagerImpl";
|
||||
private static final String WINDOW_MANAGER_GLOBAL_CLAZZ = "android.view.WindowManagerGlobal";
|
||||
private static final String VIEWS_FIELD = "mViews";
|
||||
private static final String WINDOW_PARAMS_FIELD = "mParams";
|
||||
private static final String GET_DEFAULT_IMPL = "getDefault";
|
||||
private static final String GET_GLOBAL_INSTANCE = "getInstance";
|
||||
|
||||
private boolean initialized;
|
||||
private Object windowManagerObj;
|
||||
private Field viewsField;
|
||||
private Field paramsField;
|
||||
|
||||
public static class Root {
|
||||
public final View view;
|
||||
public final LayoutParams param;
|
||||
|
||||
private Root(View view, LayoutParams param) {
|
||||
this.view = view;
|
||||
this.param = param;
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable List<Root> listActiveRoots() {
|
||||
if (!initialized) {
|
||||
initialize();
|
||||
}
|
||||
|
||||
if (null == windowManagerObj) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (null == viewsField) {
|
||||
return null;
|
||||
}
|
||||
if (null == paramsField) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<View> views = null;
|
||||
List<LayoutParams> params = null;
|
||||
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT < 19) {
|
||||
views = Arrays.asList((View[]) viewsField.get(windowManagerObj));
|
||||
params = Arrays.asList((LayoutParams[]) paramsField.get(windowManagerObj));
|
||||
} else {
|
||||
views = (List<View>) viewsField.get(windowManagerObj);
|
||||
params = (List<LayoutParams>) paramsField.get(windowManagerObj);
|
||||
}
|
||||
} catch (RuntimeException | IllegalAccessException re) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<Root> roots = new ArrayList<>();
|
||||
for (int i = 0, stop = views.size(); i < stop; i++) {
|
||||
roots.add(new Root(views.get(i), params.get(i)));
|
||||
}
|
||||
return roots;
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
initialized = true;
|
||||
String accessClass =
|
||||
Build.VERSION.SDK_INT > 16 ? WINDOW_MANAGER_GLOBAL_CLAZZ : WINDOW_MANAGER_IMPL_CLAZZ;
|
||||
String instanceMethod = Build.VERSION.SDK_INT > 16 ? GET_GLOBAL_INSTANCE : GET_DEFAULT_IMPL;
|
||||
|
||||
try {
|
||||
Class<?> clazz = Class.forName(accessClass);
|
||||
Method getMethod = clazz.getMethod(instanceMethod);
|
||||
windowManagerObj = getMethod.invoke(null);
|
||||
viewsField = clazz.getDeclaredField(VIEWS_FIELD);
|
||||
viewsField.setAccessible(true);
|
||||
paramsField = clazz.getDeclaredField(WINDOW_PARAMS_FIELD);
|
||||
paramsField.setAccessible(true);
|
||||
} catch (InvocationTargetException | IllegalAccessException | RuntimeException | NoSuchMethodException | NoSuchFieldException | ClassNotFoundException ignored) {
|
||||
}
|
||||
}
|
||||
}
|
||||
51
android/plugins/inspector/descriptors/utils/EnumMapping.java
Normal file
51
android/plugins/inspector/descriptors/utils/EnumMapping.java
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* 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.utils;
|
||||
|
||||
import static com.facebook.sonar.plugins.inspector.InspectorValue.Type.Enum;
|
||||
|
||||
import android.support.v4.util.SimpleArrayMap;
|
||||
import com.facebook.sonar.plugins.inspector.InspectorValue;
|
||||
|
||||
public class EnumMapping {
|
||||
private final SimpleArrayMap<String, Integer> mMapping = new SimpleArrayMap<>();
|
||||
private final String mDefaultKey;
|
||||
|
||||
public EnumMapping(String defaultKey) {
|
||||
mDefaultKey = defaultKey;
|
||||
}
|
||||
|
||||
public void put(String s, int i) {
|
||||
mMapping.put(s, i);
|
||||
}
|
||||
|
||||
public InspectorValue get(final int i) {
|
||||
return get(i, true);
|
||||
}
|
||||
|
||||
public InspectorValue get(final int i, final boolean mutable) {
|
||||
for (int ii = 0, count = mMapping.size(); ii < count; ii++) {
|
||||
if (mMapping.valueAt(ii) == i) {
|
||||
return mutable
|
||||
? InspectorValue.mutable(Enum, mMapping.keyAt(ii))
|
||||
: InspectorValue.immutable(Enum, mMapping.keyAt(ii));
|
||||
}
|
||||
}
|
||||
return mutable
|
||||
? InspectorValue.mutable(Enum, mDefaultKey)
|
||||
: InspectorValue.immutable(Enum, mDefaultKey);
|
||||
}
|
||||
|
||||
public int get(String s) {
|
||||
if (mMapping.containsKey(s)) {
|
||||
return mMapping.get(s);
|
||||
}
|
||||
return mMapping.get(mDefaultKey);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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.utils;
|
||||
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.accessibility.AccessibilityNodeInfoCompat;
|
||||
import android.view.View;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/** Class that helps with accessibility by providing useful methods. */
|
||||
public final class ViewAccessibilityHelper {
|
||||
|
||||
/**
|
||||
* Creates and returns an {@link AccessibilityNodeInfoCompat} from the the provided {@link View}.
|
||||
* Note: This does not handle recycling of the {@link AccessibilityNodeInfoCompat}.
|
||||
*
|
||||
* @param view The {@link View} to create the {@link AccessibilityNodeInfoCompat} from.
|
||||
* @return {@link AccessibilityNodeInfoCompat}
|
||||
*/
|
||||
@Nullable
|
||||
public static AccessibilityNodeInfoCompat createNodeInfoFromView(View view) {
|
||||
if (view == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final AccessibilityNodeInfoCompat nodeInfo = AccessibilityNodeInfoCompat.obtain();
|
||||
|
||||
// For some unknown reason, Android seems to occasionally throw a NPE from
|
||||
// onInitializeAccessibilityNodeInfo.
|
||||
try {
|
||||
ViewCompat.onInitializeAccessibilityNodeInfo(view, nodeInfo);
|
||||
} catch (NullPointerException e) {
|
||||
if (nodeInfo != null) {
|
||||
nodeInfo.recycle();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
return nodeInfo;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user