Add support for JNI libraries to android_app modules

Make android_app modules a MultiTargets module, which means the
common variant will have a list of Targets that it needs to handle.
Collect JNI libraries for each Target, and package them into or
alongside the APK.

Bug: 80095087
Test: app_test.go
Change-Id: Iabd3921e1d4c4b4cfcc7e131a0b0d9ab83b0ebbb
diff --git a/java/androidmk.go b/java/androidmk.go
index 313a144..7a2fe1e 100644
--- a/java/androidmk.go
+++ b/java/androidmk.go
@@ -243,6 +243,10 @@
 				if len(app.appProperties.Overrides) > 0 {
 					fmt.Fprintln(w, "LOCAL_OVERRIDES_PACKAGES := "+strings.Join(app.appProperties.Overrides, " "))
 				}
+
+				for _, jniLib := range app.installJniLibs {
+					fmt.Fprintln(w, "LOCAL_SOONG_JNI_LIBS_"+jniLib.target.Arch.ArchType.String(), "+=", jniLib.name)
+				}
 			},
 		},
 	}
diff --git a/java/app.go b/java/app.go
index dc5296d..843ced6 100644
--- a/java/app.go
+++ b/java/app.go
@@ -19,9 +19,11 @@
 import (
 	"strings"
 
+	"github.com/google/blueprint"
 	"github.com/google/blueprint/proptools"
 
 	"android/soong/android"
+	"android/soong/cc"
 	"android/soong/tradefed"
 )
 
@@ -59,6 +61,11 @@
 	// binaries would be installed by default (in PRODUCT_PACKAGES) the other binary will be removed
 	// from PRODUCT_PACKAGES.
 	Overrides []string
+
+	// list of native libraries that will be provided in or alongside the resulting jar
+	Jni_libs []string `android:"arch_variant"`
+
+	EmbedJNI bool `blueprint:"mutated"`
 }
 
 type AndroidApp struct {
@@ -70,6 +77,8 @@
 	appProperties appProperties
 
 	extraLinkFlags []string
+
+	installJniLibs []jniLib
 }
 
 func (a *AndroidApp) ExportedProguardFlagFiles() android.Paths {
@@ -92,9 +101,21 @@
 
 func (a *AndroidApp) DepsMutator(ctx android.BottomUpMutatorContext) {
 	a.Module.deps(ctx)
+
 	if !Bool(a.properties.No_framework_libs) && !Bool(a.properties.No_standard_libs) {
 		a.aapt.deps(ctx, sdkContext(a))
 	}
+
+	for _, jniTarget := range ctx.MultiTargets() {
+		variation := []blueprint.Variation{
+			{Mutator: "arch", Variation: jniTarget.String()},
+			{Mutator: "link", Variation: "shared"},
+		}
+		tag := &jniDependencyTag{
+			target: jniTarget,
+		}
+		ctx.AddFarVariationDependencies(variation, tag, a.appProperties.Jni_libs...)
+	}
 }
 
 func (a *AndroidApp) GenerateAndroidBuildActions(ctx android.ModuleContext) {
@@ -178,7 +199,19 @@
 
 	packageFile := android.PathForModuleOut(ctx, "package.apk")
 
-	CreateAppPackage(ctx, packageFile, a.exportPackage, a.outputFile, certificates)
+	var jniJarFile android.WritablePath
+	jniLibs := a.collectJniDeps(ctx)
+	if len(jniLibs) > 0 {
+		embedJni := ctx.Config().UnbundledBuild() || a.appProperties.EmbedJNI
+		if embedJni {
+			jniJarFile = android.PathForModuleOut(ctx, "jnilibs.zip")
+			TransformJniLibsToJar(ctx, jniJarFile, jniLibs)
+		} else {
+			a.installJniLibs = jniLibs
+		}
+	}
+
+	CreateAppPackage(ctx, packageFile, a.exportPackage, jniJarFile, a.outputFile, certificates)
 
 	a.outputFile = packageFile
 
@@ -192,6 +225,35 @@
 	}
 }
 
+func (a *AndroidApp) collectJniDeps(ctx android.ModuleContext) []jniLib {
+	var jniLibs []jniLib
+
+	ctx.VisitDirectDeps(func(module android.Module) {
+		otherName := ctx.OtherModuleName(module)
+		tag := ctx.OtherModuleDependencyTag(module)
+
+		if jniTag, ok := tag.(*jniDependencyTag); ok {
+			if dep, ok := module.(*cc.Module); ok {
+				lib := dep.OutputFile()
+				if lib.Valid() {
+					jniLibs = append(jniLibs, jniLib{
+						name:   ctx.OtherModuleName(module),
+						path:   lib.Path(),
+						target: jniTag.target,
+					})
+				} else {
+					ctx.ModuleErrorf("dependency %q missing output file", otherName)
+				}
+			} else {
+				ctx.ModuleErrorf("jni_libs dependency %q must be a cc library", otherName)
+
+			}
+		}
+	})
+
+	return jniLibs
+}
+
 func AndroidAppFactory() android.Module {
 	module := &AndroidApp{}
 
@@ -212,7 +274,9 @@
 		return class == android.Device && ctx.Config().DevicePrefer32BitApps()
 	})
 
-	InitJavaModule(module, android.DeviceSupported)
+	android.InitAndroidMultiTargetsArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
+
 	return module
 }
 
@@ -258,6 +322,7 @@
 
 	module.Module.properties.Instrument = true
 	module.Module.properties.Installable = proptools.BoolPtr(true)
+	module.appProperties.EmbedJNI = true
 
 	module.AddProperties(
 		&module.Module.properties,
@@ -268,6 +333,7 @@
 		&module.appTestProperties,
 		&module.testProperties)
 
-	InitJavaModule(module, android.DeviceSupported)
+	android.InitAndroidMultiTargetsArchModule(module, android.DeviceSupported, android.MultilibCommon)
+	android.InitDefaultableModule(module)
 	return module
 }
diff --git a/java/app_builder.go b/java/app_builder.go
index e27b1b7..b9b5f43 100644
--- a/java/app_builder.go
+++ b/java/app_builder.go
@@ -19,9 +19,11 @@
 // functions.
 
 import (
+	"path/filepath"
 	"strings"
 
 	"github.com/google/blueprint"
+	"github.com/google/blueprint/proptools"
 
 	"android/soong/android"
 )
@@ -61,16 +63,18 @@
 	})
 
 func CreateAppPackage(ctx android.ModuleContext, outputFile android.WritablePath,
-	resJarFile, dexJarFile android.Path, certificates []certificate) {
-
-	// TODO(ccross): JNI libs
+	resJarFile, jniJarFile, dexJarFile android.Path, certificates []certificate) {
 
 	unsignedApk := android.PathForModuleOut(ctx, "unsigned.apk")
 
-	inputs := android.Paths{resJarFile}
+	var inputs android.Paths
 	if dexJarFile != nil {
 		inputs = append(inputs, dexJarFile)
 	}
+	inputs = append(inputs, resJarFile)
+	if jniJarFile != nil {
+		inputs = append(inputs, jniJarFile)
+	}
 
 	ctx.Build(pctx, android.BuildParams{
 		Rule:   combineApk,
@@ -132,3 +136,37 @@
 		},
 	})
 }
+
+func TransformJniLibsToJar(ctx android.ModuleContext, outputFile android.WritablePath,
+	jniLibs []jniLib) {
+
+	var deps android.Paths
+	jarArgs := []string{
+		"-j", // junk paths, they will be added back with -P arguments
+	}
+
+	if !ctx.Config().UnbundledBuild() {
+		jarArgs = append(jarArgs, "-L 0")
+	}
+
+	for _, j := range jniLibs {
+		deps = append(deps, j.path)
+		jarArgs = append(jarArgs,
+			"-P "+targetToJniDir(j.target),
+			"-f "+j.path.String())
+	}
+
+	ctx.Build(pctx, android.BuildParams{
+		Rule:        zip,
+		Description: "zip jni libs",
+		Output:      outputFile,
+		Implicits:   deps,
+		Args: map[string]string{
+			"jarArgs": strings.Join(proptools.NinjaAndShellEscape(jarArgs), " "),
+		},
+	})
+}
+
+func targetToJniDir(target android.Target) string {
+	return filepath.Join("lib", target.Arch.Abi[0])
+}
diff --git a/java/app_test.go b/java/app_test.go
index c7c94ec..f6476dc 100644
--- a/java/app_test.go
+++ b/java/app_test.go
@@ -17,6 +17,7 @@
 import (
 	"android/soong/android"
 	"fmt"
+	"path/filepath"
 	"reflect"
 	"sort"
 	"strings"
@@ -338,3 +339,118 @@
 		}
 	}
 }
+
+func TestJNI(t *testing.T) {
+	ctx := testJava(t, `
+		toolchain_library {
+			name: "libcompiler_rt-extras",
+			src: "",
+		}
+
+		toolchain_library {
+			name: "libatomic",
+			src: "",
+		}
+
+		toolchain_library {
+			name: "libgcc",
+			src: "",
+		}
+
+		toolchain_library {
+			name: "libclang_rt.builtins-aarch64-android",
+			src: "",
+		}
+
+		toolchain_library {
+			name: "libclang_rt.builtins-arm-android",
+			src: "",
+		}
+
+		cc_object {
+			name: "crtbegin_so",
+			stl: "none",
+		}
+
+		cc_object {
+			name: "crtend_so",
+			stl: "none",
+		}
+
+		cc_library {
+			name: "libjni",
+			system_shared_libs: [],
+			stl: "none",
+		}
+
+		android_test {
+			name: "test",
+			no_framework_libs: true,
+			jni_libs: ["libjni"],
+		}
+
+		android_test {
+			name: "test_first",
+			no_framework_libs: true,
+			compile_multilib: "first",
+			jni_libs: ["libjni"],
+		}
+
+		android_test {
+			name: "test_both",
+			no_framework_libs: true,
+			compile_multilib: "both",
+			jni_libs: ["libjni"],
+		}
+
+		android_test {
+			name: "test_32",
+			no_framework_libs: true,
+			compile_multilib: "32",
+			jni_libs: ["libjni"],
+		}
+
+		android_test {
+			name: "test_64",
+			no_framework_libs: true,
+			compile_multilib: "64",
+			jni_libs: ["libjni"],
+		}
+		`)
+
+	// check the existence of the internal modules
+	ctx.ModuleForTests("test", "android_common")
+	ctx.ModuleForTests("test_first", "android_common")
+	ctx.ModuleForTests("test_both", "android_common")
+	ctx.ModuleForTests("test_32", "android_common")
+	ctx.ModuleForTests("test_64", "android_common")
+
+	testCases := []struct {
+		name string
+		abis []string
+	}{
+		{"test", []string{"arm64-v8a"}},
+		{"test_first", []string{"arm64-v8a"}},
+		{"test_both", []string{"arm64-v8a", "armeabi-v7a"}},
+		{"test_32", []string{"armeabi-v7a"}},
+		{"test_64", []string{"arm64-v8a"}},
+	}
+
+	for _, test := range testCases {
+		t.Run(test.name, func(t *testing.T) {
+			app := ctx.ModuleForTests(test.name, "android_common")
+			jniLibZip := app.Output("jnilibs.zip")
+			var abis []string
+			args := strings.Fields(jniLibZip.Args["jarArgs"])
+			for i := 0; i < len(args); i++ {
+				if args[i] == "-P" {
+					abis = append(abis, filepath.Base(args[i+1]))
+					i++
+				}
+			}
+			if !reflect.DeepEqual(abis, test.abis) {
+				t.Errorf("want abis %v, got %v", test.abis, abis)
+			}
+		})
+	}
+}
diff --git a/java/builder.go b/java/builder.go
index 07af8eb..f55a7c7 100644
--- a/java/builder.go
+++ b/java/builder.go
@@ -109,6 +109,15 @@
 		},
 		"jarArgs")
 
+	zip = pctx.AndroidStaticRule("zip",
+		blueprint.RuleParams{
+			Command:        `${config.SoongZipCmd} -o $out @$out.rsp`,
+			CommandDeps:    []string{"${config.SoongZipCmd}"},
+			Rspfile:        "$out.rsp",
+			RspfileContent: "$jarArgs",
+		},
+		"jarArgs")
+
 	combineJar = pctx.AndroidStaticRule("combineJar",
 		blueprint.RuleParams{
 			Command:     `${config.MergeZipsCmd} --ignore-duplicates -j $jarArgs $out $in`,
diff --git a/java/java.go b/java/java.go
index b4b8feb..7ef3626 100644
--- a/java/java.go
+++ b/java/java.go
@@ -95,9 +95,6 @@
 	// list of java libraries that will be compiled into the resulting jar
 	Static_libs []string `android:"arch_variant"`
 
-	// list of native libraries that will be provided in or alongside the resulting jar
-	Jni_libs []string `android:"arch_variant"`
-
 	// manifest file to be included in resulting jar
 	Manifest *string
 
@@ -365,6 +362,11 @@
 	name string
 }
 
+type jniDependencyTag struct {
+	blueprint.BaseDependencyTag
+	target android.Target
+}
+
 var (
 	staticLibTag     = dependencyTag{name: "staticlib"}
 	libTag           = dependencyTag{name: "javalib"}
@@ -389,6 +391,12 @@
 	aidl android.Path
 }
 
+type jniLib struct {
+	name   string
+	path   android.Path
+	target android.Target
+}
+
 func (j *Module) shouldInstrument(ctx android.BaseContext) bool {
 	return j.properties.Instrument && ctx.Config().IsEnvTrue("EMMA_INSTRUMENT")
 }
@@ -597,6 +605,7 @@
 	ctx.AddFarVariationDependencies([]blueprint.Variation{
 		{Mutator: "arch", Variation: ctx.Config().BuildOsCommonVariant},
 	}, annoTag, j.properties.Annotation_processors...)
+
 	android.ExtractSourcesDeps(ctx, j.properties.Srcs)
 	android.ExtractSourcesDeps(ctx, j.properties.Exclude_srcs)
 	android.ExtractSourcesDeps(ctx, j.properties.Java_resources)
@@ -787,6 +796,11 @@
 		otherName := ctx.OtherModuleName(module)
 		tag := ctx.OtherModuleDependencyTag(module)
 
+		if _, ok := tag.(*jniDependencyTag); ok {
+			// Handled by AndroidApp.collectJniDeps
+			return
+		}
+
 		if to, ok := module.(*Library); ok {
 			switch tag {
 			case bootClasspathTag, libTag, staticLibTag:
diff --git a/java/java_test.go b/java/java_test.go
index 82accd5..1bfd24b 100644
--- a/java/java_test.go
+++ b/java/java_test.go
@@ -15,8 +15,6 @@
 package java
 
 import (
-	"android/soong/android"
-	"android/soong/genrule"
 	"fmt"
 	"io/ioutil"
 	"os"
@@ -27,6 +25,10 @@
 	"testing"
 
 	"github.com/google/blueprint/proptools"
+
+	"android/soong/android"
+	"android/soong/cc"
+	"android/soong/genrule"
 )
 
 var buildDir string
@@ -73,6 +75,7 @@
 	ctx := android.NewTestArchContext()
 	ctx.RegisterModuleType("android_app", android.ModuleFactoryAdaptor(AndroidAppFactory))
 	ctx.RegisterModuleType("android_library", android.ModuleFactoryAdaptor(AndroidLibraryFactory))
+	ctx.RegisterModuleType("android_test", android.ModuleFactoryAdaptor(AndroidTestFactory))
 	ctx.RegisterModuleType("java_binary_host", android.ModuleFactoryAdaptor(BinaryHostFactory))
 	ctx.RegisterModuleType("java_library", android.ModuleFactoryAdaptor(LibraryFactory))
 	ctx.RegisterModuleType("java_library_host", android.ModuleFactoryAdaptor(LibraryHostFactory))
@@ -95,6 +98,16 @@
 		ctx.TopDown("java_sdk_library", sdkLibraryMutator).Parallel()
 	})
 	ctx.RegisterPreSingletonType("overlay", android.SingletonFactoryAdaptor(OverlaySingletonFactory))
+
+	// Register module types and mutators from cc needed for JNI testing
+	ctx.RegisterModuleType("cc_library", android.ModuleFactoryAdaptor(cc.LibraryFactory))
+	ctx.RegisterModuleType("cc_object", android.ModuleFactoryAdaptor(cc.ObjectFactory))
+	ctx.RegisterModuleType("toolchain_library", android.ModuleFactoryAdaptor(cc.ToolchainLibraryFactory))
+	ctx.PreDepsMutators(func(ctx android.RegisterMutatorsContext) {
+		ctx.BottomUp("link", cc.LinkageMutator).Parallel()
+		ctx.BottomUp("begin", cc.BeginMutator).Parallel()
+	})
+
 	ctx.Register()
 
 	extraModules := []string{