use std::fs;
use std::path::{Path, PathBuf};
const PERMISSIONS: &[&str] = &[
"android.permission.POST_NOTIFICATIONS",
"android.permission.FOREGROUND_SERVICE",
"android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK",
"android.permission.WAKE_LOCK",
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS",
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_MEDIA_AUDIO",
"android.permission.READ_MEDIA_IMAGES",
];
const KEEP_RULES: &str = r#"# AUTO-GENERATED by kopuz/build.rs — keep JNI-reached classes from R8.
-keep class com.temidaradev.kopuz.** { *; }
-keepclassmembers class com.temidaradev.kopuz.** { *; }
-keep class dev.dioxus.main.MainActivity { *; }
"#;
const INSIDE_APPLICATION: &str = r#" <service
android:name="com.temidaradev.kopuz.MusicService"
android:foregroundServiceType="mediaPlayback"
android:stopWithTask="false"
android:exported="false" />
<receiver
android:name="com.temidaradev.kopuz.MediaReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.temidaradev.kopuz.ACTION_MEDIA" />
</intent-filter>
</receiver>
"#;
fn warn(msg: &str) {
println!("cargo:warning={msg}");
}
fn embed_fonts(crate_dir: &Path) {
let assets = crate_dir.join("assets");
let fonts = assets.join("fonts");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set"));
for css_name in ["fontawesome.css", "jetbrains-mono.css", "main.css"] {
let src = assets.join(css_name);
println!("cargo:rerun-if-changed={}", src.display());
let css = match fs::read_to_string(&src) {
Ok(c) => c,
Err(e) => {
warn(&format!("cannot read {}: {e}", src.display()));
continue;
}
};
let baked = bake_font_css(&css, &fonts);
let dst = out_dir.join(css_name);
if let Err(e) = fs::write(&dst, baked) {
warn(&format!("cannot write {}: {e}", dst.display()));
}
}
}
fn embed_favicon(crate_dir: &Path) {
use base64::Engine;
let src = crate_dir.join("assets").join("favicon.ico");
println!("cargo:rerun-if-changed={}", src.display());
let bytes = match fs::read(&src) {
Ok(b) => b,
Err(e) => {
warn(&format!("cannot read {}: {e}", src.display()));
return;
}
};
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
let uri = format!("data:image/x-icon;base64,{b64}");
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR not set"));
let dst = out_dir.join("favicon.uri");
if let Err(e) = fs::write(&dst, uri) {
warn(&format!("cannot write {}: {e}", dst.display()));
}
}
fn bake_font_css(css: &str, fonts: &Path) -> String {
use base64::Engine;
const PREFIX: &str = "url(fonts/";
let mut out = String::with_capacity(css.len());
let mut rest = css;
while let Some(idx) = rest.find(PREFIX) {
out.push_str(&rest[..idx]);
let after = &rest[idx + PREFIX.len()..];
let end = after
.find(')')
.unwrap_or_else(|| panic!("unterminated `{PREFIX}` in font CSS"));
let file = &after[..end];
let path = fonts.join(file);
println!("cargo:rerun-if-changed={}", path.display());
let bytes =
fs::read(&path).unwrap_or_else(|e| panic!("cannot read font {}: {e}", path.display()));
let mime = match Path::new(file).extension().and_then(|e| e.to_str()) {
Some("woff2") => "font/woff2",
Some("woff") => "font/woff",
Some("otf") => "font/otf",
Some("ttf") => "font/ttf",
_ => "application/octet-stream",
};
let b64 = base64::engine::general_purpose::STANDARD.encode(&bytes);
out.push_str("url(data:");
out.push_str(mime);
out.push_str(";base64,");
out.push_str(&b64);
out.push(')');
rest = &after[end + 1..];
}
out.push_str(rest);
out
}
fn main() {
let crate_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
embed_fonts(&crate_dir);
embed_favicon(&crate_dir);
#[cfg(target_os = "windows")]
{
let mut res = winres::WindowsResource::new();
res.set_icon("assets/app.ico");
res.compile().expect("failed to compile Windows resources");
}
println!("cargo:rerun-if-env-changed=WRY_ANDROID_KOTLIN_FILES_OUT_DIR");
let out_dir = match std::env::var("WRY_ANDROID_KOTLIN_FILES_OUT_DIR") {
Ok(d) => PathBuf::from(d),
Err(_) => return,
};
let android_src = crate_dir.join("..").join("..").join("android-src");
println!("cargo:rerun-if-changed={}", android_src.display());
let kotlin_root = match out_dir.ancestors().nth(3) {
Some(p) => p.to_path_buf(),
None => {
warn(&format!(
"unexpected WRY kotlin out dir: {}",
out_dir.display()
));
return;
}
};
let main_dir = match kotlin_root.parent() {
Some(p) => p.to_path_buf(),
None => return,
};
let manifest = main_dir.join("AndroidManifest.xml");
let stale_java = main_dir.join("java/com/temidaradev/kopuz");
if stale_java.exists() {
let _ = fs::remove_dir_all(&stale_java);
}
let helper_src = android_src.join("java/com/temidaradev/kopuz");
let helper_dst = kotlin_root.join("com/temidaradev/kopuz");
let _ = fs::remove_dir_all(&helper_dst);
copy_kt_dir(&helper_src, &helper_dst);
let main_activity_src = android_src.join("kotlin/dev/dioxus/main/MainActivity.kt");
let main_activity_dst = out_dir.join("MainActivity.kt");
copy_file(&main_activity_src, &main_activity_dst);
if manifest.exists() {
println!("cargo:rerun-if-changed={}", manifest.display());
patch_manifest(&manifest);
} else {
warn(&format!(
"AndroidManifest.xml not found at {}",
manifest.display()
));
}
copy_str(&main_dir.join("kopuz-keep.pro"), KEEP_RULES);
generate_launcher_icons(&main_dir.join("res"));
}
fn generate_launcher_icons(res: &Path) {
if !res.is_dir() {
return;
}
let logo = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/logo.png");
println!("cargo:rerun-if-changed={}", logo.display());
let img = match image::open(&logo) {
Ok(i) => i,
Err(e) => {
warn(&format!("cannot open logo {}: {e}", logo.display()));
return;
}
};
let densities = [
("mdpi", 48u32),
("hdpi", 72),
("xhdpi", 96),
("xxhdpi", 144),
("xxxhdpi", 192),
];
for (suffix, size) in densities {
let dir = res.join(format!("mipmap-{suffix}"));
if !dir.is_dir() {
continue;
}
let _ = fs::remove_file(dir.join("ic_launcher.webp"));
let _ = fs::remove_file(dir.join("ic_launcher_round.webp"));
let resized = img.resize_exact(size, size, image::imageops::FilterType::Lanczos3);
if let Err(e) = resized.save(dir.join("ic_launcher.png")) {
warn(&format!("cannot write {} icon: {e}", suffix));
}
}
let anydpi = res.join("mipmap-anydpi-v26");
let _ = fs::remove_file(anydpi.join("ic_launcher.xml"));
let _ = fs::remove_file(anydpi.join("ic_launcher_round.xml"));
}
fn copy_str(dst: &Path, contents: &str) {
if let Some(parent) = dst.parent() {
let _ = fs::create_dir_all(parent);
}
if let Err(e) = fs::write(dst, contents) {
warn(&format!("cannot write {}: {e}", dst.display()));
}
}
fn copy_kt_dir(src: &Path, dst: &Path) {
let entries = match fs::read_dir(src) {
Ok(e) => e,
Err(e) => {
warn(&format!("cannot read {}: {e}", src.display()));
return;
}
};
if let Err(e) = fs::create_dir_all(dst) {
warn(&format!("cannot create {}: {e}", dst.display()));
return;
}
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("kt")
&& let Some(name) = path.file_name()
{
copy_file(&path, &dst.join(name));
}
}
}
fn copy_file(src: &Path, dst: &Path) {
println!("cargo:rerun-if-changed={}", src.display());
if let Some(parent) = dst.parent() {
let _ = fs::create_dir_all(parent);
}
if let Err(e) = fs::copy(src, dst) {
warn(&format!(
"cannot copy {} -> {}: {e}",
src.display(),
dst.display()
));
}
}
fn patch_manifest(path: &Path) {
let mut content = match fs::read_to_string(path) {
Ok(c) => c,
Err(e) => {
warn(&format!("cannot read {}: {e}", path.display()));
return;
}
};
let original = content.clone();
for perm in PERMISSIONS {
let marker = format!("android:name=\"{perm}\"");
if !content.contains(&marker) {
content = content.replacen(
" <application ",
&format!(" <uses-permission android:name=\"{perm}\" />\n <application "),
1,
);
}
}
if !content.contains("android:requestLegacyExternalStorage=\"true\"") {
content = content.replacen(
" <application ",
" <application android:requestLegacyExternalStorage=\"true\" ",
1,
);
}
if !content.contains("android:launchMode=") {
content = content.replacen(
"<activity ",
"<activity android:launchMode=\"singleTask\" ",
1,
);
}
if !content.contains("MusicService") {
content = content.replacen(
" </application>",
&format!("{INSIDE_APPLICATION} </application>"),
1,
);
}
if content != original
&& let Err(e) = fs::write(path, &content)
{
warn(&format!("cannot write {}: {e}", path.display()));
}
}