tessera-mobile 0.0.0

Rust on mobile made easy.
Documentation
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);
    }
}