Skip to content
Draft
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
16 changes: 16 additions & 0 deletions UsedByNativeSample/.gitignore
Original file line number Diff line number Diff line change
@@ -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/
60 changes: 60 additions & 0 deletions UsedByNativeSample/README.md
Original file line number Diff line number Diff line change
@@ -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 <variantName>ExtractKeepRules

// For Android AAR Modules

./gradlew <variantName>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<org.gradle.jvm.tasks.Jar>("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 })
}
}
```
1 change: 1 addition & 0 deletions UsedByNativeSample/app/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/build
66 changes: 66 additions & 0 deletions UsedByNativeSample/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
28 changes: 28 additions & 0 deletions UsedByNativeSample/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Example">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Example"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
10 changes: 10 additions & 0 deletions UsedByNativeSample/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -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)
63 changes: 63 additions & 0 deletions UsedByNativeSample/app/src/main/cpp/native-lib.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#include <jni.h>
#include <string>
#include <android/log.h>

#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 <methods>` 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, "<init>", "(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);
}
Original file line number Diff line number Diff line change
@@ -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 "";
}
Original file line number Diff line number Diff line change
@@ -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...")
}
}
Original file line number Diff line number Diff line change
@@ -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!")
}
}
Original file line number Diff line number Diff line change
@@ -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<out StartupTask>) {
// 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...")
}
}
Loading
Loading