diff --git a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ActivityDescriptor.java b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ActivityDescriptor.java index eeba69925..925a7a9a5 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ActivityDescriptor.java +++ b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ActivityDescriptor.java @@ -14,6 +14,7 @@ import com.facebook.flipper.core.FlipperObject; import com.facebook.flipper.plugins.inspector.Named; import com.facebook.flipper.plugins.inspector.NodeDescriptor; import com.facebook.flipper.plugins.inspector.Touch; +import com.facebook.flipper.plugins.inspector.descriptors.utils.ContextDescriptorUtils; import com.facebook.flipper.plugins.inspector.descriptors.utils.stethocopies.FragmentActivityAccessor; import com.facebook.flipper.plugins.inspector.descriptors.utils.stethocopies.FragmentCompat; import com.facebook.flipper.plugins.inspector.descriptors.utils.stethocopies.FragmentManagerAccessor; @@ -65,7 +66,7 @@ public class ActivityDescriptor extends NodeDescriptor { @Override public List> getData(Activity node) { - return Collections.EMPTY_LIST; + return Collections.singletonList(new Named<>("Theme", ContextDescriptorUtils.themeData(node))); } @Override diff --git a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ApplicationDescriptor.java b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ApplicationDescriptor.java index 4942fd179..7f4c38066 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ApplicationDescriptor.java +++ b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ApplicationDescriptor.java @@ -19,6 +19,7 @@ import com.facebook.flipper.plugins.inspector.ApplicationWrapper; import com.facebook.flipper.plugins.inspector.Named; import com.facebook.flipper.plugins.inspector.NodeDescriptor; import com.facebook.flipper.plugins.inspector.Touch; +import com.facebook.flipper.plugins.inspector.descriptors.utils.ContextDescriptorUtils; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -204,7 +205,8 @@ public class ApplicationDescriptor extends NodeDescriptor { @Override public List> getData(ApplicationWrapper node) { - return Collections.EMPTY_LIST; + return Collections.singletonList( + new Named<>("Theme", ContextDescriptorUtils.themeData(node.getApplication()))); } @Override diff --git a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ViewDescriptor.java b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ViewDescriptor.java index 9b2f9fb88..7e8e750ca 100644 --- a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ViewDescriptor.java +++ b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/ViewDescriptor.java @@ -38,6 +38,7 @@ import com.facebook.flipper.plugins.inspector.Touch; import com.facebook.flipper.plugins.inspector.descriptors.utils.AccessibilityEvaluationUtil; import com.facebook.flipper.plugins.inspector.descriptors.utils.AccessibilityRoleUtil; import com.facebook.flipper.plugins.inspector.descriptors.utils.AccessibilityUtil; +import com.facebook.flipper.plugins.inspector.descriptors.utils.ContextDescriptorUtils; import com.facebook.flipper.plugins.inspector.descriptors.utils.EnumMapping; import com.facebook.flipper.plugins.inspector.descriptors.utils.stethocopies.ResourcesUtil; import java.lang.reflect.Field; @@ -186,7 +187,9 @@ public class ViewDescriptor extends NodeDescriptor { viewProps.put("foreground", fromDrawable(node.getForeground())); } - return Arrays.asList(new Named<>("View", viewProps.build())); + return Arrays.asList( + new Named<>("View", viewProps.build()), + new Named<>("Theme", ContextDescriptorUtils.themeData(node.getContext()))); } @Override diff --git a/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/ContextDescriptorUtils.java b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/ContextDescriptorUtils.java new file mode 100644 index 000000000..8f6d6efae --- /dev/null +++ b/android/src/main/java/com/facebook/flipper/plugins/inspector/descriptors/utils/ContextDescriptorUtils.java @@ -0,0 +1,121 @@ +/* + * Copyright (c) Facebook, Inc. and its affiliates. + * + * 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.flipper.plugins.inspector.descriptors.utils; + +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.util.Log; +import android.util.TypedValue; +import com.facebook.flipper.core.FlipperObject; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +public final class ContextDescriptorUtils { + private static String TAG = "ContextDescriptor"; + + private static Field sThemeImplField; + private static Field sThemeImplThemeKeyField; + private static Field sThemeImplAssetManagerField; + private static Field sThemeKeyResIdField; + private static Method sAssetManagerGetStyleAttributesMethod; + + private ContextDescriptorUtils() {} + + public static FlipperObject themeData(Context context) { + FlipperObject.Builder themeData = new FlipperObject.Builder(); + Map builders = collectThemeValues(context); + for (Map.Entry entry : builders.entrySet()) { + themeData.put(entry.getKey(), entry.getValue().build()); + } + return themeData.build(); + } + + private static Map collectThemeValues(Context context) { + Map builderMap = new HashMap<>(3); + try { + Resources.Theme theme = context.getTheme(); + AssetManager assetManager = context.getAssets(); + final Object themeImpl; + final Object themeKey; + + // Nasty reflection to get a list of theme attributes that apply to this context. + if (sThemeImplField == null + || sThemeImplThemeKeyField == null + || sThemeKeyResIdField == null + || sThemeImplAssetManagerField == null) { + sThemeImplField = theme.getClass().getDeclaredField("mThemeImpl"); + sThemeImplField.setAccessible(true); + + themeImpl = sThemeImplField.get(theme); + sThemeImplThemeKeyField = themeImpl.getClass().getDeclaredField("mKey"); + sThemeImplThemeKeyField.setAccessible(true); + + sThemeImplAssetManagerField = themeImpl.getClass().getDeclaredField("mAssets"); + sThemeImplAssetManagerField.setAccessible(true); + + sAssetManagerGetStyleAttributesMethod = + assetManager.getClass().getDeclaredMethod("getStyleAttributes", int.class); + sAssetManagerGetStyleAttributesMethod.setAccessible(true); + + themeKey = sThemeImplThemeKeyField.get(themeImpl); + sThemeKeyResIdField = themeKey.getClass().getDeclaredField("mResId"); + sThemeKeyResIdField.setAccessible(true); + } else { + themeImpl = sThemeImplField.get(theme); + themeKey = sThemeImplThemeKeyField.get(themeImpl); + } + + int[] appliedThemeResIds = (int[]) sThemeKeyResIdField.get(themeKey); + TypedValue typedValue = new TypedValue(); + Resources resources = context.getResources(); + for (int themeId : appliedThemeResIds) { + String name = resources.getResourceName(themeId); + // The res id array can have duplicates + if (builderMap.containsKey(name)) { + continue; + } + + FlipperObject.Builder builder = new FlipperObject.Builder(); + builderMap.put(name, builder); + + int[] attributes = + (int[]) sAssetManagerGetStyleAttributesMethod.invoke(assetManager, themeId); + for (int attribute : attributes) { + if (!theme.resolveAttribute(attribute, typedValue, true)) { + continue; + } + String attributeName = context.getResources().getResourceName(attribute); + String[] nameParts = attributeName.split(":"); + if (nameParts.length < 2) { + Log.d(TAG, "Unknown attribute name format " + attributeName); + } else { + attributeName = nameParts[1].split("/")[1]; + } + String strValue = TypedValue.coerceToString(typedValue.type, typedValue.data); + if (strValue == null) { + strValue = "null"; + } else if (strValue.startsWith("@")) { + int resId = Integer.parseInt(strValue.substring(1)); + if (resId == 0) { + strValue = "null"; + } else { + strValue = context.getResources().getResourceName(resId); + } + } + builder.put(attributeName, strValue); + } + } + } catch (Throwable ignored) { + Log.d(TAG, "Failed to generate theme attribute data!", ignored); + } + return builderMap; + } +} diff --git a/android/src/test/java/com/facebook/flipper/plugins/inspector/InspectorFlipperPluginTest.java b/android/src/test/java/com/facebook/flipper/plugins/inspector/InspectorFlipperPluginTest.java index 3e9bb43df..f138f5b17 100644 --- a/android/src/test/java/com/facebook/flipper/plugins/inspector/InspectorFlipperPluginTest.java +++ b/android/src/test/java/com/facebook/flipper/plugins/inspector/InspectorFlipperPluginTest.java @@ -98,7 +98,7 @@ public class InspectorFlipperPluginTest { new FlipperObject.Builder() .put("id", "com.facebook.flipper") .put("name", "com.facebook.flipper") - .put("data", new FlipperObject.Builder()) + .put("data", new FlipperObject.Builder().put("Theme", new FlipperObject.Builder())) .put("children", new FlipperArray.Builder().put("test")) .put("attributes", new FlipperArray.Builder()) .put("decoration", (String) null)