1

Is there a way to add or remove keys from an AttributeSet when defining a custom View?

    public BFPlayer(Context context, AttributeSet attrs, int defStyle) {
        super(context, camelToSnake(attrs), defStyle);
        init(context, attrs, defStyle);
    }

    private static AttributeSet camelToSnake(AttributeSet attrs) {
        int num = attrs.getAttributeCount();
        for (int i = 0; i < num; i++)
        {
            String name = attrs.getAttributeName(i);
            String newName = CaseFormat.UPPER_CAMEL.to(CaseFormat.UPPER_SNAKE_CASE, name);

            // I'm looking for the two methods below:
            attrs.addAttribute(newName, attrs.getAttributeValue(i));
            attrs.removeAttribute(name);

        }
        return attrs;
    }

Update: My attempt

I tried creating the following class:

BFAttributes.java

public class BFAttributeSet implements AttributeSet {
    private final AttributeSet attrs;
    private static final List<String> playerViewAttributes = new ArrayList<>(Arrays.asList(
            "useArtwork",
            "defaultArtwork",
            "useController",
            "hideOnTouch",
            "autoShow",
            "hideDuringAds",
            "showBuffering",
            "resizeMode",
            "surfaceType"
    ));

    public BFAttributeSet(AttributeSet attrs)
    {
        this.attrs = attrs;
    }

    @Override
    public int getAttributeCount() {
        return attrs.getAttributeCount();
    }

    @Override
    public String getAttributeName(int index) {
        String name = attrs.getAttributeName(index);
        if (playerViewAttributes.contains(name))
        {
            name = CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, name);
        }
        return name;
    }

    @Override
    public String getAttributeValue(int index) {
        return attrs.getAttributeValue(index);
    }

    @Override
    public String getAttributeValue(String namespace, String name) {
        return attrs.getAttributeValue(namespace, name);
    }

    @Override
    public String getPositionDescription() {
        return attrs.getPositionDescription();
    }

    @Override
    public int getAttributeNameResource(int index) {
        return attrs.getAttributeNameResource(index);
    }

    @Override
    public int getAttributeListValue(String namespace, String attribute, String[] options, int defaultValue) {
        return attrs.getAttributeListValue(namespace, attribute, options, defaultValue);
    }

    @Override
    public boolean getAttributeBooleanValue(String namespace, String attribute, boolean defaultValue) {
        return attrs.getAttributeBooleanValue(namespace, attribute, defaultValue);
    }

    @Override
    public int getAttributeResourceValue(String namespace, String attribute, int defaultValue) {
        return attrs.getAttributeResourceValue(namespace, attribute, defaultValue);
    }

    @Override
    public int getAttributeIntValue(String namespace, String attribute, int defaultValue) {
        return attrs.getAttributeIntValue(namespace, attribute, defaultValue);
    }

    @Override
    public int getAttributeUnsignedIntValue(String namespace, String attribute, int defaultValue) {
        return attrs.getAttributeUnsignedIntValue(namespace, attribute, defaultValue);
    }

    @Override
    public float getAttributeFloatValue(String namespace, String attribute, float defaultValue) {
        return attrs.getAttributeFloatValue(namespace, attribute, defaultValue);
    }

    @Override
    public int getAttributeListValue(int index, String[] options, int defaultValue) {
        return attrs.getAttributeListValue(index, options, defaultValue);
    }

    @Override
    public boolean getAttributeBooleanValue(int index, boolean defaultValue) {
        return attrs.getAttributeBooleanValue(index, defaultValue);
    }

    @Override
    public int getAttributeResourceValue(int index, int defaultValue) {
        return attrs.getAttributeResourceValue(index, defaultValue);
    }

    @Override
    public int getAttributeIntValue(int index, int defaultValue) {
        return attrs.getAttributeIntValue(index, defaultValue);
    }

    @Override
    public int getAttributeUnsignedIntValue(int index, int defaultValue) {
        return attrs.getAttributeUnsignedIntValue(index, defaultValue);
    }

    @Override
    public float getAttributeFloatValue(int index, float defaultValue) {
        return attrs.getAttributeFloatValue(index, defaultValue);
    }

    @Override
    public String getIdAttribute() {
        return attrs.getIdAttribute();
    }

    @Override
    public String getClassAttribute() {
        return attrs.getClassAttribute();
    }

    @Override
    public int getIdAttributeResourceValue(int defaultValue) {
        return attrs.getIdAttributeResourceValue(defaultValue);
    }

    @Override
    public int getStyleAttribute() {
        return attrs.getStyleAttribute();
    }
}

Then in my custom view, I did the following:

    public BFPlayer(Context context, AttributeSet attrs, int defStyle) {
        super(context, new BFAttributeSet(attrs), defStyle);
        init(context, attrs, defStyle);
    }

This threw the following error when my app launched:

2019-05-08 17:54:54.641 24456-24456/? E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.blueframetech.blueframesdk, PID: 24456
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.blueframetech.blueframesdk/com.blueframetech.blueframesdk.MainActivity}: android.view.InflateException: Binary XML file line #9: Binary XML file line #9: Error inflating class com.blueframetech.bfplayer.BFPlayer
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2913)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
     Caused by: android.view.InflateException: Binary XML file line #9: Binary XML file line #9: Error inflating class com.blueframetech.bfplayer.BFPlayer
     Caused by: android.view.InflateException: Binary XML file line #9: Error inflating class com.blueframetech.bfplayer.BFPlayer
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Constructor.newInstance0(Native Method)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
        at android.view.LayoutInflater.createView(LayoutInflater.java:647)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:790)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:730)
        at android.view.LayoutInflater.rInflate(LayoutInflater.java:863)
        at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:824)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:515)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:374)
        at android.support.v7.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:469)
        at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:140)
        at com.blueframetech.blueframesdk.MainActivity.onCreate(MainActivity.java:14)
        at android.app.Activity.performCreate(Activity.java:7136)
        at android.app.Activity.performCreate(Activity.java:7127)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1271)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2893)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3048)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:78)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:108)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:68)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1808)
        at android.os.Handler.dispatchMessage(Handler.java:106)
        at android.os.Looper.loop(Looper.java:193)
        at android.app.ActivityThread.main(ActivityThread.java:6669)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
     Caused by: java.lang.ClassCastException: com.blueframetech.bfplayer.facades.BFAttributeSet cannot be cast to android.content.res.XmlBlock$Parser
        at android.content.res.ResourcesImpl$ThemeImpl.obtainStyledAttributes(ResourcesImpl.java:1336)
        at android.content.res.Resources$Theme.obtainStyledAttributes(Resources.java:1537)
        at android.content.Context.obtainStyledAttributes(Context.java:712)
2019-05-08 17:54:54.642 24456-24456/? E/AndroidRuntime:     at android.view.View.<init>(View.java:4950)
        at android.view.ViewGroup.<init>(ViewGroup.java:659)
        at android.widget.FrameLayout.<init>(FrameLayout.java:92)
        at android.widget.FrameLayout.<init>(FrameLayout.java:87)
        at com.google.android.exoplayer2.ui.PlayerView.<init>(PlayerView.java:308)
        at com.google.android.exoplayer2.ui.PlayerView.<init>(PlayerView.java:304)
        at com.blueframetech.bfplayer.BFPlayer.<init>(BFPlayer.java:38)
            ... 28 more

The underlying error is this one:

java.lang.ClassCastException: com.blueframetech.bfplayer.facades.BFAttributeSet cannot be cast to android.content.res.XmlBlock$Parser
stevendesu
  • 15,753
  • 22
  • 105
  • 182
  • You could make a facade for initial attribute set that would return your modified values or not return values that are supposed to be removed. – M. Prokhorov May 08 '19 at 17:32
  • Which methods of would I need to override in order to achieve the desired result, if the only time the AttributeSet is ever used is when it's passed to `context.obtainStyledAttributes`? – stevendesu May 08 '19 at 17:42

1 Answers1

1

After a lot of experimentation, reading the Android source code, and Googling, my suspicions were confirmed when I found this post to a similar question

Even though it looks like Android is parsing an XML file to build the layout for your app, the XML is actually processed as build time and converted to an optimized class which is loaded. Modifying attributes on the fly (such as via a facade) would circumvent the performance gains from this build-time compilation of views. Therefore it can't be done. Whatever is in the XML at compile time, that's what your class will see in the AttributeSet at run time.

Specifically, XML files are compiled to a binary representation which is parsed by XmlBlock - a private class of android.content.res which is not exposed for subclassing.

It may be possible to subclass XmlBlock by using Reflection (since Reflection bypasses a lot of checks and balances, allowing you to access private members and methods) - but I'm not very knowledgeable about using Reflection and I think it's an anti-pattern, anyway.

Still, if someone knows a way to subclass a package-private class using Reflection and can post how, I would gladly accept it as the answer (since it's the only way to accomplish what I want)

stevendesu
  • 15,753
  • 22
  • 105
  • 182