-
Notifications
You must be signed in to change notification settings - Fork 0
Manifest Editors
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.
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.
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.
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.