use std::{collections::BTreeSet, process::Command};
use once_cell_regex::regex_multi_line;
use thiserror::Error;
use crate::{
android::{
device::{ConnectionStatus, Device},
env::Env,
target::Target,
},
env::ExplicitEnv as _,
target::TargetTrait,
util::cli::{Report, Reportable},
};
use super::{device_name, get_prop};
#[derive(Debug, Error)]
pub enum Error {
#[error("Failed to run `adb devices`: {0}")]
DevicesFailed(#[from] super::RunCheckedError),
#[error("Failed to run `adb -s {serial_no} shell getprop ro.product.model`: {error}")]
ModelFailed {
serial_no: String,
error: get_prop::Error,
},
#[error("Failed to run `adb -s {serial_no} shell getprop ro.product.cpu.abi`: {error}")]
AbiFailed {
serial_no: String,
error: get_prop::Error,
},
#[error("{0:?} isn't a valid target ABI.")]
AbiInvalid(String),
#[error("Failed to run {command}: {error}")]
CommandFailed {
command: String,
error: std::io::Error,
},
}
impl Reportable for Error {
fn report(&self) -> Report {
let msg = "Failed to detect connected Android devices";
match self {
Self::DevicesFailed(err) => err.report("Failed to run `adb devices`"),
Self::ModelFailed { serial_no, error } | Self::AbiFailed { serial_no, error } => {
Report::error(
format!("Failed to run `adb -s {serial_no} shell getprop`"),
error,
)
}
Self::AbiInvalid(_) => Report::error(msg, self),
Self::CommandFailed { command, error } => {
Report::error(format!("Failed to run {command}"), error)
}
}
}
}
const ADB_DEVICE_REGEX: &str = r"^([\S]{6,100}) ([\S]{6,100})\b";
pub fn device_list(env: &Env) -> Result<BTreeSet<Device<'static>>, Error> {
let mut cmd = Command::new(env.platform_tools_path().join("adb"));
cmd.arg("devices").envs(env.explicit_env());
super::check_authorized(&cmd.output().map_err(|error| Error::CommandFailed {
command: format!("{} devices", cmd.get_program().to_string_lossy()),
error,
})?)
.map(|raw_list| {
regex_multi_line!(ADB_DEVICE_REGEX)
.captures_iter(&raw_list)
.map(|caps| {
assert_eq!(caps.len(), 3);
let serial_no = caps.get(1).unwrap().as_str().to_owned();
let status = caps.get(2).unwrap().as_str();
let status = match status {
"device" => ConnectionStatus::Connected,
"unauthorized" => ConnectionStatus::Unauthorized,
"offline" => ConnectionStatus::Offline,
"authorizing" => ConnectionStatus::Authorizing,
_ => {
log::warn!("Unknown device status {status}");
ConnectionStatus::Offline
}
};
if status == ConnectionStatus::Connected {
let model = get_prop(env, &serial_no, "ro.product.model").map_err(|error| {
Error::ModelFailed {
serial_no: serial_no.clone(),
error,
}
})?;
let name = device_name(env, &serial_no).unwrap_or_else(|_| model.clone());
let abi = get_prop(env, &serial_no, "ro.product.cpu.abi").map_err(|error| {
Error::AbiFailed {
serial_no: serial_no.clone(),
error,
}
})?;
let target =
Target::for_abi(&abi).ok_or_else(|| Error::AbiInvalid(abi.clone()))?;
Ok(Device::new(serial_no, name, model, target, status))
} else {
let name = device_name(env, &serial_no).unwrap_or_else(|_| "unknown".into());
Ok(Device::new(
serial_no,
name,
"unknown".into(),
Target::all().values().next().unwrap(),
status,
))
}
})
.collect()
})
.map_err(Error::DevicesFailed)?
}
#[cfg(test)]
mod test {
use super::*;
use rstest::rstest;
#[rstest(input, devices,
case("* daemon not running; starting now at tcp:5020\n\
* daemon started successfully\n\
List of devices attached\n\
AB1234DEFG\tdevice\n\
192.168.100.103:55555\tdevice\n\
", vec!["AB1234DEFG", "192.168.100.103:55555"]
),
case("List of devices attached \n", vec![]),
case("** daemon not running; starting now at tcp:5037\n\
* daemon started successfully\n\
List of devices attached\n\
emulator-5556 device product:sdk_google_phone_x86_64 model:Android_SDK_built_for_x86_64 device:generic_x86_64\n\
emulator-5554 device product:sdk_google_phone_x86 model:Android_SDK_built_for_x86 device:generic_x86\n\
0a388e93 device usb:1-1 product:razor model:Nexus_7 device:flo\n\
", vec!["emulator-5556", "emulator-5554", "0a388e93"]
),
)]
fn test_adb_output_regex(input: &str, devices: Vec<&'static str>) {
let regex = regex_multi_line!(ADB_DEVICE_REGEX);
println!("{input}");
let captures = regex
.captures_iter(input)
.map(|x| x.get(1).unwrap().as_str())
.collect::<Vec<_>>();
assert_eq!(captures, devices);
}
}