1use std::collections::BTreeMap;
9use std::io::{Read, Write};
10use std::path::{Path, PathBuf};
11use std::process::Command;
12
13use crate::axml::{self, attr, Attr, Element, Node, Value, TYPE_INT_BOOLEAN, TYPE_REFERENCE};
14use crate::{read, R};
15
16const THEME_TRANSLUCENT_NO_TITLE: u32 = 0x0103_0010;
17const HOST_PERMISSIONS: [&str; 3] = [
18 "android.permission.INTERNET",
19 "android.permission.ACCESS_NETWORK_STATE",
20 "android.permission.RECEIVE_BOOT_COMPLETED",
21];
22const STUB_ACTIVITIES: [&str; 4] = [
23 "com.andysu.host.HostActivity",
24 "com.andysu.host.StubActivity1",
25 "com.andysu.host.StubActivity2",
26 "com.andysu.host.StubActivity3",
27];
28const STUB_SERVICES: [&str; 2] = ["com.andysu.host.StubService0", "com.andysu.host.StubService1"];
29const STUB_RECEIVERS: [&str; 2] = ["com.andysu.host.StubReceiver0", "com.andysu.host.StubReceiver1"];
30
31pub struct HostOptions {
32 pub template: PathBuf,
33 pub config: PathBuf,
34 pub factory: PathBuf,
35 pub out: PathBuf,
36 pub keystore: Option<PathBuf>,
37 pub ks_pass: Option<String>,
38 pub key_alias: Option<String>,
39 pub cleartext: bool,
40 pub transparent: bool,
41 pub payload_has_assets: bool,
42}
43
44#[derive(serde::Serialize, Debug, Default)]
45pub struct HostReport {
46 pub apk: String,
47 pub signed: bool,
48 pub stubs: BTreeMap<String, String>,
50 pub dropped: Vec<String>,
51 pub abis: Vec<String>,
52 pub warnings: Vec<String>,
53}
54
55fn b(v: bool) -> Value {
56 Value::Typed { dtype: TYPE_INT_BOOLEAN, data: if v { u32::MAX } else { 0 } }
57}
58
59fn s(v: &str) -> Value {
60 Value::Str(v.into())
61}
62
63fn is_launcher(e: &Element) -> bool {
64 e.elements().filter(|f| f.name == "intent-filter").any(|f| {
65 let has = |tag: &str, v: &str| f.elements().any(|x| x.name == tag && x.android_name() == Some(v));
66 has("action", "android.intent.action.MAIN") && has("category", "android.intent.category.LAUNCHER")
67 })
68}
69
70fn full_name(pkg: &str, n: &str) -> String {
71 if n.starts_with('.') {
72 format!("{pkg}{n}")
73 } else if !n.contains('.') {
74 format!("{pkg}.{n}")
75 } else {
76 n.to_string()
77 }
78}
79
80pub fn transform_transparent(orig: &[u8], rep: &mut HostReport, cleartext: bool, needs_assets: bool) -> R<Vec<u8>> {
84 let mut doc = axml::parse(orig)?;
85 let pkg = doc
86 .root
87 .attrs
88 .iter()
89 .find(|a| a.name == "package")
90 .and_then(|a| a.str())
91 .ok_or("manifest has no package")?
92 .to_string();
93 let have: Vec<String> = doc
94 .root
95 .elements()
96 .filter(|e| e.name == "uses-permission")
97 .filter_map(|e| e.android_name().map(String::from))
98 .collect();
99 let app_pos = doc
100 .root
101 .children
102 .iter()
103 .position(|n| matches!(n, Node::Element(e) if e.name == "application"))
104 .ok_or("manifest has no <application>")?;
105 for (i, p) in HOST_PERMISSIONS.iter().filter(|p| !have.iter().any(|h| h == *p)).enumerate() {
106 doc.root.children.insert(
107 app_pos + i,
108 Node::Element(Element::new("uses-permission", vec![Attr::android("name", attr::NAME, s(p))])),
109 );
110 }
111 const MIN_SDK: u32 = 0x0101_020c;
113 let need = if needs_assets { 30 } else { 28 };
114 if let Some(us) = doc.root.child_mut("uses-sdk") {
115 let cur = match us.attr(MIN_SDK).map(|a| &a.value) {
116 Some(Value::Typed { data, .. }) => *data,
117 Some(Value::Str(x)) => x.parse().unwrap_or(1),
118 None => 1,
119 };
120 if cur < need {
121 us.set(Attr::android("minSdkVersion", MIN_SDK, Value::Typed { dtype: axml::TYPE_INT_DEC, data: need }));
122 rep.warnings.push(format!("transparent mode raises minSdkVersion from {cur} to {need}"));
123 }
124 }
125 let app = doc.root.child_mut("application").unwrap();
126 if let Some(f) = app.attr(attr::APP_COMPONENT_FACTORY).and_then(|a| a.str()) {
127 rep.warnings.push(format!("replacing the app's AppComponentFactory {f}"));
128 }
129 app.set(Attr::android(
130 "appComponentFactory",
131 attr::APP_COMPONENT_FACTORY,
132 s("com.andysu.host.AndySUComponentFactory"),
133 ));
134 app.remove(attr::HAS_CODE);
135 app.set(Attr::android("extractNativeLibs", attr::EXTRACT_NATIVE_LIBS, b(false)));
137 if cleartext {
138 const USES_CLEARTEXT_TRAFFIC: u32 = 0x0101_04ec;
139 app.set(Attr::android("usesCleartextTraffic", USES_CLEARTEXT_TRAFFIC, b(true)));
140 rep.warnings.push("--cleartext: plain HTTP allowed (testing only)".into());
141 }
142 const INIT_ORDER: u32 = 0x0101_001a;
143 app.push(Element::new(
144 "provider",
145 vec![
146 Attr::android("name", attr::NAME, s("com.andysu.host.InitProvider")),
147 Attr::android("exported", attr::EXPORTED, b(false)),
148 Attr::android("authorities", attr::AUTHORITIES, s(&format!("{pkg}.andysu.init"))),
149 Attr::android("initOrder", INIT_ORDER, Value::Typed { dtype: axml::TYPE_INT_DEC, data: 10_000 }),
150 ],
151 ));
152 push_host_components(app, &pkg);
153 Ok(axml::write(&doc))
154}
155
156fn push_host_components(app: &mut Element, pkg: &str) {
157 app.push(Element::new(
158 "activity",
159 vec![
160 Attr::android("name", attr::NAME, s("com.andysu.host.PhoenixActivity")),
161 Attr::android("process", attr::PROCESS, s(":andysu_phoenix")),
162 Attr::android(
163 "theme",
164 attr::THEME,
165 Value::Typed { dtype: TYPE_REFERENCE, data: THEME_TRANSLUCENT_NO_TITLE },
166 ),
167 Attr::android("excludeFromRecents", attr::EXCLUDE_FROM_RECENTS, b(true)),
168 Attr::android("noHistory", attr::NO_HISTORY, b(true)),
169 Attr::android("exported", attr::EXPORTED, b(false)),
170 ],
171 ));
172 app.push(Element::new(
173 "service",
174 vec![
175 Attr::android("name", attr::NAME, s("com.andysu.host.UpdateJobService")),
176 Attr::android("permission", attr::PERMISSION, s("android.permission.BIND_JOB_SERVICE")),
177 Attr::android("exported", attr::EXPORTED, b(false)),
178 ],
179 ));
180 let mut recv = Element::new(
181 "receiver",
182 vec![
183 Attr::android("name", attr::NAME, s("com.andysu.host.ShellInstallReceiver")),
184 Attr::android("exported", attr::EXPORTED, b(false)),
185 ],
186 );
187 let mut f = Element::new("intent-filter", vec![]);
188 f.push(Element::new("action", vec![Attr::android("name", attr::NAME, s("com.andysu.host.SHELL_INSTALL_STATUS"))]));
189 recv.push(f);
190 app.push(recv);
191 app.push(Element::new(
192 "provider",
193 vec![
194 Attr::android("name", attr::NAME, s("com.andysu.host.StubFileProvider")),
195 Attr::android("exported", attr::EXPORTED, b(false)),
196 Attr::android("authorities", attr::AUTHORITIES, s(&format!("{pkg}.andysu.files"))),
197 Attr::android("grantUriPermissions", attr::GRANT_URI_PERMISSIONS, b(true)),
198 ],
199 ));
200}
201
202pub fn transform_manifest(orig: &[u8], rep: &mut HostReport, cleartext: bool) -> R<Vec<u8>> {
203 let mut doc = axml::parse(orig)?;
204 let pkg = doc
205 .root
206 .attrs
207 .iter()
208 .find(|a| a.name == "package")
209 .and_then(|a| a.str())
210 .ok_or("manifest has no package")?
211 .to_string();
212 let have: Vec<String> = doc
214 .root
215 .elements()
216 .filter(|e| e.name == "uses-permission")
217 .filter_map(|e| e.android_name().map(String::from))
218 .collect();
219 let app_pos = doc
220 .root
221 .children
222 .iter()
223 .position(|n| matches!(n, Node::Element(e) if e.name == "application"))
224 .ok_or("manifest has no <application>")?;
225 for (i, p) in HOST_PERMISSIONS.iter().filter(|p| !have.iter().any(|h| h == *p)).enumerate() {
226 doc.root.children.insert(
227 app_pos + i,
228 Node::Element(Element::new("uses-permission", vec![Attr::android("name", attr::NAME, s(p))])),
229 );
230 }
231 let app = doc.root.child_mut("application").unwrap();
232 app.set(Attr::android("name", attr::NAME, s("com.andysu.host.AndySUApplication")));
233 app.remove(attr::APP_COMPONENT_FACTORY);
234 app.remove(attr::HAS_CODE);
235 app.set(Attr::android("extractNativeLibs", attr::EXTRACT_NATIVE_LIBS, b(false)));
237 if cleartext {
238 const USES_CLEARTEXT_TRAFFIC: u32 = 0x0101_04ec;
239 app.set(Attr::android("usesCleartextTraffic", USES_CLEARTEXT_TRAFFIC, b(true)));
240 rep.warnings.push("--cleartext: plain HTTP allowed (testing only)".into());
241 }
242
243 let mut acts: Vec<usize> = app
245 .children
246 .iter()
247 .enumerate()
248 .filter(|(_, n)| matches!(n, Node::Element(e) if e.name == "activity"))
249 .map(|(i, _)| i)
250 .collect();
251 acts.sort_by_key(|&i| if let Node::Element(e) = &app.children[i] { !is_launcher(e) } else { true });
252 let mut renamed: BTreeMap<String, String> = BTreeMap::new();
253 let mut drop_idx = vec![];
254 let (mut ai, mut si, mut ri) = (0usize, 0usize, 0usize);
255 for &i in &acts {
256 let Node::Element(e) = &mut app.children[i] else { continue };
257 let old = full_name(&pkg, e.android_name().unwrap_or(""));
258 if ai >= STUB_ACTIVITIES.len() {
259 return Err(format!(
260 "the app has more than {} activities; this needs more stub activities in the host",
261 STUB_ACTIVITIES.len()
262 ));
263 }
264 e.set(Attr::android("name", attr::NAME, s(STUB_ACTIVITIES[ai])));
265 renamed.insert(old, STUB_ACTIVITIES[ai].into());
266 ai += 1;
267 }
268 for (i, n) in app.children.iter_mut().enumerate() {
269 let Node::Element(e) = n else { continue };
270 let old = full_name(&pkg, e.android_name().unwrap_or(""));
271 match e.name.as_str() {
272 "service" | "receiver" => {
273 let (stubs, k): (&[&str], &mut usize) =
274 if e.name == "service" { (&STUB_SERVICES, &mut si) } else { (&STUB_RECEIVERS, &mut ri) };
275 if *k < stubs.len() {
276 e.set(Attr::android("name", attr::NAME, s(stubs[*k])));
277 renamed.insert(old, stubs[*k].into());
278 *k += 1;
279 } else {
280 drop_idx.push(i);
281 rep.dropped.push(old);
282 }
283 }
284 "provider" => {
285 drop_idx.push(i);
286 rep.dropped.push(old);
287 }
288 "activity-alias" => {
289 const TARGET_ACTIVITY: u32 = 0x0101_0202;
291 if let Some(t) = e.attr(TARGET_ACTIVITY).and_then(|a| a.str()).map(|t| full_name(&pkg, t)) {
292 if let Some(new) = renamed.get(&t) {
293 let new = new.clone();
294 e.set(Attr::android("targetActivity", TARGET_ACTIVITY, s(&new)));
295 }
296 }
297 }
298 _ => {}
299 }
300 }
301 for i in drop_idx.into_iter().rev() {
302 app.children.remove(i);
303 }
304 if !rep.dropped.is_empty() {
305 rep.warnings.push(format!(
306 "{} component(s) cannot run from a host stub and were left out: {}. Payload code can do their work from Payload.start().",
307 rep.dropped.len(),
308 rep.dropped.join(", ")
309 ));
310 }
311 for (k, st) in STUB_ACTIVITIES.iter().enumerate().skip(ai) {
314 let launcher = k == 0;
315 let mut e = Element::new(
316 "activity",
317 vec![Attr::android("name", attr::NAME, s(st)), Attr::android("exported", attr::EXPORTED, b(launcher))],
318 );
319 if launcher {
320 let mut f = Element::new("intent-filter", vec![]);
321 f.push(Element::new("action", vec![Attr::android("name", attr::NAME, s("android.intent.action.MAIN"))]));
322 f.push(Element::new(
323 "category",
324 vec![Attr::android("name", attr::NAME, s("android.intent.category.LAUNCHER"))],
325 ));
326 e.push(f);
327 }
328 app.push(e);
329 }
330 for st in STUB_SERVICES.iter().skip(si) {
331 app.push(Element::new(
332 "service",
333 vec![Attr::android("name", attr::NAME, s(st)), Attr::android("exported", attr::EXPORTED, b(false))],
334 ));
335 }
336 for st in STUB_RECEIVERS.iter().skip(ri) {
337 app.push(Element::new(
338 "receiver",
339 vec![Attr::android("name", attr::NAME, s(st)), Attr::android("exported", attr::EXPORTED, b(false))],
340 ));
341 }
342 push_host_components(app, &pkg);
343 rep.stubs = renamed;
344 Ok(axml::write(&doc))
345}
346
347fn is_signature(n: &str) -> bool {
348 n.starts_with("META-INF/") && [".SF", ".RSA", ".EC", ".DSA", "MANIFEST.MF"].iter().any(|x| n.ends_with(x))
349}
350
351fn apksigner() -> Option<PathBuf> {
352 let sdk = std::env::var("ANDROID_HOME")
353 .ok()
354 .map(PathBuf::from)
355 .or_else(|| std::env::var("HOME").ok().map(|h| PathBuf::from(h).join("Android/Sdk")))?;
356 let mut v: Vec<PathBuf> = std::fs::read_dir(sdk.join("build-tools"))
357 .ok()?
358 .flatten()
359 .map(|e| e.path().join("apksigner"))
360 .filter(|p| p.exists())
361 .collect();
362 v.sort();
363 v.pop()
364}
365
366pub fn build(orig_apk: &Path, o: &HostOptions) -> R<HostReport> {
367 let mut rep = HostReport::default();
368 let mut z =
369 zip::ZipArchive::new(std::fs::File::open(orig_apk).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
370 let mut tpl =
371 zip::ZipArchive::new(std::fs::File::open(&o.template).map_err(|e| format!("{}: {e}", o.template.display()))?)
372 .map_err(|e| e.to_string())?;
373 let mut manifest = vec![];
374 z.by_name("AndroidManifest.xml")
375 .map_err(|e| e.to_string())?
376 .read_to_end(&mut manifest)
377 .map_err(|e| e.to_string())?;
378 let new_manifest = if o.transparent {
379 transform_transparent(&manifest, &mut rep, o.cleartext, o.payload_has_assets)?
380 } else {
381 transform_manifest(&manifest, &mut rep, o.cleartext)?
382 };
383
384 let orig_abis: Vec<String> = {
385 let mut v: Vec<String> = z
386 .file_names()
387 .filter_map(|n| n.strip_prefix("lib/"))
388 .filter_map(|r| r.split('/').next())
389 .map(String::from)
390 .collect();
391 v.sort();
392 v.dedup();
393 v
394 };
395 let tpl_names: Vec<String> = tpl.file_names().map(String::from).collect();
396 let tpl_abis: Vec<String> = {
397 let mut v: Vec<String> = tpl_names
398 .iter()
399 .filter_map(|n| n.strip_prefix("lib/"))
400 .filter_map(|r| r.split('/').next())
401 .map(String::from)
402 .collect();
403 v.sort();
404 v.dedup();
405 v
406 };
407 rep.abis = if orig_abis.is_empty() {
408 tpl_abis.clone()
409 } else {
410 orig_abis.iter().filter(|a| tpl_abis.contains(a)).cloned().collect()
411 };
412 for a in &orig_abis {
413 if !tpl_abis.contains(a) {
414 rep.warnings.push(format!("the host template has no {a} build; that ABI is left out"));
415 }
416 }
417
418 let unsigned = o.out.with_extension("unsigned.apk");
419 {
420 let f = std::fs::File::create(&unsigned).map_err(|e| e.to_string())?;
421 let mut w = zip::ZipWriter::new(f);
422 let deflate = zip::write::SimpleFileOptions::default().compression_method(zip::CompressionMethod::Deflated);
423 let stored4 = zip::write::SimpleFileOptions::default()
424 .compression_method(zip::CompressionMethod::Stored)
425 .with_alignment(4);
426 let stored16k = zip::write::SimpleFileOptions::default()
429 .compression_method(zip::CompressionMethod::Stored)
430 .with_alignment(16384);
431 w.start_file("AndroidManifest.xml", deflate).map_err(|e| e.to_string())?;
432 w.write_all(&new_manifest).map_err(|e| e.to_string())?;
433 for i in 0..z.len() {
434 let e = z.by_index_raw(i).map_err(|e| e.to_string())?;
435 let n = e.name().to_string();
436 drop(e);
437 if n == "resources.arsc" {
438 let mut data = vec![];
440 z.by_index(i).map_err(|e| e.to_string())?.read_to_end(&mut data).map_err(|e| e.to_string())?;
441 w.start_file(n.as_str(), stored4).map_err(|e| e.to_string())?;
442 w.write_all(&data).map_err(|e| e.to_string())?;
443 } else if n.starts_with("res/") && !n.ends_with('/') && !is_signature(&n) {
444 w.raw_copy_file(z.by_index_raw(i).map_err(|e| e.to_string())?).map_err(|e| e.to_string())?;
445 }
446 }
447 for n in &tpl_names {
448 let keep = (n.starts_with("classes") && n.ends_with(".dex"))
449 || n.strip_prefix("lib/").is_some_and(|r| rep.abis.iter().any(|a| r.starts_with(&format!("{a}/"))));
450 if keep && !n.ends_with('/') {
451 let mut data = vec![];
452 tpl.by_name(n).map_err(|e| e.to_string())?.read_to_end(&mut data).map_err(|e| e.to_string())?;
453 w.start_file(n.as_str(), if n.ends_with(".so") { stored16k } else { deflate })
454 .map_err(|e| e.to_string())?;
455 w.write_all(&data).map_err(|e| e.to_string())?;
456 }
457 }
458 w.start_file("assets/andysu/config.json", deflate).map_err(|e| e.to_string())?;
459 w.write_all(&read(&o.config)?).map_err(|e| e.to_string())?;
460 for rel in crate::pack::walk(&o.factory)? {
461 w.start_file(format!("assets/andysu-factory/{rel}"), deflate).map_err(|e| e.to_string())?;
462 w.write_all(&read(&o.factory.join(&rel))?).map_err(|e| e.to_string())?;
463 }
464 w.finish().map_err(|e| e.to_string())?;
465 }
466 match &o.keystore {
467 Some(ks) => {
468 let signer = apksigner().ok_or("apksigner not found (set ANDROID_HOME)")?;
469 let mut c = Command::new(signer);
470 c.args(["sign", "--ks"]).arg(ks);
471 if let Some(p) = &o.ks_pass {
472 c.args(["--ks-pass", p]);
473 }
474 if let Some(a) = &o.key_alias {
475 c.args(["--ks-key-alias", a]);
476 }
477 c.arg("--out").arg(&o.out).arg(&unsigned);
478 let r = c.output().map_err(|e| e.to_string())?;
479 if !r.status.success() {
480 return Err(format!("apksigner failed: {}", String::from_utf8_lossy(&r.stderr)));
481 }
482 let _ = std::fs::remove_file(&unsigned);
483 rep.signed = true;
484 rep.apk = o.out.display().to_string();
485 }
486 None => {
487 rep.apk = unsigned.display().to_string();
488 rep.warnings
489 .push("no --keystore: the host APK is unsigned; sign it with the app's own key (apksigner)".into());
490 }
491 }
492 Ok(rep)
493}