Skip to content

Conversation

@wuwbobo2021
Copy link

Excuse me, the previous #71 is closed, and some fixes are made here to recover my breaking changes.

Why did I make the breaking change for the activity element in the manifest?

When I added multiple activities in the overrided XML manifest file, cargo-apk still needs to parse the file into AndroidManifest; while the overriding feature makes specifying manifest elements unsupported by ndk-build possible, the single activity struct item still conflicts with multiple activity items declared in the XML (I got an runtime error); thus I have to change it.

I've tried this:

    #[serde(default)]
    pub activity: Activity,
    #[serde(default)]
    #[serde(rename = "activity", alias = "extra_activity")]
    pub extra_activities: Vec<Activity>,

It doesn't work, and this compilation warning occured:

warning: unreachable pattern
   --> /run/media/di945/500g_lin/home/di945/down/github/cargo-apk/ndk-build/src/manifest.rs:138:22
    |
136 |     pub activity: Activity,
    |         -------- matches all the relevant values
137 |     #[serde(default)]
138 |     #[serde(rename = "activity", alias = "extra_activity")]
    |                      ^^^^^^^^^^ no value can reach this

I changed the code again in order to avoid this breaking change, so [package.metadata.android.application.activity] will not need changing to [[package.metadata.android.application.activity]], and other activities can be declared as other_activity):

    #[serde(default)]
    pub activity: Activity,
    #[serde(default)]
    #[serde(rename(serialize = "activity", deserialize = "other_activity"))]
    pub other_activities: Vec<Activity>,

However, this prevents ndk-build from parsing the manifest XML's multiple activities (except the first), the deserialing key other_activity is for TOML; but the information of extra activities is not needed by cargo-apk itself when performing manifest XML override...

So I gave up the idea of deserializing the XML file "perfectly" (who will use this feature of ndk-build?); I recovered the actions and categories elements for the intent filter to Vec<String> as well (side note: I forgot to update the documentation when I changed these items).

Further changes are probably needed.

Multiple activities & dex insertion test based on https://github.com/wuwbobo2021/jni-min-helper/tree/perm:

Click to expand

Cargo.toml:

[package]
name = "android-simple-test"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
log = "0.4"
jni-min-helper = { path = "..", features = ["futures"] }
android-activity = { version = "0.6", features = ["native-activity"] }
android_logger = "0.14"

[build-dependencies]
android-build = "0.1.2"

[lib]
name = "android_simple_test"
crate-type = ["cdylib"]
path = "main.rs"

[package.metadata.android]
# android_manifest_file = "AndroidManifest.xml"
package = "com.example.android_simple_test"
build_targets = [ "aarch64-linux-android" ]

[package.metadata.android.sdk]
min_sdk_version = 16
target_sdk_version = 30

[[package.metadata.android.uses_permission]]
name = "android.permission.RECORD_AUDIO"

[[package.metadata.android.uses_permission]]
name = "android.permission.ACCESS_COARSE_LOCATION"

[[package.metadata.android.application.other_activity]]
name = "rust.jniminhelper.PermActivity"

build.rs:

use std::{env, fs, path::PathBuf};

use android_build::{Dexer, JavaBuild};

fn main() {
    let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap();
    let src_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap()).join("java");
    let out_dir = env::var("CARGO_MANIFEST_DIR")
        .map(PathBuf::from)
        .unwrap()
        .join("target")
        .join(env::var("PROFILE").unwrap());

    if target_os == "android" {
        let sources = [
            src_dir.join("PermActivity.java"),
        ];
        let android_jar = android_build::android_jar(None);

        let out_cls_dir = out_dir.join("classes");
        if out_cls_dir.try_exists().unwrap() {
            fs::remove_dir_all(&out_cls_dir).unwrap();
        }
        fs::create_dir(&out_cls_dir).unwrap();

        let mut err_string = None;
        if android_jar.is_none() {
            err_string.replace("Failed to find android.jar.".to_string());
        } else if let Err(s) =
            compile_java_source(sources, [android_jar.clone().unwrap()], out_cls_dir.clone())
        {
            err_string.replace(s);
        } else if let Err(s) = build_dex_file(out_cls_dir.clone(), android_jar, [], out_dir.clone())
        {
            err_string.replace(s);
        };

        if let Some(s) = err_string {
            for line in s.lines() {
                println!("cargo::warning={line}");
            }
            panic!("Unable to build the dex file");
        }
    }
}

fn compile_java_source(
    source_paths: impl IntoIterator<Item = PathBuf>,
    class_paths: impl IntoIterator<Item = PathBuf>,
    output_dir: PathBuf,
) -> Result<(), String> {
    let mut java_build = JavaBuild::new();

    for java_src in source_paths {
        println!("cargo:rerun-if-changed={}", java_src.to_string_lossy());
        java_build.file(java_src);
    }

    for class_path in class_paths {
        println!("cargo:rerun-if-changed={}", class_path.to_string_lossy());
        java_build.class_path(class_path);
    }

    java_build.java_source_version(8).java_target_version(8);
    java_build.classes_out_dir(output_dir);

    // Execute the command
    let result = java_build
        .command()
        .map_err(|e| e.to_string())?
        .output()
        .map_err(|e| format!("Failed to execute javac: {e:?}"))?;
    if result.status.success() {
        Ok(())
    } else {
        Err(format!(
            "Java compilation failed: {}",
            String::from_utf8_lossy(&result.stderr)
        ))
    }
}

fn build_dex_file(
    compiled_classes_path: PathBuf,
    android_jar: Option<PathBuf>,
    jar_dependencies: impl IntoIterator<Item = PathBuf>,
    output_dir: PathBuf,
) -> Result<(), String> {
    let mut dexer = Dexer::new();
    if let Some(android_jar) = android_jar {
        dexer.android_jar(&android_jar);
    }
    let dependencies: Vec<_> = jar_dependencies.into_iter().collect();
    for dependency in dependencies.iter() {
        println!("cargo:rerun-if-changed={}", dependency.to_string_lossy());
        dexer.class_path(dependency);
    }
    dexer
        .android_min_api(20)
        .release(env::var("PROFILE").as_ref().map(|s| s.as_str()) == Ok("release"))
        .class_path(&compiled_classes_path)
        .no_desugaring(true)
        .out_dir(output_dir)
        .files(dependencies.iter())
        .collect_classes(&compiled_classes_path)
        .map_err(|e| e.to_string())?;

    // Execute the command
    let result = dexer
        .run()
        .map_err(|e| format!("Failed to execute d8.jar: {e:?}"))?;
    if result.success() {
        Ok(())
    } else {
        Err(format!("Dexer invocation failed: {result}"))
    }
}

main.rs:

use android_activity::{AndroidApp, MainEvent, PollEvent};
use jni_min_helper::*;
use std::time::Duration;
use log::info;

#[no_mangle]
fn android_main(app: AndroidApp) {
    android_logger::init_once(
        android_logger::Config::default()
            .with_max_level(log::LevelFilter::Info)
            .with_tag(android_app_name().as_bytes()),
    );

    info!("spawning...");
    std::thread::spawn(background_loop);

    let mut on_destroy = false;
    loop {
        app.poll_events(
            Some(Duration::from_secs(1)), // timeout
            |event| match event {
                PollEvent::Main(MainEvent::Start) => {
                    info!("Main Start.");
                }
                PollEvent::Main(MainEvent::Resume { loader: _, .. }) => {
                    info!("Main Resume.");
                }
                PollEvent::Main(MainEvent::Pause) => {
                    info!("Main Pause.");
                }
                PollEvent::Main(MainEvent::Stop) => {
                    info!("Main Stop.");
                }
                PollEvent::Main(MainEvent::Destroy) => {
                    info!("Main Destroy.");
                    on_destroy = true;
                }
                _ => (),
            },
        );
        if on_destroy {
            return;
        }
    }
}

fn background_loop() {
    // JniClassLoader::helper_loader().unwrap().replace_app_loader().unwrap();
    // info!("lodaer replaced...");
    let req = PermissionRequest::request("Test", [
        "android.permission.RECORD_AUDIO",
        "android.permission.ACCESS_COARSE_LOCATION",
    ])
    .unwrap();
    let Some(req) = req else {
        info!("permission request is not needed.");
        return;
    };
    info!("requesting permissions...");
    let result = req.wait();
    info!("{result:#?}");
}

Current problem of dex insertion: it's not appended to the APK when the application is compiled for the first time, but it's added while running cargo apk build for the second time.

MarijnS95 and others added 9 commits July 9, 2025 15:29
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v3...v4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](actions/download-artifact@v3...v4)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Google Play will require this starting August 31 2025.  The default of
`23` for `min_sdk_version` is retained, allowing apps to be installed
all the way back to Android 6.0.

https://developer.android.com/google/play/requirements/target-sdk
…shes

Both `log` and `android-activity` seem to be creating vectors or slices
from NULL pointers, which was already invalid but the standard library
now asserts and panics on.  Since both have been fixed, upgrade all
dependencies to get the local and CI runs working again.

To keep duplicate dependencies to a minimum, upgrade `ndk` to `0.9`
as well which now uses `OwnedFd`/`BorrowedFd` to accurately represent
ownership transitions.  This allows us to convert the `looper` example
to safe `rustix` bindings.
@wuwbobo2021
Copy link
Author

wuwbobo2021 commented Jul 10, 2025

Changed api-level to 35 in CI android-emulator to fix the dead problem; fixed multiple activity entries problem while overriding manifest XML.

@wuwbobo2021
Copy link
Author

Fixed the problem "Dex data is not appended to the APK when the application is compiled for the first time". Used aapt to add the dex file into the unaligned apk file, removed zip dependency (that's my mistake).

If requiring the application crate's build script to put the classes.dex under target/<profile> path looks intrusive to the cargo ecosystem, just remove the .parent() in let dexes: Vec<_> = if let Ok(read_dir) = std::fs::read_dir(self.build_dir.parent().unwrap()) {..., cargo-apk/src/apk.rs; then change <CARGO_MANIFEST_DIR>/target/<PROFILE> in Dex File section of README.md to <CARGO_MANIFEST_DIR>/target/<PROFILE>/apk, and note about ensuring the folder is created in the application's build script. I'm not sure if this makes sense, it's up to you to make the decision.

I just realized that https://github.com/mzdk100/cargo-apk2 is currently making more agressive changes, probably inspired by this PR. It's supporting specifying a Java source folder dedicated for the cargo-apk2 managed application to be compiled by cargo-apk2.

@MarijnS95 MarijnS95 deleted the branch rust-mobile:test July 10, 2025 08:48
@MarijnS95 MarijnS95 closed this Jul 10, 2025
@MarijnS95
Copy link
Member

This should not have been targeting the temporary test branch, and GitHub now closed this automatically.

Please have a little more patience before opening new PRs. While I appreciate your enthusiasm, your work and implementation doesn't align with the future plans I have for cargo-apk 😅. Stay tuned!

I just realized that https://github.com/mzdk100/cargo-apk2 is currently making more agressive changes, probably inspired by this PR. It's supporting specifying a Java source folder dedicated for the cargo-apk2 managed application to be compiled by cargo-apk2.

That is the nature of the Rust (Android?) ecosystem. Isolated copy-paste repositories (based on your PR?) instead of collaboration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants