android-cli 0.2.0

Create, build, and release Android apps faster without Android Studio.
Documentation
mod utils;

use anyhow::{Context, Result};
use guidon::{GitOptions, Guidon, TryNew};
use itertools::Itertools;
use serde::{Deserialize, Serialize};

use std::{
    collections::BTreeMap,
    path::{Path, PathBuf},
    process::{Command, ExitStatus},
};

use utils::{find_adb, find_gradle};

pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DEFAULT_MAIN_ACTIVITY: &str = "MainActivity";

const DEFAULT_TEMPLATE_REPO: &str = "https://github.com/SyedAhkam/android-cli-template";
const TEMPLATE_REV: &str = "master";

const DOTFILE_COMMENT: &str = "// DO NOT MODIFY; Generated by Android CLI for internal usage.\n";

#[derive(Debug, Serialize, Deserialize)]
pub struct DotAndroid {
    pub project_name: String,
    pub package_id: String,
    pub gen_at_version: String,
    pub main_activity_name: String,
}

pub fn copy_template(dest: &Path, vars: BTreeMap<String, String>) -> Result<()> {
    let git_options = GitOptions::builder()
        .repo(DEFAULT_TEMPLATE_REPO)
        .rev(TEMPLATE_REV)
        .build()
        .unwrap();

    let mut guidon = Guidon::try_new(git_options)?;

    guidon.variables(vars);
    guidon.apply_template(dest)?;

    Ok(())
}

pub fn create_local_properties_file(root: &Path, sdk_path: &str) -> Result<()> {
    let sdk_path = Path::new(sdk_path);
    let prop_file_path = PathBuf::new().join(root).join("local.properties");

    // It is necessary to escape paths on windows
    let content = if cfg!(windows) {
        format!("sdk.dir={}", sdk_path.display()).replace("\\", "\\\\")
    } else {
        format!("sdk.dir={}", sdk_path.display())
    };
    
    if sdk_path.exists() {
        std::fs::write(prop_file_path, content).context("Unable to write local.properties file")?;
    } else {
        eprintln!("warning: did not create local.properties file because of invalid sdk path")
    }

    Ok(())
}

pub fn invoke_gradle_command(cmd: &str) -> Result<ExitStatus> {
    let gradle_path = find_gradle().context("ERROR: Gradle not found on system")?;

    let mut run = Command::new(gradle_path);
    run.arg(cmd);

    println!(
        "Invoking Gradle: {} {}",
        &run.get_program().to_string_lossy(),
        &run.get_args().map(|arg| arg.to_string_lossy()).join(" ")
    );

    Ok(run.status()?)
}

pub fn invoke_adb_command(args: &[&str]) -> Result<ExitStatus> {
    let adb_path = find_adb().context("ERROR: ADB not found on system")?;

    let mut run = Command::new(adb_path);
    run.args(args);

    println!(
        "Invoking ADB: {} {}",
        &run.get_program().to_string_lossy(),
        &run.get_args().map(|arg| arg.to_string_lossy()).join(" ")
    );

    Ok(run.status()?)
}

pub fn create_dot_android(
    dest: &Path,
    project_name: String,
    package_id: String,
    main_activity_name: Option<String>,
) -> Result<()> {
    // Construct the structure
    let dot_android = DotAndroid {
        package_id,
        project_name,
        gen_at_version: VERSION.to_owned(),
        main_activity_name: main_activity_name.unwrap_or(DEFAULT_MAIN_ACTIVITY.to_owned()),
    };

    // Serialize into Ron
    let mut ron_contents =
        ron::ser::to_string_pretty(&dot_android, ron::ser::PrettyConfig::default())?;

    // Add a comment at top
    ron_contents.insert_str(0, DOTFILE_COMMENT);

    // Write to file
    let path = PathBuf::from(dest).join(".android");
    std::fs::write(path, ron_contents).context("failed to write .android file")?;

    Ok(())
}

pub fn get_dot_android() -> Option<DotAndroid> {
    if let Ok(contents) = std::fs::read_to_string(".android") {
        return ron::from_str::<DotAndroid>(&contents).ok();
    };

    None
}

pub fn install_apk(is_release: bool) -> Result<ExitStatus> {
    let output_dir = PathBuf::from("app/build/outputs/apk");

    let apk_path = match is_release {
        true => output_dir.join("release/app-release.apk"),
        false => output_dir.join("debug/app-debug.apk"),
    };

    Ok(invoke_adb_command(&["install", apk_path.to_str().unwrap()])
        .context("failed to run adb command")?)
}

pub fn trigger_build(is_release: bool) -> Result<ExitStatus> {
    // Decide gradle subcommand to use
    let cmd = match is_release {
        true => "assembleRelease",
        false => "assembleDebug",
    };

    Ok(invoke_gradle_command(cmd).context("failed to invoke gradle command")?)
}

pub fn launch_activity(package_id: String, activity_name: String) -> Result<ExitStatus> {
    Ok(invoke_adb_command(&[
        "shell",
        "am",
        "start",
        "-n",
        &format!("{}/.{}", package_id, activity_name),
    ])
    .context("failed to invoke adb command")?)
}

pub fn query_devices() -> Result<ExitStatus> {
    Ok(invoke_adb_command(&["devices"]).context("failed to invoke adb command")?)
}

pub fn attach_shell() -> Result<ExitStatus> {
    Ok(invoke_adb_command(&["shell"]).context("failed to invoke adb command")?)
}