Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added @UsesXmls annotation which allows to create xml files in APK resources #3292

Open
wants to merge 5 commits into
base: master
Choose a base branch
from

Conversation

patryk84a
Copy link
Contributor

@patryk84a patryk84a commented Dec 14, 2024

General items:

If your code changes how something works on the device (i.e., it affects the companion):

  • I branched from ucr
  • My pull request has ucr as the base

Further, if you've changed the blocks language or another user-facing designer/blocks API (added a SimpleProperty, etc.):

  • I have updated the corresponding version number in appinventor/components/src/.../common/YaVersion.java
  • I have updated the corresponding upgrader in appinventor/appengine/src/.../client/youngandroid/YoungAndroidFormUpgrader.java (components only)
  • I have updated the corresponding entries in appinventor/blocklyeditor/src/versioning.js

For all other changes:

  • I branched from master
  • My pull request has master as the base

What does this PR accomplish?

I think this will solve some problems caused by lack of resources in APK.

We can only create xml files and only in these folders with different configurations:

layout, values, drawable, mipmap, xml, color, menu, animator, anim

Example usage:

@UsesXmls(xmls = {
    @XmlElement(dir = "xml",
                name = "automotive_app_desc.xml",
                content = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<automotiveApp>\n<uses name=\"media\"/>\n</automotiveApp>")
})

Reference:
https://developer.android.com/guide/topics/resources/providing-resources.html

Resolves #3246 .

@AppInventorWorkerBee
Copy link
Collaborator

Can one of the admins verify this patch?

@ewpatton
Copy link
Member

@AppInventorWorkerBee ok to test

Comment on lines 2 to 3
// Copyright 2009-2011 Google, All Rights reserved
// Copyright 2011-2024 MIT, All rights reserved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that since this is a new file you only need to have (c) 2024 MIT here.

// Transform a @XmlElement into an String for use later
// in creating xml files.
private static String xmlElementToString(XmlElement element)
throws IllegalAccessException, InvocationTargetException {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not clear that these exceptions should ever be thrown in this context.

Comment on lines 1863 to 1867
} catch (IllegalAccessException e) {
messager.printMessage(Diagnostic.Kind.ERROR, "IllegalAccessException when gathering " +
"xml attributes and subelements for component " + componentInfo.name);
throw new RuntimeException(e);
} catch (InvocationTargetException e) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my note below about whether these exceptions can ever be thrown.

@@ -243,7 +244,7 @@ private boolean generateServices() {
private boolean generateContentProviders() {
try {
loadJsonInfo(context.getComponentInfo().getContentProvidersNeeded(),
ComponentDescriptorConstants.SERVICES_TARGET);
ComponentDescriptorConstants.CONTENT_PROVIDERS_TARGET);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this was addressed in #3279

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so I'll remove this change.

for (Map.Entry<String, Set<String>> component : xmlsNeeded.entrySet()) {
for (String xml : component.getValue()) {
String[] parts = xml.split(":", 2);
context.getReporter().log("Creating " + parts[0]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably don't need to log every single file getting created.

Comment on lines +183 to +184
createDir(context.getPaths().getResDir(), new File(parts[0]).getParent()),
new File(parts[0]).getName());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably do some validation of parts[0] here. My concern is that a maliciously crafted components.json might have something like "../../../../../aapt" or similar in an attempt to overwrite executable files outside of the build directory.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can't overwrite other files than XML because the file extension is hardcoded. UsesXmls only specifies the file name. But I assumed that the file will be saved only in a single folder nesting. I haven't tested it with a larger nesting. Maybe we need to validate by checking the first folder names and limit to a few hardcoded names like xml, drawable etc.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My point is that if you edit the JSON file you can make the filename be something else since the .xml is added at the point of the component processing when the extension is built, not at app build time, which is when this code is run. It would be less of a problem if you added the ".xml" here rather than in the component processor (which would be behavior that matches your statement).

Copy link
Contributor Author

@patryk84a patryk84a Dec 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I've researched and tested a few things. We can create any folder but it won't be included in the android resources. We have to use android defined folders. A folder can't contain a subfolder. Names can contain lowercase and uppercase letters, numbers, underscore, dash and plus (including configuration), can't start with numbers. I will use this to return an error for the file or folder name if it is outside of this range.

path.matches("^(?:(layout|values|drawable|mipmap|xml|color|menu|animator|anim)[a-zA-Z0-9-+_]*/)?[a-z][a-zA-Z0-9-+_.]*$")

As for the .xml extension, I think to check it, if the file contains the .xml extension, we can leave it, if it does not contain the extension, I will add it. If it contains another extension, return an error or change it to xml?

Reference:
https://developer.android.com/guide/topics/resources/providing-resources.html

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As for the .xml extension, I think to check it, if the file contains the .xml extension, we can leave it, if it does not contain the extension, I will add it. If it contains another extension, return an error or change it to xml?

Yes, I think you can just simplify it to something like:

if (!name.endsWith(".xml")) {
  name += ".xml"
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I will add it additionally before validation.

@patryk84a patryk84a requested a review from ewpatton December 18, 2024 21:42
@patryk84a
Copy link
Contributor Author

@ewpatton Adding xml works fine. But there is a problem with the id for these files. If the library uses some drawable or layout by its id, it does not work. The id generated by the compiler is different from the one in the R file created for the given library. AppInventor has several fixed xml files so creating an R file here is easy. Is there a way to make the compiler index the xml files with the ids we want? I have an idea to predict the index, because the index is assigned in alphabetical order in a given folder. Is there another way?

@ewpatton ewpatton linked an issue Dec 22, 2024 that may be closed by this pull request
@ewpatton
Copy link
Member

ewpatton commented Jan 6, 2025

@ewpatton Adding xml works fine. But there is a problem with the id for these files. If the library uses some drawable or layout by its id, it does not work. The id generated by the compiler is different from the one in the R file created for the given library. AppInventor has several fixed xml files so creating an R file here is easy. Is there a way to make the compiler index the xml files with the ids we want? I have an idea to predict the index, because the index is assigned in alphabetical order in a given folder. Is there another way?

I don't really know. Can you provide a test project with the issue so I can take a look?

@patryk84a
Copy link
Contributor Author

Yes, I'll try to make an example project. Maybe I described it badly here. Libraries expect indexes from the R file from their package. However, the index in R files from the library package is different than the index in the R file created in the main application class, by aapt2. Because we deliver the R file for the library as an R.java file with the extension.

@patryk84a
Copy link
Contributor Author

patryk84a commented Jan 8, 2025

@ewpatton So, I have prepared a short presentation showing what the problem is.

Let's start with the aar sample library. I've built a simple library that sets the AppCompatImageView drawable from the resource: "drawable/media3_icon_settings.xml", using the setImageResource(int id); method. The library has two methods for setting an image. The first sets an image directly from a library resource, the second sets an icon from any resource id. The library also has a method that returns the id of a drawabe from the library.

Source:

package my.ex.library;

import android.annotation.SuppressLint;
import android.content.Context;
import android.util.AttributeSet;
import androidx.appcompat.widget.AppCompatImageView;

public class CustomImageView extends AppCompatImageView {

    public CustomImageView(Context context) {
        super(context);
    }

    public CustomImageView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    public CustomImageView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    public void SetIcon() {
        setImageResource(R.drawable.media3_icon_settings);
    }

    public void SetIconId(int drawableId){
        setImageResource(drawableId);
    }

    @SuppressLint("ResourceType")
    public static int getLibId() {
        return R.drawable.media3_icon_settings;
    }
}

Resources:
image

Next I built a simple extension. It uses methods from the library to set the image:

package pl.patryk_f.extxml;

import com.google.appinventor.components.annotations.*;
import com.google.appinventor.components.runtime.*;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import my.ex.library.CustomImageView;

@DesignerComponent(version = 4, versionName = "1.0", description = "Developed by Patryk by Fast.", iconName = "icon.png")
public class ExtXml extends AndroidNonvisibleComponent {
   public ExtXml(ComponentContainer container) {
    super(container.$form());    
  }

  @SimpleFunction
  public void AddCustomImageView(AndroidViewComponent component) {
    CustomImageView customImageView = new CustomImageView(form.$context());
    FrameLayout view = (FrameLayout) component.getView();    
    LinearLayout linearLayout = (LinearLayout) view.getChildAt(0);    
    linearLayout.addView(customImageView);
    customImageView.SetIcon();
  }

  @SimpleFunction
  public void AddCustomImageViewId(AndroidViewComponent component, int id) {
    CustomImageView customImageView = new CustomImageView(form.$context());
    FrameLayout view = (FrameLayout) component.getView();    
    LinearLayout linearLayout = (LinearLayout) view.getChildAt(0);    
    linearLayout.addView(customImageView);
    customImageView.SetIconId(id);
  }

  @SimpleProperty
  public int IdFromLibrary() {
    return CustomImageView.getLibId();
  }
  @SimpleProperty
  public int IdFromApp() {
    return form.$context().getResources().getIdentifier("media3_icon_settings", "drawable", form.$context().getPackageName());
  }
}

The extension also returns the index of our drawable resource as assigned by aapt2 in the R file in the application class.
We must also add the R.java file to the extension so that we do not receive an error after starting the application. We are not able to predict what id will be assigned to our aapt2 drawable, so we enter any id to not receive an error.

Error without R.java:

image

R source:

package my.ex.library;

public final class R {
    public static final class drawable {
        public static final int media3_icon_settings = 0x7f099000;
    }
}

It's R for the library, so it has a library package.

We also add the xml file itself to the extension using @UsesXmls or another method to get such content in the component_build_infos.json file:

[
  {
    "author": "Patryk",
    "type": "pl.patryk_f.extxml.ExtXml",
    "androidMinSdk": ["7"],
    "compiledBy": "FAST",
    "xmls": [
    "drawable-anydpi-v4/media3_icon_settings.xml:<!-- Copyright 2024 The Android Open Source Project\n\n     Licensed under the Apache License, Version 2.0 (the \"License\");\n     you may not use this file except in compliance with the License.\n     You may obtain a copy of the License at\n\n          http://www.apache.org/licenses/LICENSE-2.0\n\n     Unless required by applicable law or agreed to in writing, software\n     distributed under the License is distributed on an \"AS IS\" BASIS,\n     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n     See the License for the specific language governing permissions and\n     limitations under the License.\n-->\n<vector xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    android:width=\"24dp\"\n    android:height=\"24dp\"\n    android:viewportWidth=\"960\"\n    android:viewportHeight=\"960\">\n  <path\n      android:fillColor=\"@android:color/white\"\n      android:pathData=\"M370,880L354,752Q341,747 329.5,740Q318,733 307,725L188,775L78,585L181,507Q180,500 180,493.5Q180,487 180,480Q180,473 180,466.5Q180,460 181,453L78,375L188,185L307,235Q318,227 330,220Q342,213 354,208L370,80L590,80L606,208Q619,213 630.5,220Q642,227 653,235L772,185L882,375L779,453Q780,460 780,466.5Q780,473 780,480Q780,487 780,493.5Q780,500 778,507L881,585L771,775L653,725Q642,733 630,740Q618,747 606,752L590,880L370,880ZM482,620Q540,620 581,579Q622,538 622,480Q622,422 581,381Q540,340 482,340Q423,340 382.5,381Q342,422 342,480Q342,538 382.5,579Q423,620 482,620Z\"/>\n</vector>\n"
  ]
  }
]

Now application:

blocks (42)

We display the indexes from the library's R class file and the application's R class file. We also have three buttons that we will use to set our drawable image in ImageView. The first button sets the image directly in the library without an external id. The next two use the id returned by the extension.

After decompiling the application, we can see that it has our drawables:

image

The file is correct:

image

The application contains classes from our library:

image

The R file from our library contains an index assigned to our drawable, which we assigned in the R.java class whose source I showed above.

image

Now let's look for the R file in the main class of our application. It's here:

image

Let's look for our drawable and its index. As you can see, it's in line 186:

image

But unfortunately the index generated by aapt2 is different from the one we saved in the R.java file.

Now let's open our application on the phone:

image

Both identifiers of our drawable are displayed. As you can see, they match what was visible in the R files.

When we click the button that sets the image directly from the resource in the library, or the button that sets the image by specifying the resource index from the R library file, we will get the error:

image

As we can see the error indicates that the index assigned in the R.java file added to our extension is incorrect. Now let's use the second button which will use the index from the R file of our application, which was generated by aapt2:

image

This index is valid and the image has been displayed.

So the conclusion is that the index of our drawable in the R file of the library should be the same as the index in the R file of the application. But the index cannot be predicted in advance, so we cannot save it in the R.java file.

Probably the R file for the library should be generated automatically during compilation by aapt2, with the correct index. Aapt2 generates R files for the androidx libraries that the app inventor has, thanks to the R.txt files placed in the appropriate paths.

Now the question is what should the extension do to achieve the same as the built-in aar libraries.

Project AIA:

testRes.aia.zip

@ewpatton ewpatton added this to the nb201 milestone Jan 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Request to add a resource. Add UsesXml annotation
3 participants