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

Android Studio Integration: Attempt via bsp for Java android apps #4362

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import android.content.Context;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.platform.app.InstrumentationRegistry;
import com.helloworld.SampleLogic;
import org.junit.Test;
import org.junit.runner.RunWith;

Expand All @@ -20,5 +21,6 @@ public void useAppContext() {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
assertEquals("com.helloworld.app", appContext.getPackageName());
assertEquals(32.0f, SampleLogic.textSize(), 0.0001f);
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.helloworld;

class SampleLogic {
public class SampleLogic {

public static float textSize() {
return 32f;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import static org.junit.Assert.*;

import com.helloworld.app.R;
import org.junit.Test;

/**
Expand All @@ -12,6 +13,8 @@
public class ExampleUnitTest {
@Test
public void textSize_isCorrect() {

assertEquals(32f, SampleLogic.textSize(), 0.000001f);
assertEquals(0x7f010000, R.color.text_green);
}
}
3 changes: 1 addition & 2 deletions example/android/javalib/1-hello-world/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ package build
import mill._, javalib._

import mill.javalib.android.{AndroidAppModule, AndroidSdkModule}
import coursier.maven.MavenRepository
import mill.javalib.android.AndroidTestModule

// Create and configure an Android SDK module to manage Android SDK paths and tools.
Expand Down Expand Up @@ -107,7 +106,7 @@ object app extends AndroidAppModule {
/** Usage

> ./mill show app.test
...compiling 3 Java sources...
...compiling 1 Java source...

> cat out/app/test/test.dest/out.json
["",[{"fullyQualifiedName":"com.helloworld.ExampleUnitTest.textSize_isCorrect","selector":"com.helloworld.ExampleUnitTest.textSize_isCorrect","duration":...,"status":"Success"}]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.helloworld.app

import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.helloworld.SampleLogic
import com.helloworld.SampleLogicInKotlinDir
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
Expand All @@ -18,5 +20,7 @@ class ExampleInstrumentedTest {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.helloworld.app", appContext.packageName)
assertEquals(32f, SampleLogic.textSize(), 0.0001f)
assertEquals(64f, SampleLogicInKotlinDir.textSize(), 0.00001f)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.os.Bundle
import android.view.Gravity
import android.view.ViewGroup.LayoutParams
import android.widget.TextView
import com.helloworld.SampleLogic

class MainActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
Expand All @@ -17,7 +18,7 @@ class MainActivity : Activity() {
textView.text = getString(R.string.hello_world)

// Set text size
textView.textSize = 32f
textView.textSize = SampleLogic.textSize()

// Center the text within the view
textView.gravity = Gravity.CENTER
Expand Down
2 changes: 1 addition & 1 deletion example/android/kotlinlib/1-hello-kotlin/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ object app extends AndroidAppKotlinModule {
/** Usage

> ./mill show app.test
...Compiling 5 Kotlin sources...
...Compiling 2 Kotlin sources...

> cat out/app/test/test.dest/out.json
["",[{"fullyQualifiedName":"com.helloworld.ExampleUnitTest.text_size_is_correct","selector":"com.helloworld.ExampleUnitTest.text_size_is_correct","duration":...,"status":"Success"},{"fullyQualifiedName":"com.helloworld.ExampleUnitTestInKotlinDir.kotlin_dir_text_size_is_correct","selector":"com.helloworld.ExampleUnitTestInKotlinDir.kotlin_dir_text_size_is_correct","duration":...,"status":"Success"}]]
Expand Down
1 change: 0 additions & 1 deletion example/android/kotlinlib/2-compose/build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ package build

import mill._
import kotlinlib._
import coursier.{Repository, MavenRepository}
import coursier.core.{MinimizedExclusions, ModuleName, Organization}
import coursier.params.ResolutionParams
import mill.kotlinlib.android.AndroidAppKotlinModule
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import mill.javalib.android.AndroidAppModule
trait AndroidAppKotlinModule extends AndroidAppModule with KotlinModule { outer =>

override def sources: T[Seq[PathRef]] =
super.sources() :+ PathRef(millSourcePath / "src/main/kotlin")
super[AndroidAppModule].sources() :+ PathRef(millSourcePath / "src/main/kotlin")

override def kotlincOptions = super.kotlincOptions() ++ {
if (androidEnableCompose()) {
Expand Down Expand Up @@ -54,7 +54,7 @@ trait AndroidAppKotlinModule extends AndroidAppModule with KotlinModule { outer

trait AndroidAppKotlinTests extends AndroidAppTests with KotlinTests {
override def sources: T[Seq[PathRef]] =
super.sources() :+ PathRef(outer.millSourcePath / "src/test/kotlin")
super[AndroidAppTests].sources() ++ Seq(PathRef(outer.millSourcePath / "src/test/kotlin"))
}

trait AndroidAppKotlinInstrumentedTests extends AndroidAppKotlinModule
Expand All @@ -64,6 +64,8 @@ trait AndroidAppKotlinModule extends AndroidAppModule with KotlinModule { outer
override final def androidSdkModule = outer.androidSdkModule

override def sources: T[Seq[PathRef]] =
super.sources() :+ PathRef(outer.millSourcePath / "src/androidTest/kotlin")
super[AndroidAppInstrumentedTests].sources() :+ PathRef(
outer.millSourcePath / "src/androidTest/kotlin"
)
}
}
2 changes: 1 addition & 1 deletion scalalib/src/mill/javalib/android/AndroidAppBundle.scala
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ trait AndroidAppBundle extends AndroidAppModule with JavaModule {
*/
def androidBundleZip: T[PathRef] = Task {
val dexFile = androidDex().path
val resFile = androidResources().path / "res.apk"
val resFile = androidResources()._1.path / "res.apk"
val baseDir = Task.dest / "base"
val appDir = Task.dest / "app"

Expand Down
105 changes: 81 additions & 24 deletions scalalib/src/mill/javalib/android/AndroidAppModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ package mill.javalib.android
import coursier.Repository
import mill._
import mill.scalalib._
import mill.api.{Logger, PathRef}
import mill.define.ModuleRef
import mill.api.{Logger, PathRef, internal}
import mill.define.{ModuleRef, Task}
import mill.scalalib.bsp.BspBuildTarget
import mill.testrunner.TestResult
import mill.util.Jvm
import os.RelPath
Expand Down Expand Up @@ -266,9 +267,20 @@ trait AndroidAppModule extends JavaModule {
*/
override def resources: T[Seq[PathRef]] = Task {
val libResFolders = androidUnpackArchives().flatMap(_.resources)
super.resources() ++ libResFolders :+ PathRef(millSourcePath / "src/main/res")
libResFolders :+ PathRef(millSourcePath / "src/main/res")
}

@internal
override def bspCompileClasspath: T[Agg[UnresolvedPath]] = Task {
compileClasspath().map(_.path).map(UnresolvedPath.ResolvedPath(_))
}

@internal
override def bspBuildTarget: BspBuildTarget = super.bspBuildTarget.copy(
baseDirectory = Some(millSourcePath / "src/main"),
tags = Seq("application")
)

/**
* Replaces AAR files in classpath with their extracted JARs.
*/
Expand All @@ -278,29 +290,42 @@ trait AndroidAppModule extends JavaModule {
super.resolvedRunIvyDeps().filter(_.path.ext == "jar")
// TODO process metadata shipped with Android libs. It can have some rules with Target SDK, for example.
// TODO support baseline profiles shipped with Android libs.
super.compileClasspath().filter(_.path.ext == "jar") ++ jarFiles
super.compileClasspath().filter(_.path.ext != "aar") ++ jarFiles
}

/**
* Specifies AAPT options for Android resource compilation.
*/
def androidAaptOptions: T[Seq[String]] = Task { Seq("--auto-add-overlay") }

def androidTransitiveResources: Target[Seq[PathRef]] = Task {
T.traverse(transitiveModuleCompileModuleDeps) { m =>
Task.Anon(m.resources())
}().flatten
}

/**
* Compiles Android resources and generates `R.java` and `res.apk`.
*
* @return [[PathRef]] to the directory with application's `R.java` and `res.apk`.
* @return [[(PathRef, Seq[PathRef]]] to the directory with application's `R.java` and `res.apk` and a list
* of the zip files containing the resource compiled files
*
* For more details on the aapt2 tool, refer to:
* [[https://developer.android.com/tools/aapt2 aapt Documentation]]
*/
def androidResources: T[PathRef] = Task {
def androidResources: T[(PathRef, Seq[PathRef])] = Task {
val rClassDir = T.dest / rClassDirName
val compiledResDir = T.dest / compiledResourcesDirName
os.makeDir(compiledResDir)
val compiledResources = collection.mutable.Buffer[os.Path]()

for (resDir <- resources().map(_.path).filter(os.exists)) {
val transitiveResources = androidTransitiveResources().map(_.path).filter(os.exists)

val localResources = resources().map(_.path).filter(os.exists)

val allResources = localResources ++ transitiveResources

for (resDir <- allResources) {
val segmentsSeq = resDir.segments.toSeq
val libraryName = segmentsSeq.dropRight(1).last
// if not directory, but file, it should have one of the possible extensions: .flata, .zip, .jar, .jack,
Expand Down Expand Up @@ -382,7 +407,7 @@ trait AndroidAppModule extends JavaModule {

os.call(appLinkArgs)

PathRef(T.dest)
(PathRef(T.dest), compiledResources.toSeq.map(PathRef(_)))
}

/**
Expand All @@ -397,7 +422,7 @@ trait AndroidAppModule extends JavaModule {
* the Android resource generation step.
*/
override def generatedSources: T[Seq[PathRef]] = Task {
super.generatedSources() ++ Seq(androidResources()) ++ Seq(androidLibsRClasses())
Seq(PathRef(androidResources()._1.path / rClassDirName)) ++ androidLibsRClasses()
}

/**
Expand All @@ -406,12 +431,20 @@ trait AndroidAppModule extends JavaModule {
* @return os.Path to the Generated DEX File Directory
*/
def androidDex: T[PathRef] = Task {

val inheritedClassFiles = compileClasspath().map(_.path).filter(os.isDir)
.flatMap(os.walk(_))
.filter(os.isFile)
.filter(_.ext == "class")
.map(_.toString())

val appCompiledFiles = os.walk(compile().classes.path)
.filter(_.ext == "class")
.map(_.toString)
.map(_.toString) ++ inheritedClassFiles

val libsJarFiles = compileClasspath()
.filter(_ != androidSdkModule().androidJarPath())
.filter(_.path.ext == "jar")
.map(_.path.toString())

val proguardFile = T.dest / "proguard-rules.pro"
Expand All @@ -421,13 +454,14 @@ trait AndroidAppModule extends JavaModule {
.flatMap(_.proguardRules)
.map(p => os.read(p.path))
.appendedAll(mainDexPlatformRules)
.appended(os.read(androidResources().path / "main-dex-rules.pro"))
.appended(os.read(androidResources()._1.path / "main-dex-rules.pro"))
.mkString("\n")
os.write(proguardFile, knownProguardRules)

val d8ArgsBuilder = Seq.newBuilder[String]

d8ArgsBuilder += androidSdkModule().d8Path().path.toString

if (androidIsDebug()) {
d8ArgsBuilder += "--debug"
} else {
Expand Down Expand Up @@ -462,7 +496,7 @@ trait AndroidAppModule extends JavaModule {
def androidUnsignedApk: T[PathRef] = Task {
val unsignedApk = Task.dest / "app.unsigned.apk"

os.copy(androidResources().path / "res.apk", unsignedApk)
os.copy(androidResources()._1.path / "res.apk", unsignedApk)
val dexFiles = os.walk(androidDex().path)
.filter(_.ext == "dex")
.map(os.zip.ZipSource.fromPath)
Expand Down Expand Up @@ -650,29 +684,33 @@ trait AndroidAppModule extends JavaModule {
PathRef(signedApk)
}

def androidLibsRClasses: T[PathRef] = Task {
def androidLibsRClasses: T[Seq[PathRef]] = Task {
// TODO do this better
// So we have application R.java class generated by aapt2 link, which includes IDs of app resources + libs resources.
// But we also need to have R.java classes for libraries. The process below is quite hacky and inefficient, because:
// * it will generate R.java for the library even library has no resources declared
// * R.java will have not only resource ID from this library, but from other libraries as well. They should be stripped.
val mainRClassPath = os.walk(androidResources().path / rClassDirName)
val rClassDir = androidResources()._1.path / rClassDirName
val mainRClassPath = os.walk(rClassDir)
.find(_.last == "R.java")
.get

val mainRClass = os.read(mainRClassPath)
val libsPackages = androidUnpackArchives()
.flatMap(_.manifest)
.map(f => ((XML.loadFile(f.path.toIO) \\ "manifest").head \ "@package").head.toString())

for (libPackage <- libsPackages) {
val libRClassPath = T.dest / libPackage.split('.') / "R.java"
os.write(
val libClasses: Seq[PathRef] = for {
libPackage <- libsPackages
libRClassPath = T.dest / libPackage.split('.') / "R.java"
_ = os.write(
libRClassPath,
mainRClass.replaceAll("package .+;", s"package $libPackage;"),
createFolders = true
)
}
PathRef(T.dest)
} yield PathRef(libRClassPath)

libClasses :+ PathRef(mainRClassPath)
}

/**
Expand Down Expand Up @@ -1007,15 +1045,23 @@ trait AndroidAppModule extends JavaModule {
trait AndroidAppTests extends JavaTests {
private def testPath = parent.millSourcePath / "src/test"

override def sources: T[Seq[PathRef]] = parent.sources() :+ PathRef(testPath / "java")
override def sources: T[Seq[PathRef]] = Seq(PathRef(testPath / "java"))

override def resources: T[Seq[PathRef]] = Task.Sources(Seq(PathRef(testPath / "res")))

override def bspBuildTarget: BspBuildTarget = super.bspBuildTarget.copy(
baseDirectory = Some(testPath),
canTest = true
)

override def resources: T[Seq[PathRef]] = parent.resources() :+ PathRef(testPath / "res")
}

trait AndroidAppInstrumentedTests extends AndroidAppModule with AndroidTestModule {
private def androidMainSourcePath = parent.millSourcePath
private def androidTestPath = androidMainSourcePath / "src/androidTest"

override def moduleDeps: Seq[JavaModule] = Seq(parent)

override def androidCompileSdk: T[Int] = parent.androidCompileSdk
override def androidMinSdk: T[Int] = parent.androidMinSdk
override def androidTargetSdk: T[Int] = parent.androidTargetSdk
Expand All @@ -1030,11 +1076,15 @@ trait AndroidAppModule extends JavaModule {

override def androidEmulatorPort: String = parent.androidEmulatorPort

override def sources: T[Seq[PathRef]] = parent.sources() :+ PathRef(androidTestPath / "java")
override def sources: T[Seq[PathRef]] = Seq(PathRef(androidTestPath / "java"))
Copy link
Contributor

Choose a reason for hiding this comment

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

This is a good change, but still, the way instrumented testing is implemented right now is different from the way AGP does it.

The current approach is to generate a single fat APK, consisting of production + test files.

AGP is doing it in a different way: it will be 2 APKs, one is a normal production APK, another one will contain only test files and will also have a dedicated manifest obviously, with a dedicated package name attribute which will be ${main app package name}.test. And this test APK should be installed with -t option (see here).

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, you are right, I have decompiled the APKs from AGP as well and can work towards this if we want to have the exact same behaviour (which makes sense to have)

image

I want to set up a scaffold with some tests and examples working first and then do the underlying implementations on the next stage.


/** The resources in res directories of both main source and androidTest sources */
override def resources: T[Seq[PathRef]] =
super.resources() ++ parent.resources() :+ PathRef(androidTestPath / "res")
override def resources: T[Seq[PathRef]] = Task.Sources {
val libResFolders = androidUnpackArchives().flatMap(_.resources)
libResFolders ++ Seq(PathRef(androidTestPath / "res"))
}

override def generatedSources: T[Seq[PathRef]] = Task.Sources(Seq.empty[PathRef])

/* TODO on debug work, an AndroidManifest.xml with debug and instrumentation settings
* will need to be created. Then this needs to point to the location of that debug
Expand Down Expand Up @@ -1095,6 +1145,13 @@ trait AndroidAppModule extends JavaModule {

/** Builds the apk including the integration tests (e.g. from androidTest) */
def androidInstantApk: T[PathRef] = androidApk

@internal
override def bspBuildTarget: BspBuildTarget = super[AndroidTestModule].bspBuildTarget.copy(
baseDirectory = Some(androidTestPath),
canRun = false
)

}

}
Expand Down
2 changes: 1 addition & 1 deletion scalalib/src/mill/javalib/android/AndroidTestModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import mill.T
import mill.scalalib.TestModule

@mill.api.experimental
trait AndroidTestModule extends TestModule
trait AndroidTestModule extends TestModule {}

@mill.api.experimental
object AndroidTestModule {
Expand Down
Loading