All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.
HiyokoKit includes APK installation and an Android app manager. Both use ADB under the hood. Here's the implementation.
APK installation
#[tauri::command]
async fn install_apk(apk_path: String) -> Result<String, AppError> {
let output = tokio::process::Command::new("adb")
.args(["install", "-r", &apk_path]) // -r = reinstall if exists
.output()
.await?;
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
if stdout.contains("Success") {
Ok("Installation successful".into())
} else {
let error = if stderr.contains("INSTALL_FAILED_VERSION_DOWNGRADE") {
"Cannot install older version over newer. Use -d flag to downgrade."
} else if stderr.contains("INSTALL_FAILED_ALREADY_EXISTS") {
"App already installed. Use reinstall option."
} else {
"Installation failed"
};
Err(AppError::Adb(error.into()))
}
}
Parse the specific error codes — generic "installation failed" isn't useful to users.
App list
#[derive(Serialize)]
pub struct AppInfo {
package_name: String,
is_system: bool,
}
#[tauri::command]
async fn list_apps(include_system: bool) -> Result<Vec<AppInfo>, AppError> {
let flag = if include_system { "-l" } else { "-3" }; // -3 = third-party only
let output = tokio::process::Command::new("adb")
.args(["shell", "pm", "list", "packages", flag])
.output()
.await?;
let apps = String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
line.strip_prefix("package:").map(|pkg| AppInfo {
package_name: pkg.trim().to_string(),
is_system: !include_system,
})
})
.collect();
Ok(apps)
}
App uninstall
#[tauri::command]
async fn uninstall_app(package_name: String) -> Result<(), AppError> {
let output = tokio::process::Command::new("adb")
.args(["uninstall", &package_name])
.output()
.await?;
if String::from_utf8_lossy(&output.stdout).contains("Success") {
Ok(())
} else {
Err(AppError::Adb(format!("Failed to uninstall {}", package_name)))
}
}
Clipboard sync between Android and Mac
#[tauri::command]
async fn push_clipboard_to_android(text: String) -> Result<(), AppError> {
tokio::process::Command::new("adb")
.args(["shell", "am", "broadcast", "-a", "clipper.set", "-e", "text", &text])
.status()
.await?;
Ok(())
}
#[tauri::command]
async fn get_android_clipboard() -> Result<String, AppError> {
let output = tokio::process::Command::new("adb")
.args(["shell", "am", "broadcast", "-a", "clipper.get"])
.output()
.await?;
// Parse broadcast result for clipboard content
let stdout = String::from_utf8_lossy(&output.stdout);
extract_clipboard_from_broadcast(&stdout)
}
Note: clipboard sync via ADB requires Clipper or similar app on the Android device.
Error handling for ADB not found
fn check_adb_available() -> Result<(), AppError> {
Command::new("adb")
.arg("version")
.output()
.map_err(|_| AppError::Adb(
"ADB not found. Please install Android Platform Tools.".into()
))?;
Ok(())
}
Check on launch, not on first use. Users should know immediately if ADB is missing.
TL;DR: Building ADB tools in Rust + Tauri: parse specific error codes for APK install (not just "failed"), use pm list packages -3 for third-party apps, and check ADB availability on launch. Clipboard sync needs Clipper on the Android side.
If this was useful, a ❤️ helps more than you'd think — thanks!
Top comments (0)