Skip to content

Commit

Permalink
Fixes: #3550 ; Add hello-world Android Java Example using Mill
Browse files Browse the repository at this point in the history
  • Loading branch information
himanshumahajan138 authored and himanshumahajan138 committed Oct 3, 2024
1 parent e0a2c93 commit d6bcf85
Show file tree
Hide file tree
Showing 6 changed files with 548 additions and 0 deletions.
63 changes: 63 additions & 0 deletions example/javalib/android/1-hello-world/build.mill
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// This Example demonstrates a simple "Hello World"
// Android application built using the [Mill build tool](https://www.lihaoyi.com/mill/).

//// SNIPPET:BUILD
package build

import mill._
import mill.javalib.android.AndroidAppModule

// Defines an Android app build module using Mill, extending AndroidAppModule.
object App extends AndroidAppModule {

// Default project root to be one level up from the current millSourcePath
def projectRoot: T[os.Path] = T {
os.Path(millSourcePath.toString.replace("App", ""))
}
// The name of the Android application, default is "HelloWorld".
def appName: T[String] = T { "HelloWorld" }
}

////SNIPPET:END

/** Usage

> ./mill App.createApp

*/

// This is a basic Mill build for creating Simple `Hello-World` Android Application
// which extends `AndroidAppModule` for all Andorid Application related tasks,
// here we can alter the default values of `projectRoot`
// (Which Contains All Files required for Android Application Creation) and
// `appName` (responsible For Final Application Name).
// User can Change these values according to their need and the Project Folder Structure
// for this would look something like this:
//
// ----
// .
// ├── build.mill
// └── src
// └── main
// ├── AndroidManifest.xml
// └── java
// └── com
// └── helloworld
// └── app
// └── MainActivity.java
//
// ----
//
//// SNIPPET:Modules
//
// This example project uses `AndroidAppModule` and this Module further depends on two Modules
// `AndroidSdkModule`(For Installing Andorid SDK and Tools Required to create Android Application)
// and `JavaModule` (For Using Java Commands for Compiling, Creating Jars and common java tasks)
// all the code works in a flow from installing Android SDK to creating Android Application.

// Users can Perform Manual Testing to insure Proper Functioning of the Application and Automatic
// Testing Procedure is still under Observance once Approved, will be shared.

// Mill Build Tool already have Advantages over the other Build Tools and
// having the support for Android Application with Mill Build Tool Provdies Efficieny and
// Optimisation to Android Application Creation Process.
13 changes: 13 additions & 0 deletions example/javalib/android/1-hello-world/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.helloworld.app">
<uses-sdk android:minSdkVersion="9"/>
<uses-sdk android:targetSdkVersion="35"/>
<application android:label="Hello World" android:debuggable="true">
<activity android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.helloworld.app;

import android.app.Activity;
import android.os.Bundle;
import android.view.View;
import android.widget.TextView;
import android.view.ViewGroup.LayoutParams;
import android.view.Gravity;


public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);

// Create a new TextView
TextView textView = new TextView(this);

// Set the text to "Hello, World!"
textView.setText("Hello, World!");

// Set text size
textView.setTextSize(32);

// Center the text within the view
textView.setGravity(Gravity.CENTER);

// Set layout parameters (width and height)
textView.setLayoutParams(new LayoutParams(
LayoutParams.MATCH_PARENT,
LayoutParams.MATCH_PARENT));

// Set the content view to display the TextView
setContentView(textView);
}
}
1 change: 1 addition & 0 deletions example/package.mill
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ object `package` extends RootModule with Module {
.collect { case m: ExampleCrossModule => m }

object javalib extends Module {
object android extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "android"))
object basic extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "basic"))
object builds extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "builds"))
object testing extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "testing"))
Expand Down
262 changes: 262 additions & 0 deletions scalalib/src/mill/javalib/android/AndroidAppModule.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package mill.javalib.android

import mill._
import mill.api.PathRef
import mill.scalalib.JavaModule
import mill.javalib.android.AndroidSdkModule
import mill.util.Jvm

/**
* Trait for building Android applications using Mill,
* this extends [[AndroidSdkModule]] for Android SDK related tasks,
* and [[JavaModule]] for Java related tasks.
*
* This trait outlines the steps necessary to build an Android application:
* 1. Compile Java code into `.class` files.
* 2. Package the `.class` files into a JAR file.
* 3. Convert the JAR into DEX format for Android.
* 4. Package DEX files and resources into an APK.
* 5. Optimize the APK using zipalign.
* 6. Sign the APK for distribution.
*
* For detailed information, refer to Mill's [documentation](https://com-lihaoyi.github.io/mill),
* and the [Android developer guide](https://developer.android.com/studio).
*/
trait AndroidAppModule extends AndroidSdkModule with JavaModule {

/**
* Path where the Project related Files will live.
*
* @return A `PathRef` representing project directory.
*/
def projectRoot: T[os.Path] = T {
os.Path(millSourcePath.toString.replace(
"App",
""
)) // Get the parent directory of millSourcePath
}

/**
* App Name for the Application default is HelloWorld.
*
* @return A string representing the platform version.
*/
def appName: T[String] = T { "HelloWorld" }

/**
* Step 1: Compile Java source files to `.class` files.
*
* This method:
* - Ensures the Android SDK is installed.
* - Compiles all `.java` files in `src/main/java` to `.class` files stored in `obj/` directory.
*
* @return A `PathRef` to the directory containing the compiled `.class` files.
*
* @see [[createJar]]
*/
def compileJava: T[PathRef] = T {
installAndroidSdk() // Step 1: Install the Android SDK if not already done.
val outputDir = T.dest / "obj" // Directory to store compiled class files.

os.call(
Seq(
Jvm.jdkTool("javac"), // Use the Java compiler
"-classpath",
androidJarPath().path.toString, // Include Android framework classes
"-d",
outputDir.toString // Specify output directory for class files
) ++ os.walk(projectRoot() / "src/main/java").filter(_.ext == "java").map(
_.toString
) // Get all Java source files
)

PathRef(outputDir) // Return the path to compiled class files.
}

/**
* Step 2: Package `.class` files into a JAR file.
*
* This method:
* - Converts the compiled `.class` files into a JAR file using the `d8` tool.
*
* @return A `PathRef` to the generated JAR file.
*
* @see [[compileJava]]
* @see [[createDex]]
*/
def createJar: T[PathRef] = T {
val jarFile = T.dest / "my_classes.jar" // Specify output JAR file name.

os.call(
Seq(
d8Path().path.toString, // Path to the D8 tool
"--output",
jarFile.toString, // Output JAR file
"--no-desugaring" // Do not apply desugaring
) ++ os.walk(compileJava().path).filter(_.ext == "class").map(
_.toString
) // Get compiled class files from compileJava
)

PathRef(jarFile) // Return the path to the created JAR file.
}

/**
* Step 3: Convert the JAR file into a DEX file.
*
* This method:
* - Uses the `d8` tool to convert the JAR file into DEX format, required for Android apps.
*
* @return A `PathRef` to the generated DEX file.
*
* @see [[createJar]]
*/
def createDex: T[PathRef] = T {
val dexOutputDir = T.dest // Directory to store DEX files.

os.call(
Seq(d8Path().path.toString, "--output", dexOutputDir.toString) ++ Seq(
createJar().path.toString, // Use the JAR file from createJar
androidJarPath().path.toString // Include Android framework classes
)
)

PathRef(dexOutputDir) // Return the path to the generated DEX file.
}

/**
* Step 4: Package the DEX file into an unsigned APK.
*
* This method:
* - Uses the `aapt` tool to create an APK file that includes the DEX file and resources.
*
* @return A `PathRef` to the unsigned APK file.
*
* @see [[createDex]]
*/
def createApk: T[PathRef] = T {
val unsignedApk =
T.dest / s"${appName().toString}.unsigned.apk" // Specify output APK file name.

os.call(
Seq(
aaptPath().path.toString,
"package", // Command to package APK
"-f", // Force overwrite
"-M",
(projectRoot() / "src/main/AndroidManifest.xml").toString, // Path to the AndroidManifest.xml
"-I",
androidJarPath().path.toString, // Include Android framework resources
"-F",
unsignedApk.toString // Specify output APK file
) ++ Seq(createDex().path.toString) // Include the DEX file from createDex
)

PathRef(unsignedApk) // Return the path to the unsigned APK.
}

/**
* Step 5: Optimize the APK using zipalign.
*
* This method:
* - Takes the unsigned APK and optimizes it for better performance on Android devices.
*
* @return A `PathRef` to the aligned APK file.
*
* @see [[createApk]]
*/
def alignApk: T[PathRef] = T {
val alignedApk =
T.dest / s"${appName().toString}.aligned.apk" // Specify output aligned APK file name.

os.call(
Seq(
zipalignPath().path.toString, // Path to the zipalign tool
"-f",
"-p",
"4", // Force overwrite and align with a page size of 4
createApk().path.toString, // Use the unsigned APK from createApk
alignedApk.toString // Specify output aligned APK file
)
)

PathRef(alignedApk) // Return the path to the aligned APK.
}

/**
* Step 6: Sign the APK using a keystore.
*
* This method:
* - Signs the aligned APK with a keystore. If the keystore does not exist, it generates one.
*
* @return A `PathRef` to the signed APK file.
*
* @see [[alignApk]]
* @see [[createKeystore]]
*/
def createApp: T[PathRef] = T {
val signedApk =
projectRoot() / s"${appName().toString}.apk" // Specify output signed APK file name.

os.call(
Seq(
apksignerPath().path.toString,
"sign", // Command to sign APK
"--ks",
createKeystore().path.toString, // Use the keystore from createKeystore
"--ks-key-alias",
"androidkey", // Alias for the key
"--ks-pass",
"pass:android", // Keystore password
"--key-pass",
"pass:android", // Key password
"--out",
signedApk.toString, // Specify output signed APK file
alignApk().path.toString // Use the aligned APK from alignApk
)
)

PathRef(signedApk) // Return the path to the signed APK.
}

/**
* Creates a keystore for signing APKs if it doesn't already exist.
*
* This method:
* - Generates a keystore file using the `keytool` command.
*
* @return A `PathRef` to the keystore file.
*
* @see [[createApp]]
*/
def createKeystore: T[PathRef] = T {
val keystoreFile = T.dest / "keystore.jks" // Specify keystore file name.

if (!os.exists(keystoreFile)) {
os.call(
Seq(
"keytool",
"-genkeypair",
"-keystore",
keystoreFile.toString, // Generate keystore
"-alias",
"androidkey", // Key alias
"-dname",
"CN=MILL, OU=MILL, O=MILL, L=MILL, S=MILL, C=IN", // Distinguished name
"-validity",
"10000", // Validity period in days
"-keyalg",
"RSA", // Key algorithm
"-keysize",
"2048", // Key size
"-storepass",
"android", // Keystore password
"-keypass",
"android" // Key password
)
)
}

PathRef(keystoreFile) // Return the path to the keystore file.
}
}
Loading

0 comments on commit d6bcf85

Please sign in to comment.