diff --git a/UsedByNativeSample/.gitignore b/UsedByNativeSample/.gitignore new file mode 100644 index 00000000..834ecd9d --- /dev/null +++ b/UsedByNativeSample/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties +.kotlin/ diff --git a/UsedByNativeSample/README.md b/UsedByNativeSample/README.md new file mode 100644 index 00000000..b3b1b447 --- /dev/null +++ b/UsedByNativeSample/README.md @@ -0,0 +1,60 @@ +# KeepAnnotations Sample + +This examples shows the use of the `androidx.annotation.keep` Gradle plugin. + +- This programmatically generates `keep` rules when an `app` / `android library` / `java library` module uses `androidx.annotation:annotation-keep`. + +## Testing + +### Android AAR Modules (Android Libraries, KMP AAR Modules) + +```bash + +// App Modules + +./gradlew ExtractKeepRules + +// For Android AAR Modules + +./gradlew KeepRulesTransformAar +``` + +### Java / Kotlin Library Modules + +For Java Libraries, you need to do the following. + +* Apply the Gradle Plugin. + +```kotlin +plugins { + alias(libs.plugins.androidx.annotation.keep) +} +``` + +* Register a new task that can generate the artifact with keep rules. + +```kotlin +val jarTask = project.tasks.named("jar") +val outputJar = jarTask.flatMap { it.archiveFile } +val outputFile = jarTask.flatMap { it.archiveFileName }.map { fileName -> + "keep${fileName.uppercaseFirstChar()}" +} + +// Register the JAR transform for extracting keep rules. +val artifactTask = annotationKeep.registerJavaArchiveTransform( + taskName = "keepRulesJar", + input = outputJar, + fileName = outputFile +) +``` + +* Register the output artifact for publication. + +```kotlin +// Register the new artifact. +project.configurations.configureEach { + if (name == "apiElements" || name == "runtimeElements") { + outgoing.artifact(artifactTask.flatMap { it.outputJar }) + } +} +``` diff --git a/UsedByNativeSample/app/.gitignore b/UsedByNativeSample/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/UsedByNativeSample/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/UsedByNativeSample/app/build.gradle.kts b/UsedByNativeSample/app/build.gradle.kts new file mode 100644 index 00000000..3f5fecb6 --- /dev/null +++ b/UsedByNativeSample/app/build.gradle.kts @@ -0,0 +1,66 @@ +import kotlin.math.sign + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.androidx.annotation.keep) + id("kotlin-parcelize") +} + +android { + namespace = "org.example" + compileSdk { + version = release(37) + } + + defaultConfig { + applicationId = "org.example" + minSdk = 24 + targetSdk = 37 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + optimization { + enable = true + } + signingConfig = signingConfigs.getByName("debug") + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + buildFeatures { + compose = true + } + // Native library used by the JNI examples (downcalls + C++ -> Kotlin upcalls). + ndkVersion = "29.0.14206865" + externalNativeBuild { + cmake { + path = file("src/main/cpp/CMakeLists.txt") + version = "3.22.1" + } + } +} + +dependencies { + implementation(libs.androidx.annotation.keep) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.graphics) + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(libs.androidx.junit) + debugImplementation(libs.androidx.compose.ui.test.manifest) + debugImplementation(libs.androidx.compose.ui.tooling) +} diff --git a/UsedByNativeSample/app/src/androidTest/java/org/example/ExampleInstrumentedTest.kt b/UsedByNativeSample/app/src/androidTest/java/org/example/ExampleInstrumentedTest.kt new file mode 100644 index 00000000..1e085ab6 --- /dev/null +++ b/UsedByNativeSample/app/src/androidTest/java/org/example/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package org.example + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("org.example", appContext.packageName) + } +} \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/AndroidManifest.xml b/UsedByNativeSample/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..170354c4 --- /dev/null +++ b/UsedByNativeSample/app/src/main/AndroidManifest.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/cpp/CMakeLists.txt b/UsedByNativeSample/app/src/main/cpp/CMakeLists.txt new file mode 100644 index 00000000..816e4bb0 --- /dev/null +++ b/UsedByNativeSample/app/src/main/cpp/CMakeLists.txt @@ -0,0 +1,10 @@ +# Builds libnative-lib.so, which contains the JNI downcall entry points and the +# C++ -> Kotlin upcalls used by the R8 JNI keep rule examples. +cmake_minimum_required(VERSION 3.22.1) + +project("r8examples") + +add_library(native-lib SHARED native-lib.cpp) + +# liblog provides __android_log_print, used to log from C++. +target_link_libraries(native-lib log) diff --git a/UsedByNativeSample/app/src/main/cpp/native-lib.cpp b/UsedByNativeSample/app/src/main/cpp/native-lib.cpp new file mode 100644 index 00000000..e0812067 --- /dev/null +++ b/UsedByNativeSample/app/src/main/cpp/native-lib.cpp @@ -0,0 +1,63 @@ +#include +#include +#include + +#define LOG_TAG "NativeLib" +#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) + +// ============================================================================================= +// Downcalls (jni/DowncallExample.kt): Kotlin -> C++ +// ============================================================================================= +// The JNI symbol names encode the Kotlin package, class and method names +// (org.example.jni.NativeLib.add). R8 keeps those names because of the default +// `native ` rule in proguard-android-optimize.txt, so no custom keep rule is needed. + +extern "C" JNIEXPORT jint JNICALL +Java_org_example_jni_NativeLib_add(JNIEnv* /* env */, jobject /* this */, jint a, jint b) { + LOGE("NativeLib.add(%d, %d) called from Kotlin", a, b); + return a + b; +} + +extern "C" JNIEXPORT jstring JNICALL +Java_org_example_jni_NativeLib_greet(JNIEnv* env, jobject /* this */, jstring name) { + const char* chars = env->GetStringUTFChars(name, nullptr); + if (chars == nullptr) return nullptr; // OutOfMemoryError pending + LOGE("NativeLib.greet(\"%s\") called from Kotlin", chars); + std::string greeting = std::string("Hello ") + chars + " from C++"; + env->ReleaseStringUTFChars(name, chars); + return env->NewStringUTF(greeting.c_str()); +} + +// ============================================================================================= +// Upcall (jni/UpcallExample.kt): C++ -> Kotlin +// ============================================================================================= +// R8 does not analyze C/C++ code, so every class, constructor and method that is looked up by +// name below is invisible to it. Each of them must be protected by a keep rule (here via +// @UsedByNative), otherwise the lookup fails after R8 runs. When a lookup fails, JNI returns +// null and leaves a Java exception pending (NoClassDefFoundError / NoSuchMethodError); we +// return early so that exception is rethrown in Kotlin when the native method returns. +static void triggerUpcall(JNIEnv* env, jobject bridgeObj) { + // Find data model class and constructor + jclass dataClass = env->FindClass("org/example/jni/NativeData"); + if (dataClass == nullptr) return; + jmethodID dataInit = env->GetMethodID(dataClass, "", "(ILjava/lang/String;)V"); + if (dataInit == nullptr) return; + + // Instantiate NativeData + jstring payload = env->NewStringUTF("Event triggered from C++"); + jobject dataObj = env->NewObject(dataClass, dataInit, 42, payload); + if (dataObj == nullptr) return; + + // Find callback method on JniBridge and invoke upcall + jclass bridgeClass = env->GetObjectClass(bridgeObj); + jmethodID onEventMethod = env->GetMethodID(bridgeClass, "onNativeEvent", "(Lorg/example/jni/NativeData;)V"); + if (onEventMethod == nullptr) return; + env->CallVoidMethod(bridgeObj, onEventMethod, dataObj); +} + +// Downcall entry point for JniBridge.triggerUpcall(), which starts the upcall above. +extern "C" JNIEXPORT void JNICALL +Java_org_example_jni_JniBridge_triggerUpcall(JNIEnv* env, jobject thiz) { + LOGE("JniBridge.triggerUpcall(): calling back into Kotlin"); + triggerUpcall(env, thiz); +} diff --git a/UsedByNativeSample/app/src/main/java/com/google/android/apps/common/proguard/UsedByNative.java b/UsedByNativeSample/app/src/main/java/com/google/android/apps/common/proguard/UsedByNative.java new file mode 100644 index 00000000..8787c11c --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/com/google/android/apps/common/proguard/UsedByNative.java @@ -0,0 +1,21 @@ +package com.google.android.apps.common.proguard; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation indicating that a class, method, or field is used by native code. + * This is used to ensure Proguard/R8 does not remove or obfuscate these members. + */ +@Retention(RetentionPolicy.CLASS) +@Target({ + ElementType.METHOD, + ElementType.FIELD, + ElementType.TYPE, + ElementType.CONSTRUCTOR +}) +public @interface UsedByNative { + String value() default ""; +} diff --git a/UsedByNativeSample/app/src/main/java/org/example/AnnotationClassReflectionExample.kt b/UsedByNativeSample/app/src/main/java/org/example/AnnotationClassReflectionExample.kt new file mode 100644 index 00000000..86dcc994 --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/AnnotationClassReflectionExample.kt @@ -0,0 +1,25 @@ +package org.example + +import android.util.Log + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +annotation class ReflectiveExecutor + +class ReflectiveTaskRunner { + fun process(task: Any) { + val taskClass = task::class.java + if (taskClass.isAnnotationPresent(ReflectiveExecutor::class.java)) { + val methodToCall = taskClass.getMethod("execute") + methodToCall.invoke(task) + } + } +} + +@ReflectiveExecutor +class ImportantBackgroundTask { + fun execute() { + // This class will be removed by R8 without a keep rule + Log.e("ImportantBackgroundTask", "Executing the important background task...") + } +} diff --git a/UsedByNativeSample/app/src/main/java/org/example/AnnotationMethodReflectionExample.kt b/UsedByNativeSample/app/src/main/java/org/example/AnnotationMethodReflectionExample.kt new file mode 100644 index 00000000..2a20ca1b --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/AnnotationMethodReflectionExample.kt @@ -0,0 +1,28 @@ +package org.example + +import android.util.Log + +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.FUNCTION) +annotation class OnEvent + +class EventBus { + fun dispatch(listener: Any) { + // Find all methods annotated with @OnEvent and invoke them + listener::class.java.declaredMethods.forEach { method -> + if (method.isAnnotationPresent(OnEvent::class.java)) { + try { + method.invoke(listener) + } catch (e: Exception) { /* ... */ } + } + } + } +} + +class MyEventListener { + @OnEvent + fun onSomethingHappened() { + // This method will be removed by R8 without a keep rule + Log.d("MyEventListener", "Event received!") + } +} diff --git a/UsedByNativeSample/app/src/main/java/org/example/InterfaceReflectionExample.kt b/UsedByNativeSample/app/src/main/java/org/example/InterfaceReflectionExample.kt new file mode 100644 index 00000000..93bdbd4d --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/InterfaceReflectionExample.kt @@ -0,0 +1,24 @@ +package org.example + +import android.util.Log + +// The library is given a direct reference to the app's task class. +interface StartupTask { + fun run() +} + +// The library object that loads and executes the task. +object TaskRunner { + fun execute(taskClass: Class) { + // The class isn't removed, but its constructor might be. + val task = taskClass.getDeclaredConstructor().newInstance() + task.run() + } +} + +// The app's task is to pre-cache data. +class PreCacheTask : StartupTask { + override fun run() { + Log.d("AppTask", "Warming up the cache...") + } +} diff --git a/UsedByNativeSample/app/src/main/java/org/example/MainActivity.kt b/UsedByNativeSample/app/src/main/java/org/example/MainActivity.kt new file mode 100644 index 00000000..f3e1bcb5 --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/MainActivity.kt @@ -0,0 +1,95 @@ +package org.example + +import android.os.Bundle +import android.util.Log +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.annotation.keep.UsesReflectionToAccessMethod +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.example.jni.JniBridge +import org.example.jni.NativeLib +import org.example.ui.theme.ExampleTheme + +class MainActivity : ComponentActivity() { + // Data objects that C++ passed to Kotlin through the JNI upcalls, shown on screen. + private val nativeResults = mutableStateListOf() + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + ExampleTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Column(modifier = Modifier.padding(innerPadding).padding(16.dp)) { + Greeting(name = "Android") + NativeResults(results = nativeResults) + } + } + } + } + + + // Call Reflection Examples + try { + // JNI downcall (jni/DowncallExample.kt): Kotlin -> C++. The arguments go down to C++ + // and the results come back as return values. + val nativeLib = NativeLib() + val sum = nativeLib.add(2, 3) + val greeting = nativeLib.greet("R8") + Log.e("NativeLib", "Downcall results: add(2, 3) = $sum, greet(\"R8\") = $greeting") + nativeResults += "Downcall: add(2, 3) = $sum" + nativeResults += "Downcall: greet(\"R8\") = $greeting" + + // JNI upcall (jni/UpcallExample.kt): C++ -> Kotlin. The triggerUpcall() downcall makes + // C++ construct a NativeData and pass it to JniBridge.onNativeEvent(). + JniBridge { data -> nativeResults += "Upcall: onNativeEvent($data)" }.triggerUpcall() + + + + // Test Serializable + SerializableTester.testSerialization() + + Log.e("MainActivity", "All reflection examples executed successfully.") + } catch (e: Throwable) { + // JNI lookup failures are Errors (NoSuchMethodError, NoClassDefFoundError, + // UnsatisfiedLinkError), so catch Throwable to log them before crashing. + Log.e("MainActivity", "Reflection example failed", e) + throw RuntimeException("Reflection example failed", e) + } + } +} + +@Composable +fun Greeting(name: String, modifier: Modifier = Modifier) { + Text( + text = "Hello $name!", + modifier = modifier + ) +} + +// Shows the results of the JNI downcall and upcall examples. +@Composable +fun NativeResults(results: List, modifier: Modifier = Modifier) { + Column(modifier = modifier.padding(top = 16.dp)) { + Text(text = "JNI examples (org.example.jni):") + results.forEach { Text(text = it, modifier = Modifier.padding(top = 8.dp)) } + } +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + ExampleTheme { + Greeting("Android") + } +} \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/java/org/example/ParcelableExample.kt b/UsedByNativeSample/app/src/main/java/org/example/ParcelableExample.kt new file mode 100644 index 00000000..71a788db --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/ParcelableExample.kt @@ -0,0 +1,11 @@ +package org.example + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +// Add the @Parcelize annotation to your data class +@Parcelize +data class UserData( + val name: String, + val age: Int +) : Parcelable diff --git a/UsedByNativeSample/app/src/main/java/org/example/ReflectionExamples.kt b/UsedByNativeSample/app/src/main/java/org/example/ReflectionExamples.kt new file mode 100644 index 00000000..ea135e16 --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/ReflectionExamples.kt @@ -0,0 +1,50 @@ +package org.example + +import android.util.Log +import androidx.annotation.keep.UsesReflectionToAccessField +import androidx.annotation.keep.UsesReflectionToConstruct + +// --- Example: Optional feature module loaded via reflection --- + +object AnalyticsManager { + private const val TAG = "AnalyticsManager" + private const val VIDEO_TRACKER_CLASS = "org.example.VideoEventTracker" + + @UsesReflectionToConstruct( + className = "org.example.VideoEventTracker", + parameterTypes = [] + ) + fun initialize() { + try { + // Attempt to load the optional module's class using reflection + Class.forName(VIDEO_TRACKER_CLASS).getDeclaredConstructor().newInstance() + Log.d(TAG, "Video tracking enabled.") + } catch (e: ClassNotFoundException) { + Log.d(TAG,"Video tracking module not found. Skipping.") + } catch (e: Exception) { + Log.e(TAG, e.stackTraceToString()) + } + } +} + +// In a real app, this might be in a different module +class VideoEventTracker { + // This constructor must be kept for the reflection call to succeed. + init { /* ... */ } +} + +// --- Example: Reflection to access private members --- + +class LibraryClass { + // This field might be removed by R8 if not kept + private val secretMessage: String = "Hidden secret" +} + +fun accessSecretMessage(instance: LibraryClass) { + // Use Java reflection from Kotlin to access the private field + val secretField = instance::class.java.getDeclaredField("secretMessage") + secretField.isAccessible = true + // This will crash at runtime with R8 enabled if the field is not kept + val message = secretField.get(instance) as String + Log.d("ReflectionExample", "Secret message: $message") +} diff --git a/UsedByNativeSample/app/src/main/java/org/example/ResourceLocator.kt b/UsedByNativeSample/app/src/main/java/org/example/ResourceLocator.kt new file mode 100644 index 00000000..571a5132 --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/ResourceLocator.kt @@ -0,0 +1,15 @@ +package org.example + +import androidx.annotation.keep.UsesReflectionToConstruct + +interface ResourceLocator { + companion object { + fun load(): ResourceLocator { + val klass = Class.forName(ResourceLocatorImpl::class.java.name) + @Suppress("DEPRECATION") + return klass.newInstance() as ResourceLocator + } + } +} + +private class ResourceLocatorImpl : ResourceLocator diff --git a/UsedByNativeSample/app/src/main/java/org/example/SerializableExample.kt b/UsedByNativeSample/app/src/main/java/org/example/SerializableExample.kt new file mode 100644 index 00000000..94826644 --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/SerializableExample.kt @@ -0,0 +1,53 @@ +package org.example + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.ObjectInputStream +import java.io.ObjectOutputStream +import java.io.Serializable + +class CustomSerializableData(var payload: String) : Serializable { + + @Transient + var isCustomDeserialized: Boolean = false + + private fun writeObject(out: ObjectOutputStream) { + out.defaultWriteObject() + } + + private fun readObject(`in`: ObjectInputStream) { + `in`.defaultReadObject() + // If this method is stripped/renamed by R8, this flag remains false + isCustomDeserialized = true + } + + companion object { + private const val serialVersionUID = 1L + } +} + +object SerializableTester { + fun testSerialization() { + val original = CustomSerializableData("SecretPayload") + + // Serialize + val baos = ByteArrayOutputStream() + val oos = ObjectOutputStream(baos) + oos.writeObject(original) + oos.close() + + // Deserialize + val bais = ByteArrayInputStream(baos.toByteArray()) + val ois = ObjectInputStream(bais) + val deserialized = ois.readObject() as CustomSerializableData + ois.close() + + // Verify that custom readObject was called. + // If the keep rule is missing, R8 will strip/rename readObject, + // and Java serialization will silently fall back to default deserialization, + // leaving isCustomDeserialized as false! + if (!deserialized.isCustomDeserialized) { + throw RuntimeException("CRASH: Custom readObject was not called! Keep rule for Serializable is missing or failing.") + } + } +} diff --git a/UsedByNativeSample/app/src/main/java/org/example/jni/DowncallExample.kt b/UsedByNativeSample/app/src/main/java/org/example/jni/DowncallExample.kt new file mode 100644 index 00000000..f1ee4025 --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/jni/DowncallExample.kt @@ -0,0 +1,27 @@ +package org.example.jni + +/** + * Downcall: Kotlin calls into C++ (native-lib.cpp). + * + * No custom keep rule is needed. The default proguard-android-optimize.txt already contains + * + * -keepclasseswithmembernames,includedescriptorclasses class * { + * native ; + * } + * + * which stops R8 from renaming native methods and their classes. The C++ symbols + * Java_org_example_jni_NativeLib_add and Java_org_example_jni_NativeLib_greet encode these names, + * so if R8 renamed them, the calls would fail with UnsatisfiedLinkError. + */ +class NativeLib { + // Arguments are passed down to C++ and the result is returned to Kotlin. + external fun add(a: Int, b: Int): Int + + external fun greet(name: String): String + + companion object { + init { + System.loadLibrary("native-lib") + } + } +} diff --git a/UsedByNativeSample/app/src/main/java/org/example/jni/UpcallExample.kt b/UsedByNativeSample/app/src/main/java/org/example/jni/UpcallExample.kt new file mode 100644 index 00000000..26be57c5 --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/jni/UpcallExample.kt @@ -0,0 +1,40 @@ +package org.example.jni + +import android.util.Log +import com.google.android.apps.common.proguard.UsedByNative + +/** + * Upcall: C++ (native-lib.cpp) calls back into Kotlin. + * + * R8 does not analyze C++, so everything C++ looks up by name is invisible to it: the NativeData + * class, its constructor and JniBridge.onNativeEvent(). They are kept by @UsedByNative together + * with the reusable @UsedByNative rules in rules.keep. + * + * C++ finds NativeData by name (FindClass) and constructs it (NewObject), so the class itself must + * be kept. The `-keepclasseswithmembers class * { @UsedByNative (...); }` rule keeps (and + * doesn't rename) every class that has an @UsedByNative constructor, plus that constructor, so the + * class needs no annotation of its own. The constructor must be annotated explicitly: in Kotlin, + * `@UsedByNative val id` annotates the backing field, not the constructor. + */ +data class NativeData @UsedByNative constructor( + @UsedByNative val id: Int, + @UsedByNative val payload: String +) + +// onEvent receives the data passed up from C++ (MainActivity uses it to show the data on screen). +class JniBridge(private val onEvent: (NativeData) -> Unit = {}) { + @UsedByNative("Invoked from native C++ code via JNI upcall") + fun onNativeEvent(data: NativeData) { + Log.e("JniBridge", "Received event from native code: $data") + onEvent(data) + } + + // Downcall into C++, which creates a NativeData and calls onNativeEvent() back (upcall). + external fun triggerUpcall() + + companion object { + init { + System.loadLibrary("native-lib") + } + } +} diff --git a/UsedByNativeSample/app/src/main/java/org/example/ui/theme/Color.kt b/UsedByNativeSample/app/src/main/java/org/example/ui/theme/Color.kt new file mode 100644 index 00000000..0b7ccd3a --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package org.example.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/java/org/example/ui/theme/Theme.kt b/UsedByNativeSample/app/src/main/java/org/example/ui/theme/Theme.kt new file mode 100644 index 00000000..d429f227 --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package org.example.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun ExampleTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/java/org/example/ui/theme/Type.kt b/UsedByNativeSample/app/src/main/java/org/example/ui/theme/Type.kt new file mode 100644 index 00000000..7eee28d5 --- /dev/null +++ b/UsedByNativeSample/app/src/main/java/org/example/ui/theme/Type.kt @@ -0,0 +1,34 @@ +package org.example.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = Typography( + bodyLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + /* Other default text styles to override + titleLarge = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 22.sp, + lineHeight = 28.sp, + letterSpacing = 0.sp + ), + labelSmall = TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Medium, + fontSize = 11.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp + ) + */ +) \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/keepRules/rules.keep b/UsedByNativeSample/app/src/main/keepRules/rules.keep new file mode 100644 index 00000000..3f6ad97b --- /dev/null +++ b/UsedByNativeSample/app/src/main/keepRules/rules.keep @@ -0,0 +1,95 @@ +# Add project specific R8 rules here. +# AGP will combine all keep rule files in src/main/keepRules to pass to R8 +# +# For more details, see +# https://d.android.com/r/tools/r8/keep-rules + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# --- Keep rule examples for libraries using reflection --- + +# Retrofit +# Preserve generic type information for Call/Observable return types +-keepattributes Signature +# If an interface has a Retrofit HTTP annotation, keep its return type (class <3>) +-if interface * { + @retrofit2.http.* public *** *(...); +} +-keep,allowoptimization,allowshrinking,allowobfuscation class <3> + +# Coroutines +# Keep the signature attribute globally +-keepattributes Signature +# Explicitly keep the Continuation class so its signature is not stripped +-keep class kotlin.coroutines.Continuation +# --- Keep rule examples for specific reflective access --- + +# Optional module loaded via reflection (e.g. VideoEventTracker) +# Keeps the class and its constructor so it can be instantiated via reflection +-keep class org.example.VideoEventTracker { + (); +} + +# Reflection to access private members +# Keeps the specific private field from being removed or obfuscated +-keepclassmembers class org.example.LibraryClass { + private java.lang.String secretMessage; +} + +# --- Keep rule examples added from documentation --- + +# Allow any implementation of StartupTask to be removed if unused. +-keep,allowobfuscation,allowshrinking class * implements org.example.StartupTask +# Keep the default constructor, which is called via reflection. +-keepclassmembers class * implements org.example.StartupTask { + (); +} + +# Retain annotation metadata for runtime reflection. +-keepattributes RuntimeVisibleAnnotations +# Keep the annotation interface itself. +-keep @interface org.example.OnEvent +# Keep the methods annotated with OnEvent +-keepclassmembers class * { + @org.example.OnEvent ; +} + +# Keep the annotation interface itself. +-keep @interface org.example.ReflectiveExecutor +# Keep the execute method in the classes which are being used +-keepclassmembers @org.example.ReflectiveExecutor class * { + public void execute(); +} + + +# Preserve (and don't rename) any class that has a constructor annotated with @UsedByNative, plus +# that constructor. Native code finds such classes by name (FindClass) and constructs them +# (NewObject), so the class must be kept even if no Kotlin/Java code references it. +-keepclasseswithmembers class * { + @com.google.android.apps.common.proguard.UsedByNative (...); +} + +# Preserve annotated members inside classes and protect their signature types +-keepclassmembers,includedescriptorclasses class * { + @com.google.android.apps.common.proguard.UsedByNative (...); + @com.google.android.apps.common.proguard.UsedByNative ; + @com.google.android.apps.common.proguard.UsedByNative ; +} + +# Keep rules for Java Serialization +-keepclassmembers class * implements java.io.Serializable { + static final long serialVersionUID; + private static final java.io.ObjectStreamField[] serialPersistentFields; + !static !transient ; + !private ; + !private ; + private void writeObject(java.io.ObjectOutputStream); + private void readObject(java.io.ObjectInputStream); + java.lang.Object writeReplace(); + java.lang.Object readResolve(); +} diff --git a/UsedByNativeSample/app/src/main/res/drawable/ic_launcher_background.xml b/UsedByNativeSample/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/UsedByNativeSample/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/UsedByNativeSample/app/src/main/res/drawable/ic_launcher_foreground.xml b/UsedByNativeSample/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/UsedByNativeSample/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/UsedByNativeSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/UsedByNativeSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/UsedByNativeSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 00000000..6f3b755b --- /dev/null +++ b/UsedByNativeSample/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/UsedByNativeSample/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 00000000..c209e78e Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/UsedByNativeSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/UsedByNativeSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 00000000..b2dfe3d1 Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/UsedByNativeSample/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/UsedByNativeSample/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 00000000..4f0f1d64 Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/UsedByNativeSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/UsedByNativeSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 00000000..62b611da Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/UsedByNativeSample/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/UsedByNativeSample/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 00000000..948a3070 Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/UsedByNativeSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/UsedByNativeSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..1b9a6956 Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/UsedByNativeSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/UsedByNativeSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 00000000..28d4b77f Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/UsedByNativeSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/UsedByNativeSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9287f508 Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/UsedByNativeSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/UsedByNativeSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 00000000..aa7d6427 Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/UsedByNativeSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/UsedByNativeSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 00000000..9126ae37 Binary files /dev/null and b/UsedByNativeSample/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/UsedByNativeSample/app/src/main/res/values/colors.xml b/UsedByNativeSample/app/src/main/res/values/colors.xml new file mode 100644 index 00000000..f8c6127d --- /dev/null +++ b/UsedByNativeSample/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/res/values/strings.xml b/UsedByNativeSample/app/src/main/res/values/strings.xml new file mode 100644 index 00000000..5d8603e6 --- /dev/null +++ b/UsedByNativeSample/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + R8 Examples + \ No newline at end of file diff --git a/UsedByNativeSample/app/src/main/res/values/themes.xml b/UsedByNativeSample/app/src/main/res/values/themes.xml new file mode 100644 index 00000000..f01b8b69 --- /dev/null +++ b/UsedByNativeSample/app/src/main/res/values/themes.xml @@ -0,0 +1,5 @@ + + + +