Skip to content

Commit fdedb16

Browse files
🔀 Merge develop into main (pull request #8)
Release 1.1.1 # External changes - Fix crash on devices with no package matches (#7). # Internal changes - Move declarations around to adhere to code style.
2 parents 3fa4145 + 1903745 commit fdedb16

7 files changed

Lines changed: 112 additions & 103 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "batch_apk_installer"
33
description = "A command-line tool for batch installation of Android Packages (APKs)."
4-
version = "1.1.0"
4+
version = "1.1.1"
55
edition = "2024"
66
license = "MIT"
77
authors = ["Matheus Amazonas"]

src/config.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,6 @@ use serde::Deserialize;
44
use std::fs::{self, File};
55
use std::io::Write;
66

7-
pub type Platform = String;
8-
pub type PackageID = String;
9-
107
const CONFIG_PATH: &str = "Batch APK Installer";
118
const CONFIG_FILE: &str = "config.toml";
129
const CONFIG_TEMPLATE: &str = r#"directory = "/Users/user_name/Desktop"
@@ -27,6 +24,9 @@ id = "com.company.product.quest_only_app"
2724
platforms = [ "pico" ]
2825
match_file_name = true"#;
2926

27+
pub type Platform = String;
28+
pub type PackageID = String;
29+
3030
#[derive(Deserialize)]
3131
pub struct Config {
3232
directory: String,

src/device.rs

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -22,46 +22,57 @@ impl Device {
2222
pub fn platform(&self) -> &String {
2323
&self.platform
2424
}
25-
}
2625

27-
impl Display for Device {
28-
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29-
write!(f, "[{}] {} ({})", self.platform, self.name, self.id)
26+
fn from_str_with_platforms(line: &str, platforms: &[Platform]) -> Option<Device> {
27+
let (id, platform) = Self::parse_info(line, platforms)?;
28+
let name = Self::parse_name(&id).ok()?;
29+
let device = Self { name, id, platform };
30+
Some(device)
3031
}
31-
}
3232

33-
pub fn get_devices(platforms: &[Platform]) -> Result<Vec<Device>, Error> {
34-
let output = Command::new("adb").args(["devices", "-l"]).output()?;
35-
let output = String::from_utf8(output.stdout)?;
36-
let header_line_ix = output
37-
.lines()
38-
.position(|l| l.contains("List of devices attached"))
39-
.ok_or(Error::DevicesFetching)?;
40-
41-
let devices = output
42-
.lines()
43-
.skip(header_line_ix + 1)
44-
.filter(|l| !l.is_empty())
45-
.filter_map(|l| parse_device(l, platforms))
46-
.collect();
47-
Ok(devices)
48-
}
33+
fn parse_name(id: &str) -> Result<String, Error> {
34+
let output = Command::new("adb")
35+
.args(["-s", id, "shell", "settings get global device_name"])
36+
.output()?;
37+
let name = String::from_utf8(output.stdout)?;
38+
match name.len() {
39+
0 => Err(Error::NoDeviceName),
40+
_ => Ok(String::from(name.trim_end())),
41+
}
42+
}
43+
44+
fn parse_info(line: &str, platforms: &[Platform]) -> Option<(String, Platform)> {
45+
let regex = Regex::new(r"(\w+)\s+.*model:(\w+)\sdevice:(\w+)").ok()?;
46+
let caps = regex.captures(line)?;
47+
let id = String::from(caps.get(1)?.as_str());
48+
let model = caps.get(2)?.as_str();
49+
let device_name = caps.get(3)?.as_str();
50+
let platform = get_platform(model, platforms).or(get_platform(device_name, platforms))?;
51+
Some((id, platform))
52+
}
4953

50-
fn parse_device(line: &str, platforms: &[Platform]) -> Option<Device> {
51-
let (id, platform) = parse_device_info(line, platforms)?;
52-
let name = get_device_name(&id).ok()?;
53-
let device = Device { name, id, platform };
54-
Some(device)
54+
pub fn get_devices(platforms: &[Platform]) -> Result<Vec<Device>, Error> {
55+
let output = Command::new("adb").args(["devices", "-l"]).output()?;
56+
let output = String::from_utf8(output.stdout)?;
57+
let header_line_ix = output
58+
.lines()
59+
.position(|l| l.contains("List of devices attached"))
60+
.ok_or(Error::DevicesFetching)?;
61+
62+
let devices = output
63+
.lines()
64+
.skip(header_line_ix + 1)
65+
.filter(|l| !l.is_empty())
66+
.filter_map(|l| Self::from_str_with_platforms(l, platforms))
67+
.collect();
68+
Ok(devices)
69+
}
5570
}
5671

57-
fn parse_device_info(line: &str, platforms: &[Platform]) -> Option<(String, Platform)> {
58-
let regex = Regex::new(r"(\w+)\s+.*model:(\w+)\sdevice:(\w+)").ok()?;
59-
let caps = regex.captures(line)?;
60-
let id = String::from(caps.get(1)?.as_str());
61-
let model = caps.get(2)?.as_str();
62-
let device_name = caps.get(3)?.as_str();
63-
let platform = get_platform(model, platforms).or(get_platform(device_name, platforms))?;
64-
Some((id, platform))
72+
impl Display for Device {
73+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74+
write!(f, "[{}] {} ({})", self.platform, self.name, self.id)
75+
}
6576
}
6677

6778
fn get_platform(identifier: &str, platforms: &[Platform]) -> Option<Platform> {
@@ -70,17 +81,6 @@ fn get_platform(identifier: &str, platforms: &[Platform]) -> Option<Platform> {
7081
Some(platform.clone())
7182
}
7283

73-
fn get_device_name(id: &str) -> Result<String, Error> {
74-
let output = Command::new("adb")
75-
.args(["-s", id, "shell", "settings get global device_name"])
76-
.output()?;
77-
let name = String::from_utf8(output.stdout)?;
78-
match name.len() {
79-
0 => Err(Error::NoDeviceName),
80-
_ => Ok(String::from(name.trim_end())),
81-
}
82-
}
83-
8484
#[cfg(test)]
8585
mod tests {
8686
use super::*;
@@ -122,7 +122,7 @@ mod tests {
122122
let data = "PA7L50MGF8290021W device usb:34873344X product:A7H10 model:Pico_Neo_3 \
123123
device:PICOA7H10 transport_id:2";
124124
let platforms = get_platforms();
125-
let info = parse_device_info(data, &platforms);
125+
let info = Device::parse_info(data, &platforms);
126126
assert!(info.is_some());
127127
let (id, platform) = info.unwrap();
128128
assert_eq!(id, String::from("PA7L50MGF8290021W"));
@@ -134,7 +134,7 @@ mod tests {
134134
let data = "PA8150MGGB230744G device usb:34603008X product:Phoenix_ovs model:A8110 \
135135
device:PICOA8110 transport_id:7";
136136
let platforms = get_platforms();
137-
let info = parse_device_info(data, &platforms);
137+
let info = Device::parse_info(data, &platforms);
138138
assert!(info.is_some());
139139
let (id, platform) = info.unwrap();
140140
assert_eq!(id, String::from("PA8150MGGB230744G"));
@@ -146,7 +146,7 @@ mod tests {
146146
let data = "1WMHHA63PR1501 device usb:34603008X product:hollywood model:Quest_2 \
147147
device:hollywood transport_id:9";
148148
let platforms = get_platforms();
149-
let info = parse_device_info(data, &platforms);
149+
let info = Device::parse_info(data, &platforms);
150150
assert!(info.is_some());
151151
let (id, platform) = info.unwrap();
152152
assert_eq!(id, String::from("1WMHHA63PR1501"));
@@ -158,7 +158,7 @@ mod tests {
158158
let data = "ce031713396bc92803 device usb:1048576X product:dreamltexx model:SM_G950F \
159159
device:dreamlte transport_id:4";
160160
let platforms = get_platforms();
161-
let info = parse_device_info(data, &platforms);
161+
let info = Device::parse_info(data, &platforms);
162162
assert!(info.is_some());
163163
let (id, platform) = info.unwrap();
164164
assert_eq!(id, String::from("ce031713396bc92803"));

src/installation.rs

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,6 @@ use std::sync::Arc;
66
use tokio::sync::mpsc;
77
use tokio_stream::wrappers::ReceiverStream;
88

9-
pub struct DeviceInstallations {
10-
device: Arc<Device>,
11-
packages: Vec<Arc<Package>>,
12-
uninstall_first: bool,
13-
}
14-
159
pub struct CommandOutcome {
1610
description: String,
1711
error: Option<Error>,
@@ -27,21 +21,32 @@ impl CommandOutcome {
2721
}
2822
}
2923

24+
pub struct DeviceInstallations {
25+
device: Arc<Device>,
26+
packages: Vec<Arc<Package>>,
27+
uninstall_first: bool,
28+
}
29+
3030
impl DeviceInstallations {
3131
pub fn build_requests(
3232
devices: &[Arc<Device>], packages: &[Arc<Package>], uninstall_first: bool,
3333
) -> Vec<DeviceInstallations> {
3434
let mut requests: Vec<DeviceInstallations> = Vec::new();
3535
for device in devices {
36-
let matches = packages.iter().filter(|p| is_package_match(device, p));
37-
let mut packages = vec![];
36+
let matches: Vec<_> = packages
37+
.iter()
38+
.filter(|p| is_package_match(device, p))
39+
.collect();
40+
if matches.is_empty() {
41+
continue;
42+
}
43+
let mut packages = Vec::with_capacity(matches.len());
3844
for package in matches {
3945
let package = package.clone();
4046
packages.push(package);
4147
}
42-
let device = device.clone();
4348
let installations = DeviceInstallations {
44-
device,
49+
device: device.clone(),
4550
packages,
4651
uninstall_first,
4752
};

src/main.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use crate::config::Config;
2+
use crate::device::Device;
23
use crate::error::Error;
34
use crate::installation::DeviceInstallations;
5+
use crate::package::Package;
46
use futures::{StreamExt, stream};
57
use std::env;
68
use std::path::PathBuf;
@@ -92,7 +94,7 @@ async fn main() {
9294
}
9395
};
9496

95-
let devices: Vec<_> = match device::get_devices(config.platforms()) {
97+
let devices: Vec<_> = match Device::get_devices(config.platforms()) {
9698
Ok(devices) if !devices.is_empty() => devices.into_iter().map(Arc::new).collect(),
9799
Ok(_) => {
98100
print_error("No devices were found.");
@@ -110,7 +112,7 @@ async fn main() {
110112
}
111113

112114
let packages_dir = PathBuf::from(config.directory()).join(packages_folder);
113-
let packages: Vec<_> = match package::find_all_packages(&packages_dir, config.packages()) {
115+
let packages: Vec<_> = match Package::find_all(&packages_dir, config.packages()) {
114116
Ok(packages) => packages.into_iter().map(Arc::new).collect(),
115117
Err(e) => {
116118
print_error(&e.to_string());

src/package.rs

Lines changed: 39 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,44 @@ use std::fs;
55
use std::path::PathBuf;
66
use std::process::Command;
77

8-
pub struct Package {
9-
id: PackageID,
10-
path: String,
11-
file_name: String,
12-
platforms: Vec<Platform>,
13-
match_file_name: bool,
14-
}
15-
168
pub struct PackageFile {
179
path: PathBuf,
1810
id: PackageID,
1911
}
2012

13+
impl PackageFile {
14+
fn try_from_path(path: &PathBuf) -> Result<PackageFile, Error> {
15+
let path_str = path.to_str().ok_or(Error::MalformedPackageFilePath)?;
16+
let output = Command::new("aapt2")
17+
.args(["dump", "packagename", path_str])
18+
.output()?;
19+
if output.status.success() {
20+
let output = String::from_utf8(output.stdout)?;
21+
let id = String::from(output.trim_end());
22+
let path = PathBuf::from(path);
23+
let package = PackageFile { path, id };
24+
Ok(package)
25+
} else {
26+
Err(Error::PackageNameNotFound)
27+
}
28+
}
29+
}
30+
2131
#[derive(Deserialize)]
2232
pub struct PackageConfig {
2333
id: PackageID,
2434
platforms: Vec<Platform>,
2535
match_file_name: bool,
2636
}
2737

38+
pub struct Package {
39+
id: PackageID,
40+
path: String,
41+
file_name: String,
42+
platforms: Vec<Platform>,
43+
match_file_name: bool,
44+
}
45+
2846
impl Package {
2947
pub fn try_new(
3048
file: PackageFile, platforms: Vec<Platform>, match_file_name: bool,
@@ -60,25 +78,25 @@ impl Package {
6078
pub fn match_file_name(&self) -> bool {
6179
self.match_file_name
6280
}
63-
}
6481

65-
pub fn find_all_packages(dir: &PathBuf, configs: &[PackageConfig]) -> Result<Vec<Package>, Error> {
6682
fn build_package(file: PackageFile, configs: &[PackageConfig]) -> Option<Package> {
6783
let config = configs.iter().find(|c| c.id == file.id)?;
68-
let package = Package::try_new(file, config.platforms.clone(), config.match_file_name)?;
84+
let package = Self::try_new(file, config.platforms.clone(), config.match_file_name)?;
6985
Some(package)
7086
}
7187

72-
if let Ok(true) = fs::exists(dir) {
73-
let files = find_apk_files(dir)?
74-
.into_iter()
75-
.filter_map(|f| get_package_file(&f).ok())
76-
.filter_map(|f| build_package(f, configs))
77-
.collect();
78-
Ok(files)
79-
} else {
80-
let path = dir.to_string_lossy().to_string();
81-
Err(Error::NoPackageDirectory(path))
88+
pub fn find_all(dir: &PathBuf, configs: &[PackageConfig]) -> Result<Vec<Package>, Error> {
89+
if let Ok(true) = fs::exists(dir) {
90+
let files = find_apk_files(dir)?
91+
.into_iter()
92+
.filter_map(|f| PackageFile::try_from_path(&f).ok())
93+
.filter_map(|f| Self::build_package(f, configs))
94+
.collect();
95+
Ok(files)
96+
} else {
97+
let path = dir.to_string_lossy().to_string();
98+
Err(Error::NoPackageDirectory(path))
99+
}
82100
}
83101
}
84102

@@ -90,19 +108,3 @@ fn find_apk_files(dir: &PathBuf) -> Result<Vec<PathBuf>, Error> {
90108
.collect();
91109
Ok(entries)
92110
}
93-
94-
fn get_package_file(path: &PathBuf) -> Result<PackageFile, Error> {
95-
let path_str = path.to_str().ok_or(Error::MalformedPackageFilePath)?;
96-
let output = Command::new("aapt2")
97-
.args(["dump", "packagename", path_str])
98-
.output()?;
99-
if output.status.success() {
100-
let output = String::from_utf8(output.stdout)?;
101-
let id = String::from(output.trim_end());
102-
let path = PathBuf::from(path);
103-
let package = PackageFile { path, id };
104-
Ok(package)
105-
} else {
106-
Err(Error::PackageNameNotFound)
107-
}
108-
}

0 commit comments

Comments
 (0)