1 use crate::capabilities::AndroidOptions;
2 use mozdevice::{AndroidStorage, Device, Host, UnixPathBuf};
3 use mozprofile::profile::Profile;
4 use serde::Serialize;
5 use serde_yaml::{Mapping, Value};
6 use std::fmt;
7 use std::io;
8 use std::time;
9 use webdriver::error::{ErrorStatus, WebDriverError};
10 
11 // TODO: avoid port clashes across GeckoView-vehicles.
12 // For now, we always use target port 2829, leading to issues like bug 1533704.
13 const MARIONETTE_TARGET_PORT: u16 = 2829;
14 
15 const CONFIG_FILE_HEADING: &str = r#"## GeckoView configuration YAML
16 ##
17 ## Auto-generated by geckodriver.
18 ## See https://mozilla.github.io/geckoview/consumer/docs/automation.
19 "#;
20 
21 pub type Result<T> = std::result::Result<T, AndroidError>;
22 
23 #[derive(Debug)]
24 pub enum AndroidError {
25     ActivityNotFound(String),
26     Device(mozdevice::DeviceError),
27     IO(io::Error),
28     PackageNotFound(String),
29     Serde(serde_yaml::Error),
30 }
31 
32 impl fmt::Display for AndroidError {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result33     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
34         match *self {
35             AndroidError::ActivityNotFound(ref package) => {
36                 write!(f, "Activity for package '{}' not found", package)
37             }
38             AndroidError::Device(ref message) => message.fmt(f),
39             AndroidError::IO(ref message) => message.fmt(f),
40             AndroidError::PackageNotFound(ref package) => {
41                 write!(f, "Package '{}' not found", package)
42             }
43             AndroidError::Serde(ref message) => message.fmt(f),
44         }
45     }
46 }
47 
48 impl From<io::Error> for AndroidError {
from(value: io::Error) -> AndroidError49     fn from(value: io::Error) -> AndroidError {
50         AndroidError::IO(value)
51     }
52 }
53 
54 impl From<mozdevice::DeviceError> for AndroidError {
from(value: mozdevice::DeviceError) -> AndroidError55     fn from(value: mozdevice::DeviceError) -> AndroidError {
56         AndroidError::Device(value)
57     }
58 }
59 
60 impl From<serde_yaml::Error> for AndroidError {
from(value: serde_yaml::Error) -> AndroidError61     fn from(value: serde_yaml::Error) -> AndroidError {
62         AndroidError::Serde(value)
63     }
64 }
65 
66 impl From<AndroidError> for WebDriverError {
from(value: AndroidError) -> WebDriverError67     fn from(value: AndroidError) -> WebDriverError {
68         WebDriverError::new(ErrorStatus::UnknownError, value.to_string())
69     }
70 }
71 
72 /// A remote Gecko instance.
73 ///
74 /// Host refers to the device running `geckodriver`.  Target refers to the
75 /// Android device running Gecko in a GeckoView-based vehicle.
76 #[derive(Debug)]
77 pub struct AndroidProcess {
78     pub device: Device,
79     pub package: String,
80     pub activity: String,
81 }
82 
83 impl AndroidProcess {
new( device: Device, package: String, activity: String, ) -> mozdevice::Result<AndroidProcess>84     pub fn new(
85         device: Device,
86         package: String,
87         activity: String,
88     ) -> mozdevice::Result<AndroidProcess> {
89         Ok(AndroidProcess {
90             device,
91             package,
92             activity,
93         })
94     }
95 }
96 
97 #[derive(Debug)]
98 pub struct AndroidHandler {
99     pub config: UnixPathBuf,
100     pub options: AndroidOptions,
101     pub process: AndroidProcess,
102     pub profile: UnixPathBuf,
103     pub test_root: UnixPathBuf,
104 
105     // Port forwarding for Marionette: host => target
106     pub marionette_host_port: u16,
107     pub marionette_target_port: u16,
108 
109     // Port forwarding for WebSocket connections (WebDriver BiDi and CDP)
110     pub websocket_port: Option<u16>,
111 }
112 
113 impl Drop for AndroidHandler {
drop(&mut self)114     fn drop(&mut self) {
115         // Try to clean up various settings
116         let clear_command = format!("am clear-debug-app {}", self.process.package);
117         match self
118             .process
119             .device
120             .execute_host_shell_command(&clear_command)
121         {
122             Ok(_) => debug!("Disabled reading from configuration file"),
123             Err(e) => error!("Failed disabling from configuration file: {}", e),
124         }
125 
126         match self.process.device.remove(&self.config) {
127             Ok(_) => debug!("Deleted GeckoView configuration file"),
128             Err(e) => error!("Failed deleting GeckoView configuration file: {}", e),
129         }
130 
131         match self.process.device.remove(&self.test_root) {
132             Ok(_) => debug!("Deleted test root folder: {}", &self.test_root.display()),
133             Err(e) => error!("Failed deleting test root folder: {}", e),
134         }
135 
136         match self
137             .process
138             .device
139             .kill_forward_port(self.marionette_host_port)
140         {
141             Ok(_) => debug!(
142                 "Marionette port forward ({} -> {}) stopped",
143                 &self.marionette_host_port, &self.marionette_target_port
144             ),
145             Err(e) => error!(
146                 "Marionette port forward ({} -> {}) failed to stop: {}",
147                 &self.marionette_host_port, &self.marionette_target_port, e
148             ),
149         }
150 
151         if let Some(port) = self.websocket_port {
152             match self.process.device.kill_forward_port(port) {
153                 Ok(_) => debug!("WebSocket port forward ({0} -> {0}) stopped", &port),
154                 Err(e) => error!(
155                     "WebSocket port forward ({0} -> {0}) failed to stop: {1}",
156                     &port, e
157                 ),
158             }
159         }
160     }
161 }
162 
163 impl AndroidHandler {
new( options: &AndroidOptions, marionette_host_port: u16, websocket_port: Option<u16>, ) -> Result<AndroidHandler>164     pub fn new(
165         options: &AndroidOptions,
166         marionette_host_port: u16,
167         websocket_port: Option<u16>,
168     ) -> Result<AndroidHandler> {
169         // We need to push profile.pathbuf to a safe space on the device.
170         // Make it per-Android package to avoid clashes and confusion.
171         // This naming scheme follows GeckoView's configuration file naming scheme,
172         // see bug 1533385.
173 
174         let host = Host {
175             host: None,
176             port: None,
177             read_timeout: Some(time::Duration::from_millis(5000)),
178             write_timeout: Some(time::Duration::from_millis(5000)),
179         };
180 
181         let mut device = host.device_or_default(options.device_serial.as_ref(), options.storage)?;
182 
183         // Set up port forwarding for Marionette.
184         device.forward_port(marionette_host_port, MARIONETTE_TARGET_PORT)?;
185         debug!(
186             "Marionette port forward ({} -> {}) started",
187             marionette_host_port, MARIONETTE_TARGET_PORT
188         );
189 
190         if let Some(port) = websocket_port {
191             // Set up port forwarding for WebSocket connections (WebDriver BiDi, and CDP).
192             device.forward_port(port, port)?;
193             debug!("WebSocket port forward ({} -> {}) started", port, port);
194         }
195 
196         let test_root = match device.storage {
197             AndroidStorage::App => {
198                 device.run_as_package = Some(options.package.to_owned());
199                 let mut buf = UnixPathBuf::from("/data/data");
200                 buf.push(&options.package);
201                 buf.push("test_root");
202                 buf
203             }
204             AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"),
205             AndroidStorage::Sdcard => {
206                 // We need to push the profile to a location on the device that can also
207                 // be read and write by the application, and works for unrooted devices.
208                 // The only location that meets this criteria is under:
209                 //     $EXTERNAL_STORAGE/Android/data/%options.package%/files
210                 let response = device.execute_host_shell_command("echo $EXTERNAL_STORAGE")?;
211                 let mut buf = UnixPathBuf::from(response.trim_end_matches('\n'));
212                 buf.push("Android/data");
213                 buf.push(&options.package);
214                 buf.push("files/test_root");
215                 buf
216             }
217         };
218 
219         debug!(
220             "Connecting: options={:?}, storage={:?}) test_root={}, run_as_package={:?}",
221             options,
222             device.storage,
223             test_root.display(),
224             device.run_as_package
225         );
226 
227         let mut profile = test_root.clone();
228         profile.push(format!("{}-geckodriver-profile", &options.package));
229 
230         // Check if the specified package is installed
231         let response =
232             device.execute_host_shell_command(&format!("pm list packages {}", &options.package))?;
233         let mut packages = response
234             .trim()
235             .split_terminator('\n')
236             .filter(|line| line.starts_with("package:"))
237             .map(|line| line.rsplit(':').next().expect("Package name found"));
238         if !packages.any(|x| x == options.package.as_str()) {
239             return Err(AndroidError::PackageNotFound(options.package.clone()));
240         }
241 
242         let config = UnixPathBuf::from(format!(
243             "/data/local/tmp/{}-geckoview-config.yaml",
244             &options.package
245         ));
246 
247         // If activity hasn't been specified default to the main activity of the package
248         let activity = match options.activity {
249             Some(ref activity) => activity.clone(),
250             None => {
251                 let response = device.execute_host_shell_command(&format!(
252                     "cmd package resolve-activity --brief {}",
253                     &options.package
254                 ))?;
255                 let activities = response
256                     .split_terminator('\n')
257                     .filter(|line| line.starts_with(&options.package))
258                     .map(|line| line.rsplit('/').next().unwrap())
259                     .collect::<Vec<&str>>();
260                 if activities.is_empty() {
261                     return Err(AndroidError::ActivityNotFound(options.package.clone()));
262                 }
263 
264                 activities[0].to_owned()
265             }
266         };
267 
268         let process = AndroidProcess::new(device, options.package.clone(), activity)?;
269 
270         Ok(AndroidHandler {
271             config,
272             process,
273             profile,
274             test_root,
275             marionette_host_port,
276             marionette_target_port: MARIONETTE_TARGET_PORT,
277             options: options.clone(),
278             websocket_port,
279         })
280     }
281 
generate_config_file<I, K, V>( &self, args: Option<Vec<String>>, envs: I, ) -> Result<String> where I: IntoIterator<Item = (K, V)>, K: ToString, V: ToString,282     pub fn generate_config_file<I, K, V>(
283         &self,
284         args: Option<Vec<String>>,
285         envs: I,
286     ) -> Result<String>
287     where
288         I: IntoIterator<Item = (K, V)>,
289         K: ToString,
290         V: ToString,
291     {
292         // To configure GeckoView, we use the automation techniques documented at
293         // https://mozilla.github.io/geckoview/consumer/docs/automation.
294         #[derive(Serialize, Deserialize, PartialEq, Debug)]
295         pub struct Config {
296             pub env: Mapping,
297             pub args: Vec<String>,
298         }
299 
300         let mut config = Config {
301             args: vec![
302                 "--marionette".into(),
303                 "--profile".into(),
304                 self.profile.display().to_string(),
305             ],
306             env: Mapping::new(),
307         };
308 
309         config.args.append(&mut args.unwrap_or_default());
310 
311         for (key, value) in envs {
312             config.env.insert(
313                 Value::String(key.to_string()),
314                 Value::String(value.to_string()),
315             );
316         }
317 
318         config.env.insert(
319             Value::String("MOZ_CRASHREPORTER".to_owned()),
320             Value::String("1".to_owned()),
321         );
322         config.env.insert(
323             Value::String("MOZ_CRASHREPORTER_NO_REPORT".to_owned()),
324             Value::String("1".to_owned()),
325         );
326         config.env.insert(
327             Value::String("MOZ_CRASHREPORTER_SHUTDOWN".to_owned()),
328             Value::String("1".to_owned()),
329         );
330 
331         let mut contents: Vec<String> = vec![CONFIG_FILE_HEADING.to_owned()];
332         contents.push(serde_yaml::to_string(&config)?);
333 
334         Ok(contents.concat())
335     }
336 
prepare<I, K, V>( &self, profile: &Profile, args: Option<Vec<String>>, env: I, ) -> Result<()> where I: IntoIterator<Item = (K, V)>, K: ToString, V: ToString,337     pub fn prepare<I, K, V>(
338         &self,
339         profile: &Profile,
340         args: Option<Vec<String>>,
341         env: I,
342     ) -> Result<()>
343     where
344         I: IntoIterator<Item = (K, V)>,
345         K: ToString,
346         V: ToString,
347     {
348         self.process.device.clear_app_data(&self.process.package)?;
349 
350         // These permissions, at least, are required to read profiles in /mnt/sdcard.
351         for perm in &["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"] {
352             self.process.device.execute_host_shell_command(&format!(
353                 "pm grant {} android.permission.{}",
354                 &self.process.package, perm
355             ))?;
356         }
357 
358         // Make sure to create the test root.
359         self.process.device.create_dir(&self.test_root)?;
360         self.process.device.chmod(&self.test_root, "777", true)?;
361 
362         // Replace the profile
363         self.process.device.remove(&self.profile)?;
364         self.process
365             .device
366             .push_dir(&profile.path, &self.profile, 0o777)?;
367 
368         let contents = self.generate_config_file(args, env)?;
369         debug!("Content of generated GeckoView config file:\n{}", contents);
370         let reader = &mut io::BufReader::new(contents.as_bytes());
371 
372         debug!(
373             "Pushing GeckoView configuration file to {}",
374             self.config.display()
375         );
376         self.process.device.push(reader, &self.config, 0o777)?;
377 
378         // Tell GeckoView to read configuration even when `android:debuggable="false"`.
379         self.process.device.execute_host_shell_command(&format!(
380             "am set-debug-app --persistent {}",
381             self.process.package
382         ))?;
383 
384         Ok(())
385     }
386 
launch(&self) -> Result<()>387     pub fn launch(&self) -> Result<()> {
388         // TODO: Remove the usage of intent arguments once Fennec is no longer
389         // supported. Packages which are using GeckoView always read the arguments
390         // via the YAML configuration file.
391         let mut intent_arguments = self
392             .options
393             .intent_arguments
394             .clone()
395             .unwrap_or_else(|| Vec::with_capacity(3));
396         intent_arguments.push("--es".to_owned());
397         intent_arguments.push("args".to_owned());
398         intent_arguments.push(format!("--marionette --profile {}", self.profile.display()));
399 
400         debug!(
401             "Launching {}/{}",
402             self.process.package, self.process.activity
403         );
404         self.process
405             .device
406             .launch(
407                 &self.process.package,
408                 &self.process.activity,
409                 &intent_arguments,
410             )
411             .map_err(|e| {
412                 let message = format!(
413                     "Could not launch Android {}/{}: {}",
414                     self.process.package, self.process.activity, e
415                 );
416                 mozdevice::DeviceError::Adb(message)
417             })?;
418 
419         Ok(())
420     }
421 
force_stop(&self) -> Result<()>422     pub fn force_stop(&self) -> Result<()> {
423         debug!(
424             "Force stopping the Android package: {}",
425             &self.process.package
426         );
427         self.process.device.force_stop(&self.process.package)?;
428 
429         Ok(())
430     }
431 }
432 
433 #[cfg(test)]
434 mod test {
435     // To successfully run those tests the geckoview_example package needs to
436     // be installed on the device or emulator. After setting up the build
437     // environment (https://mzl.la/3muLv5M), the following mach commands have to
438     // be executed:
439     //
440     //     $ ./mach build && ./mach install
441     //
442     // Currently the mozdevice API is not safe for multiple requests at the same
443     // time. It is recommended to run each of the unit tests on its own. Also adb
444     // specific tests cannot be run in CI yet. To check those locally, also run
445     // the ignored tests.
446     //
447     // Use the following command to accomplish that:
448     //
449     //     $ cargo test -- --ignored --test-threads=1
450 
451     use crate::android::AndroidHandler;
452     use crate::capabilities::AndroidOptions;
453     use mozdevice::{AndroidStorage, AndroidStorageInput, UnixPathBuf};
454 
run_handler_storage_test(package: &str, storage: AndroidStorageInput)455     fn run_handler_storage_test(package: &str, storage: AndroidStorageInput) {
456         let options = AndroidOptions::new(package.to_owned(), storage);
457         let handler = AndroidHandler::new(&options, 4242, None).expect("has valid Android handler");
458 
459         assert_eq!(handler.options, options);
460         assert_eq!(handler.process.package, package);
461 
462         let expected_config_path = UnixPathBuf::from(format!(
463             "/data/local/tmp/{}-geckoview-config.yaml",
464             &package
465         ));
466         assert_eq!(handler.config, expected_config_path);
467 
468         if handler.process.device.storage == AndroidStorage::App {
469             assert_eq!(
470                 handler.process.device.run_as_package,
471                 Some(package.to_owned())
472             );
473         } else {
474             assert_eq!(handler.process.device.run_as_package, None);
475         }
476 
477         let test_root = match handler.process.device.storage {
478             AndroidStorage::App => {
479                 let mut buf = UnixPathBuf::from("/data/data");
480                 buf.push(&package);
481                 buf.push("test_root");
482                 buf
483             }
484             AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"),
485             AndroidStorage::Sdcard => {
486                 let response = handler
487                     .process
488                     .device
489                     .execute_host_shell_command("echo $EXTERNAL_STORAGE")
490                     .unwrap();
491 
492                 let mut buf = UnixPathBuf::from(response.trim_end_matches('\n'));
493                 buf.push("Android/data/");
494                 buf.push(&package);
495                 buf.push("files/test_root");
496                 buf
497             }
498         };
499         assert_eq!(handler.test_root, test_root);
500 
501         let mut profile = test_root;
502         profile.push(format!("{}-geckodriver-profile", &package));
503         assert_eq!(handler.profile, profile);
504     }
505 
506     #[test]
507     #[ignore]
android_handler_storage_as_app()508     fn android_handler_storage_as_app() {
509         let package = "org.mozilla.geckoview_example";
510         run_handler_storage_test(package, AndroidStorageInput::App);
511     }
512 
513     #[test]
514     #[ignore]
android_handler_storage_as_auto()515     fn android_handler_storage_as_auto() {
516         let package = "org.mozilla.geckoview_example";
517         run_handler_storage_test(package, AndroidStorageInput::Auto);
518     }
519 
520     #[test]
521     #[ignore]
android_handler_storage_as_internal()522     fn android_handler_storage_as_internal() {
523         let package = "org.mozilla.geckoview_example";
524         run_handler_storage_test(package, AndroidStorageInput::Internal);
525     }
526 
527     #[test]
528     #[ignore]
android_handler_storage_as_sdcard()529     fn android_handler_storage_as_sdcard() {
530         let package = "org.mozilla.geckoview_example";
531         run_handler_storage_test(package, AndroidStorageInput::Sdcard);
532     }
533 }
534