-
-
Notifications
You must be signed in to change notification settings - Fork 331
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fixes: #3550 ; Add hello-world Android Java Example using Mill
- Loading branch information
himanshumahajan138
authored and
himanshumahajan138
committed
Oct 3, 2024
1 parent
e0a2c93
commit d6bcf85
Showing
6 changed files
with
548 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
13
example/javalib/android/1-hello-world/src/main/AndroidManifest.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
36 changes: 36 additions & 0 deletions
36
example/javalib/android/1-hello-world/src/main/java/com/helloworld/app/MainActivity.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
262 changes: 262 additions & 0 deletions
262
scalalib/src/mill/javalib/android/AndroidAppModule.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. | ||
} | ||
} |
Oops, something went wrong.