Skip to content

Manifest Editors

Ben Nordick edited this page Jun 27, 2020 · 4 revisions

Now that we've seen how to replace an Android activity with a provided version, let's have the student create a new activity that should open on app startup.

Welcome activity

In the main app project, create a new Empty Activity in the com.example.mp package called WelcomeActivity. Do not check the Launcher Activity box. We're not going to go too overboard with this activity; just throw around some views:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".WelcomeActivity">

    <LinearLayout
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Math for unclear purposes" />

        <Button
            android:id="@+id/goAccumulate"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Let's Accumulate" />
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

And fill in onCreate to make the button do something:

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_welcome);
    
    findViewById(R.id.goAccumulate).setOnClickListener(
            unused -> startActivity(new Intent(this, MainActivity.class)));
}

Run the emulator and notice that the app still launches to the multiply-accumulate activity. You are welcome to write a test to check the startup activity or create a provided AAR for this activity if you like. But even if the provided Android library gives its version of the activity an <intent-filter> to make it the startup activity, the student's MainActivity might still also have an intent filter. We therefore need to provide a change to the manifest.

Manifest editor JAR

When compiling an Android app, Android Gradle assembles a final manifest from the AndroidManifest.xml file in the main sources plus the manifests of Android library dependencies plus some information from the buildscript. eMPire allows you to register manifest editors that manipulate Android Gradle's final manifest XML before the app runs. You can write arbitrary Java code that will be loaded into the Gradle process and passed the XML document to do whatever you like with. To be clear, none of that code will run during testing, only at build time, so you can't call any app functions.

Create another new IntelliJ project called welcome-manifest. Again make sure to adjust the buildscript to target Java 8 for compatibility with all modern Gradle processes. I recommend against having any extra dependencies in manifest editors. If you do add dependencies, you'll either need to shade them into your JAR or stick to ones already available in the Gradle process, e.g. those used by eMPire itself. Fortunately org.w3c.dom is in the Java standard library and should be able to take care of all your XML processing needs.

Create a Java class called WelcomeManifestEditor directly inside the main java folder. In it, define a public static void method, let's say editManifest, that takes a Document (from the org.w3c.dom package). The method name can be different as long as the eMPire configuration matches, but the argument list must consist exactly of one Document. This is the function that eMPire will call and pass the processed manifest. Any return value of your function will be ignored—you must modify the Document in-place. This code removes any existing <intent-filter> from the MainActivity or WelcomeActivity activity declarations, then adds a main/launcher intent filter to WelcomeActivity:

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

@SuppressWarnings("unused")
public class WelcomeManifestEditor {

    private static final String PACKAGE = "com.example.mp";

    public static void editManifest(Document manifest) {
        Element manifestElem = manifest.getDocumentElement();
        Element applicationElem = (Element) manifestElem.getElementsByTagName("application").item(0);
        NodeList activities = applicationElem.getElementsByTagName("activity");
        for (int i = 0; i < activities.getLength(); i++) {
            Element activityElem = (Element) activities.item(i);
            switch (activityElem.getAttribute("android:name")) {
                case PACKAGE + ".MainActivity":
                    removeIntentFilter(activityElem);
                    break;
                case PACKAGE + ".WelcomeActivity":
                    removeIntentFilter(activityElem);
                    Element intentFilterElem = manifest.createElement("intent-filter");
                    Element actionElem = manifest.createElement("action");
                    actionElem.setAttribute("android:name", "android.intent.action.MAIN");
                    intentFilterElem.appendChild(actionElem);
                    Element categoryElem = manifest.createElement("category");
                    categoryElem.setAttribute("android:name", "android.intent.category.LAUNCHER");
                    intentFilterElem.appendChild(categoryElem);
                    activityElem.appendChild(intentFilterElem);
            }
        }
    }

    private static void removeIntentFilter(Element activityElem) {
        NodeList intentFilters = activityElem.getElementsByTagName("intent-filter");
        if (intentFilters.getLength() > 0) {
            activityElem.removeChild(intentFilters.item(0));
        }
    }

}

As you can see, it is fine to declare fields or helper functions. You can even write additional classes if you need.

Let's look carefully at something interesting. The switch statement is looking for fully-qualified (prefixed with the package) activity class names in android:name attributes, but if you look in the app's AndroidManifest.xml, those attributes' values are short names relative to the app package. Android Gradle expands them when processing the manifest and it is that processed manifest that eMPire gives you.

Registering manifest editors

Now that we have our manifest editing code, compile the project as usual with the jar task. Copy the result into the app's provided folder, renaming it manifest-welcome.jar. We'll register a segment for it with a manifestEditor directive:

register("welcome") {
    manifestEditor("manifest-welcome.jar", "WelcomeManifestEditor", "editManifest")
    // any addAars or removeClasses directives you added if providing WelcomeActivity
}

The parameters to manifestEditor are the filename (with extension) of your manifest editor, the name of the class holding the editor method, then the name of the static editor method itself. If you have multiple manifest editors active for one checkpoint, the order in which they run is not specified. They must be independent.

Add this welcome segment to the 1 and demo checkpoints, update the student YAML file to use provided components from one of those checkpoints, and sync. Run the "app" run configuration to install the updated app on the emulator. Android Studio is confused by this build-time rewriting of the manifest, so it won't launch the app on the emulator for you, but if you start the app from inside the virtual phone, you'll see the welcome screen!

The Gradle process may hold manifest editor JAR files open for a while even after the build finishes. If the file being in use prevents you from replacing it with a new version, you'll need to stop the Gradle daemon, either by restarting Android Studio or passing --stop to the gradlew script in the Terminal tab.

In the next chapter, we'll do even trickier things with Android activity classes.