android_cli/
lib.rs

1mod utils;
2
3use anyhow::{Context, Result};
4use guidon::{GitOptions, Guidon, TryNew};
5use itertools::Itertools;
6use serde::{Deserialize, Serialize};
7
8use std::{
9    collections::BTreeMap,
10    path::{Path, PathBuf},
11    process::{Command, ExitStatus},
12};
13
14use utils::{find_adb, find_gradle};
15
16pub const VERSION: &str = env!("CARGO_PKG_VERSION");
17pub const DEFAULT_MAIN_ACTIVITY: &str = "MainActivity";
18
19const DEFAULT_TEMPLATE_REPO: &str = "https://github.com/SyedAhkam/android-cli-template";
20const TEMPLATE_REV: &str = "master";
21
22const DOTFILE_COMMENT: &str = "// DO NOT MODIFY; Generated by Android CLI for internal usage.\n";
23
24#[derive(Debug, Serialize, Deserialize)]
25pub struct DotAndroid {
26    pub project_name: String,
27    pub package_id: String,
28    pub gen_at_version: String,
29    pub main_activity_name: String,
30}
31
32pub fn copy_template(dest: &Path, vars: BTreeMap<String, String>) -> Result<()> {
33    let git_options = GitOptions::builder()
34        .repo(DEFAULT_TEMPLATE_REPO)
35        .rev(TEMPLATE_REV)
36        .build()
37        .unwrap();
38
39    let mut guidon = Guidon::try_new(git_options)?;
40
41    guidon.variables(vars);
42    guidon.apply_template(dest)?;
43
44    Ok(())
45}
46
47pub fn create_local_properties_file(root: &Path, sdk_path: &str) -> Result<()> {
48    let sdk_path = Path::new(sdk_path);
49    let prop_file_path = PathBuf::new().join(root).join("local.properties");
50
51    // It is necessary to escape paths on windows
52    let content = if cfg!(windows) {
53        format!("sdk.dir={}", sdk_path.display()).replace("\\", "\\\\")
54    } else {
55        format!("sdk.dir={}", sdk_path.display())
56    };
57    
58    if sdk_path.exists() {
59        std::fs::write(prop_file_path, content).context("Unable to write local.properties file")?;
60    } else {
61        eprintln!("warning: did not create local.properties file because of invalid sdk path")
62    }
63
64    Ok(())
65}
66
67pub fn invoke_gradle_command(cmd: &str) -> Result<ExitStatus> {
68    let gradle_path = find_gradle().context("ERROR: Gradle not found on system")?;
69
70    let mut run = Command::new(gradle_path);
71    run.arg(cmd);
72
73    println!(
74        "Invoking Gradle: {} {}",
75        &run.get_program().to_string_lossy(),
76        &run.get_args().map(|arg| arg.to_string_lossy()).join(" ")
77    );
78
79    Ok(run.status()?)
80}
81
82pub fn invoke_adb_command(args: &[&str]) -> Result<ExitStatus> {
83    let adb_path = find_adb().context("ERROR: ADB not found on system")?;
84
85    let mut run = Command::new(adb_path);
86    run.args(args);
87
88    println!(
89        "Invoking ADB: {} {}",
90        &run.get_program().to_string_lossy(),
91        &run.get_args().map(|arg| arg.to_string_lossy()).join(" ")
92    );
93
94    Ok(run.status()?)
95}
96
97pub fn create_dot_android(
98    dest: &Path,
99    project_name: String,
100    package_id: String,
101    main_activity_name: Option<String>,
102) -> Result<()> {
103    // Construct the structure
104    let dot_android = DotAndroid {
105        package_id,
106        project_name,
107        gen_at_version: VERSION.to_owned(),
108        main_activity_name: main_activity_name.unwrap_or(DEFAULT_MAIN_ACTIVITY.to_owned()),
109    };
110
111    // Serialize into Ron
112    let mut ron_contents =
113        ron::ser::to_string_pretty(&dot_android, ron::ser::PrettyConfig::default())?;
114
115    // Add a comment at top
116    ron_contents.insert_str(0, DOTFILE_COMMENT);
117
118    // Write to file
119    let path = PathBuf::from(dest).join(".android");
120    std::fs::write(path, ron_contents).context("failed to write .android file")?;
121
122    Ok(())
123}
124
125pub fn get_dot_android() -> Option<DotAndroid> {
126    if let Ok(contents) = std::fs::read_to_string(".android") {
127        return ron::from_str::<DotAndroid>(&contents).ok();
128    };
129
130    None
131}
132
133pub fn install_apk(is_release: bool) -> Result<ExitStatus> {
134    let output_dir = PathBuf::from("app/build/outputs/apk");
135
136    let apk_path = match is_release {
137        true => output_dir.join("release/app-release.apk"),
138        false => output_dir.join("debug/app-debug.apk"),
139    };
140
141    Ok(invoke_adb_command(&["install", apk_path.to_str().unwrap()])
142        .context("failed to run adb command")?)
143}
144
145pub fn trigger_build(is_release: bool) -> Result<ExitStatus> {
146    // Decide gradle subcommand to use
147    let cmd = match is_release {
148        true => "assembleRelease",
149        false => "assembleDebug",
150    };
151
152    Ok(invoke_gradle_command(cmd).context("failed to invoke gradle command")?)
153}
154
155pub fn launch_activity(package_id: String, activity_name: String) -> Result<ExitStatus> {
156    Ok(invoke_adb_command(&[
157        "shell",
158        "am",
159        "start",
160        "-n",
161        &format!("{}/.{}", package_id, activity_name),
162    ])
163    .context("failed to invoke adb command")?)
164}
165
166pub fn query_devices() -> Result<ExitStatus> {
167    Ok(invoke_adb_command(&["devices"]).context("failed to invoke adb command")?)
168}
169
170pub fn attach_shell() -> Result<ExitStatus> {
171    Ok(invoke_adb_command(&["shell"]).context("failed to invoke adb command")?)
172}