Android-ElectricSheep-Automator

 view release on metacpan or  search on metacpan

lib/Android/ElectricSheep/Automator.pm  view on Meta::CPAN

use Mojo::Log;
use Config::JSON::Enhanced;
# it requires v0.002 (which is with my modifications)
#use Android::ADB;
# issue filed for Android::ADB :
#   https://rt.cpan.org/Public/Bug/Display.html?id=163391
# until this is resolved I am copying Android::ADB
# into my distribution, fixing the issues and renaming it to
# Android::ElectricSheep::Automator::ADB
# and using that. When the issue is resolved I will go back
# using Android::ADB
# Credits for Android::ADB (now Android::ElectricSheep::Automator::ADB)
# go to Marius Gavrilescu (marius@ieval.ro)
# as seen in:
#   https://metacpan.org/pod/Android::ADB
#
use Android::ElectricSheep::Automator::ADB;
use File::Temp qw/tempfile/;
use File::Basename;
use File::Spec;
use File::Path qw/make_path/;
use Cwd;
use FindBin;
use Time::HiRes qw/usleep/;
use Image::PNG;
use XML::LibXML;
use XML::LibXML::XPathContext;
use Text::ParseWords;

use Data::Roundtrip qw/perl2dump perl2json no-unicode-escape-permanently/;

use Android::ElectricSheep::Automator::DeviceProperties;
use Android::ElectricSheep::Automator::AppProperties;
use Android::ElectricSheep::Automator::ScreenLayout;
use Android::ElectricSheep::Automator::XMLParsers;

my $_DEFAULT_CONFIG = <<'EODC';
</* $VERSION = '0.09'; */>
</* comments are allowed */>
</* and <% vars %> and <% verbatim sections %> */>
{
	"adb" : {
		"path-to-executable" : "/usr/local/android-sdk/platform-tools/adb"
	},
	"debug" : {
		"verbosity" : 0,
		</* cleanup temp files on exit */>
		"cleanup" : 1
	},
	"logger" : {
		</* log to file if you uncomment this */>
		</* "filename" : "..." */>
	}
	</* config for our plugins (each can go to separate file also) */>
}
EODC

# NOTE: by default, it assumes that no device is connected
# and so it does not enquire about screen size etc on startup
# In order to tell it that a device (just one)
# is connected to the desktop and that we should connect to
# it, 
#   use param 'device-is-connected' => 1
# if there are more than one devices and you want to connect to
# one of them, then 
#  use param 'device-serial' => <serial-of-device-to-connect>
# or
#  use param 'device-object' => <device object>
#        (of type Android::ElectricSheep::Automator::ADB::Device)
# or after instantiation with $obj->connect_device(...);
# and similarly for disconnect_device()
# NOTE: without connecting to a device you can not use
#       methods which require a connected device, e.g. open_app(), swipe() etc.
sub new {
	my $class = ref($_[0]) || $_[0]; # aka proto
	my $params = $_[1] // {};

        my $parent = ( caller(1) )[3] || "N/A";
        my $whoami = ( caller(0) )[3];

	my $self = {
		'_private' => {
			'confighash' => undef,
			'configfile' => '', # this should never be undef
			'Android::ADB' => undef,
			'debug' => {
				'verbosity' => 0,
				'cleanup' => 1,
			},
			'log' => {
				'logger-object' => undef,
				'logfile' => undef
			},
		},

		# object of type Android::ElectricSheep::Automator::DeviceProperties
		# if this is undef, then it means caller did not call connect_device()
		# when caller calls disconnect_device(), this becomes undef again
		# this is a cheap way to not proceed to device-needed subs, e.g. swipe()
		# of course we could make an adb query with e.g. adb get-state
		'device-properties' => undef,

		# object of type Android::ElectricSheep::Automator::ADB::Device
		# which is created when we call connect_device()
		'device-object' => undef,

		# a hash of installed apps by package name (e.g. android.google.calendar)
		# the value will be an AppProperties object if it was enquired or undef
		# if it wasn't. As the addition of apps is done in a lazy way, when
		# needed, unless specified otherwise. In any event open_app() will add an
		# AppProperties object if missing to the specified package.
		'apps' => {},

		# legacy, no worries.
		'apps-roundabout-way' => undef,
	};
	bless $self => $class;

	# this will read configuration and create confighash,
	# make logger, verbosity,
	# instantiate any objects we need here etc.
	if( $self->init($params) ){ print STDERR __PACKAGE__."${whoami} (via $parent), line ".__LINE__." : error, call to init() has failed.\n"; return undef }

	# Now we have a logger
	my $log = $self->log();

	# do module-specific init
	if( $self->init_module_specific($params) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to init_module_specific() has failed."); return undef }

	# optional params, defaults exist above or in the configfile
	if( exists($params->{'verbosity'}) && defined($params->{'verbosity'}) ){ $self->verbosity($params->{'verbosity'}) } # later we will call verbosity()
	if( exists($params->{'cleanup'}) && defined($params->{'cleanup'}) ){ $self->cleanup($params->{'cleanup'}) }
	else { $self->cleanup($self->confighash->{'debug'}->{'cleanup'}) }

	my $verbosity = $self->verbosity;

	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : done, success (verbosity is set to ".$self->verbosity." and cleanup to ".$self->cleanup.").") }

	return $self;
}

# This signals our object that there is at least one device connected
# to the desktop which ADB can access and so can we.
# set the device by specifying one of
#  'serial' : the device's serial
#  'device-object' : a Android::ADB::Device object
#     as returned by any item of $self->adb->devices()
# However, if there is ONLY ONE device connected to the desktop, then
# you do not need to specify a device, use this method without arguments
#
# It returns the device object (Android::ADB::Device) on success
# or undef on failure
sub connect_device {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];

	my $log = $self->log();
	my $verbosity = $self->verbosity;

	my ($what_device, $m);

	if( exists($params->{'serial'}) && defined($m=$params->{'serial'}) ){
		my $devs = $self->devices();
		
		for (@$devs){
			if( $_->serial eq $m ){ $what_device = $_; last }
		}
		if( ! defined $what_device ){ $log->error(devices_toString($devs)."\n${whoami} (via $parent), line ".__LINE__." : error, there is no device with specified serial '$m', above are all the connected devices."); return undef }
	} elsif( exists($params->{'device-object'}) && defined($m=$params->{'device-object'})
	      && (ref($params->{'device-object'})eq'Android::ElectricSheep::Automator::ADB::Device')
	){
		$what_device = $m
	} else {
		# no params means we assume there is exactly 1 device connected to the desktop
		my $devs = $self->devices();
		if( scalar(@$devs) == 1 ){
			$what_device = $devs->[0];
		} else { $log->error("${whoami} (via $parent), line ".__LINE__." : error, expecting exactly one device connected to the desktop but found ".scalar(@$devs)." instead. In the case of more than one devices connected to the desktop then specify which o...
	}

	# this can die
	my $res = eval { $self->adb->set_device($what_device) };
	if( $@ || ! defined $res ){ $log->error(device_toString($what_device)."\n${whoami} (via $parent), line ".__LINE__." : error, call to ".'adb->set_device()'." has failed for above device."); return undef }

	# and get the device properties of the set device
	# that method will also set $self->{'device-properties'} to the returned object
	my $device_properties = $self->find_current_device_properties();
	if( ! defined $device_properties ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'find_current_device_properties()'." has failed."); return 1; }

	$self->{'device-object'} = $what_device;

	return $what_device; # Android::ADB::Device object
}

sub devices_toString { join("\n", map { device_toString($_) } @{$_[0]}) }
sub device_toString { join('/', $_[0]->serial, $_[0]->product, $_[0]->model, $_[0]->device) }

# returns the device properties object
# use $self->device_properties->get('w') to get or ->set('w', 12) to set.
sub device_properties { return $_[0]->{'device-properties'} }

# it dumps the current screen UI as XML and returns that as a scalar string,
# optionally saving it to the specified file
# It returns undef on failure.
# On success, it returns a hash with 2 keys:
#   'raw' : contains the raw XML content (as a string)
#   'XML::LibXML' : contains an XML::LibXML object with the parsed
#                  XML string, ready to do XPath queries
# it needs that connect_device() to have been called prior to this call
sub dump_current_screen_ui {
	my ($self, $params) = @_;

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];

	my $log = $self->log();
	my $verbosity = $self->verbosity;

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'device_connected()'." before calling this."); return undef }

	my $filename = exists($params->{'filename'}) && defined($params->{'filename'}) ? $params->{'filename'} : undef;

	my $FH;
	if( ! defined $filename ){
		($FH, $filename) = tempfile(CLEANUP=>$self->cleanup);
		close $FH;
	}
	# WARNING, you need to wake up the phone before dumping !!!!
	my $devicefile = File::Spec->catfile('/', 'data', 'local', 'tmp', $$.'.xml');

	my (@cmd, $res);

	my $maxiters = 7;
	WMA:
	while( $maxiters-- > 0 ){
		@cmd = ('uiautomator', 'dump', $devicefile);
		if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : maxiters $maxiters : sending command to adb: @cmd") }
		my $res = $self->adb->shell(@cmd);
		if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return und...
		if( $res->[0] != 0 ){ $log->error("--begin result:\n".perl2dump($res)."--end result.\n--begin command:\n".join(" ", @cmd)."\n--end command.\n\n"."${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n"....

		# check twice with a sleep if the dump is there, else
		# we are repeating the previous command to dump the ui
		for(1..2){
			# now check if the dump file is there, sometimes it is not. repeat if not
			if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : maxiters $maxiters : in order to find if the uiautomator dump succeeded : sending command to adb: @cmd") }
			@cmd = ('if', '[', '-f', $devicefile, ']', ';', 'then', 'echo', 'found', ';', 'else', 'echo', 'notfound', ';', 'fi');
			$res = $self->adb->shell(@cmd);
			if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return un...
			if( $res->[0] != 0 ){ $log->error("--begin result:\n".perl2dump($res)."--end result.\n--begin command:\n".join(" ", @cmd)."\n--end command.\n\n"."${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n"...
			if( $res->[1] =~ /^found/ ){
				if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : maxiters $maxiters : found the dump file '$devicefile' on device, stopping the loop.") }
				last WMA
			} else { if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : maxiters $maxiters : DID NOT FIND the dump file '$devicefile' on device, continue the loop until iters reach 0.") } }
			usleep(0.5);
		}
	}
	# This may fail because above dump sometimes fails, no error
	# just not producing any output file. If this persists we
	# can try dumping to stdout.

	# pull the output
	$res = $self->adb->pull($devicefile, $filename);
	if( ! defined $res ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$devicefile' into local file '$filename', because undefined was returned, this should not be happening."); return undef }
	if( $res->[0] != 0 ){ $log->error("--begin result:\n".perl2dump($res)."--end result.\n"."${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$devicefile' into local file '$filename' with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR...

	# remove the device file
	@cmd = ("rm", "-f", $devicefile);
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	$res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	# let's return the string content back
	my $contents;
	if( ! open($FH, '<', $filename) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to open file with dump for reading '$filename', $!"); return undef }
	{ local $/ = undef; $contents = <$FH> } close $FH;

	my $retxmlobj = XML::LibXML->load_xml(string => $contents);
	if( ! defined $retxmlobj ){ $log->error("${contents}\n${whoami} (via $parent), line ".__LINE__." : error, failed to parse above XML content."); return undef }
	my $xc = XML::LibXML::XPathContext->new($retxmlobj);
	$xc->registerFunction('matches', \&Android::ElectricSheep::Automator::XMLParsers::xpath_matches);
	return {
		'raw' => $contents,
		'XML::LibXML' => $retxmlobj,
		'XML::LibXML::XPathContext' => $xc
	}
}

# return an ARRAYref of all connected devices. This array can be empty if none is connected
# but an arrayref is definetely returned.
sub devices {
	my $self = $_[0];
	my @devs = $self->adb->devices();
	return \@devs;
}

# It installs an app specified by its APK file
# it accepts extra parameters as specified in the
# documentation of adb https://developer.android.com/tools/adb
# (search for 'install')
# Inputs parameters:
#   'apk-filename' => this can be a string or an array of strings
#        containing existing APK (.apk) file(s) to be installed on device.
#        Sometimes an app consists of many apks, in this case use the ARRAY mode.
#   'install-params' => an array of installation parameters in the fashion of when shelling out a @cmd
#      each parameter and each of its attributes, if any, must be
#      in a separate item in the array, e.g. ['-r', '-i', 'newpagename']
# it needs that connect_device() to have been called prior to this call
# It returns 1 on failure, 0 on success.
sub install_app {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return 1 }

	my $apkfilename = exists($params->{'apk-filename'}) ? $params->{'apk-filename'} : undef;
	if( ! defined $apkfilename ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'apk-filename' is missing."); return 1 }
	my $installparams = exists($params->{'install-parameters'}) ? $params->{'install-parameters'} : [];

	my @cmd = ('later...');
	for (@$installparams){ push @cmd, $_ }
	if( ref($apkfilename) eq '' ){
		# just one apk file
		push @cmd, $apkfilename;
		$cmd[0] = 'install';
	} elsif( ref($apkfilename) eq 'ARRAY' ){
		push @cmd, @$apkfilename;
		$cmd[0] = 'install-multiple';
	} else { $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'apk-filename' must either be a string scalar or an ARRAY of strings. Instead it is '".ref($apkfilename)."'."); return 1 }
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->run(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above adb command has failed, got undefined result, most likely adb command did not run at all, this should not be happening."); return 1 }
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above adb command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }
	if( $res->[1] !~ /\bSuccess$/s ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above adb command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }
	return 0; # success
}

# It extracts the specified application from the device
# in the form of APK file.
# Inputs parameters:
#   'output-dir' => directory where we save the APK(s)
# these are same as those for search_app():
#   'package' => required package name as a SCALAR (for an exact search)
#     or a regex (qr//) object for regex search including case-insensitive.
#   'force-reload-apps-list' => 0,1 : optionally call find_installed_apps() if > 0
#     but restricted only to the packages match 'package' NOT ALL.
#   'lazy' => 0,1 : pass this lazy value to the find_installed_apps()
#     and be lazy (i.e. without enquiring on each app's specifics
#     and creating an AppProperties object) if ==1
#     or not be lazy if ==0 ...
#     ... (which means an AppProperties object is created for EACH package in the device)
#     Default is force-reload-apps-list=>0
# it needs that connect_device() to have been called prior to this call
# It returns a hash whose keys are package-names it extracted their apk(s)
# and value is an array of 0, 1 or more apk(s) related to the package and which
# are extracted. Each item in that array is a hash with fields 'local-name'
# (the apk saved locally) and 'device-name' (the path of the apk on the device)
sub pull_app_apk_from_device {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my $outdir = exists($params->{'output-dir'}) ? $params->{'output-dir'} : undef;
	if( ! defined $outdir ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'output-dir' is missing."); return undef }

	# this can be regex or absolute string, like what search_apps() takes
	my $package = exists($params->{'package'}) ? $params->{'package'} : undef;
	if( ! defined $package ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'package' is missing."); return undef }

	my $sparams = {
		'package' => $package,
	};
	for ('lazy', 'force-reload-apps-list'){
		if( exists $params->{$_} ){ $sparams->{$_} = $params->{$_} }
	}
	my $searchres = $self->search_app($sparams);
	if( ! defined $searchres ){ $log->error(perl2dump($sparams)."${whoami} (via $parent), line ".__LINE__." : error, call to ".'search_app()'." has failed with above parameters."); return undef }

	# now we should have 1 or more items in the hash for the matched apps
	if( ! -d $outdir ){ File::Path::make_path($outdir); if( ! -d $outdir ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to create output dir '$outdir', or it is not a dir."); return undef } }
	my %ret;
	for my $packagename (sort keys %$searchres){
		my $appobj = $searchres->{$packagename};
		my $apks = $appobj->get('apkPaths');
		my $codePath = $appobj->get('codePath');
		next unless defined $apks; # actually it can not be undef, the worst it can be is empty, []
		my @arr;
		for my $apkfilename (@$apks){
			my $inbasename = File::Basename::basename($apkfilename);
			my $outfile = File::Spec->catfile($outdir, $inbasename);
			my $res = $self->adb->pull($apkfilename, $outfile);
			if( ! defined $res ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$apkfilename' into local file '$outfile', because undefined was returned, this should not be happening."); return undef }
			if( $res->[0] != 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$apkfilename' into local file '$outfile' with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }
			# it must be in outdir now...
			if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : extracted apk '$apkfilename' of package '$packagename' into '$outfile'.") }
			push @arr, {
				'device-path' => $apkfilename,
				'local-path' => $outfile
			};
		}
		$ret{$packagename} = \@arr;
		# each package name has an array of file specs as hash
	}
	return \%ret; # success
}

# It returns 0 on success, 1 on failure.
# It swipes the screen as per the 'direction'
# or full spec (x1,y1,x2,y2) (e.g. from x1 to x2 etc.)
# optional parameter 'dt' is the milliseconds to take for the swipe
# small is fast. Some speed is needed for certain gestures, so
# this is important parameter. E.g. for swiping to another screen.
# it needs that connect_device() to have been called prior to this call
sub swipe {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return 1 }

	my $w = $self->device_properties->get('w');
	my $h = $self->device_properties->get('h');

	my @fullspec;
	for ('x1', 'y1', 'x2', 'y2', 'dt'){
		if( ! exists($params->{$_}) || ! defined($params->{$_}) ){ last }
		push @fullspec, $params->{$_};
	}
	my @cmd;
	if( scalar(@fullspec) == 4 ){
		if( ($params->{'x1'} < 0) || ($params->{'x1'} > $w) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'x1' has a value (".$params->{'x1'}.") which is out of bounds."); return 1 }
		if( ($params->{'y1'} < 0) || ($params->{'y1'} > $h) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'y1' has a value (".$params->{'y1'}.") which is out of bounds."); return 1 }
		if( ($params->{'x2'} < 0) || ($params->{'x2'} > $w) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'x2' has a value (".$params->{'x2'}.") which is out of bounds."); return 1 }
		if( ($params->{'y2'} < 0) || ($params->{'y2'} > $h) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'y2' has a value (".$params->{'y2'}.") which is out of bounds."); return 1 }
		@cmd = ('input', 'touchscreen', 'swipe', @fullspec);
	} else {
		# the time to do the move in milliseconds, there is a default of 100
		# which is enough to swipe to next screen
		my $dt = (exists($params->{'dt'}) && defined($params->{'dt'})) ? $params->{'dt'} : 100;
		my $direction = (exists($params->{'direction'}) && defined($params->{'direction'})) ? $params->{'direction'} : undef;
		if( ! defined $direction ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'direction' was not specified."); return 1 }

		my ($x1, $y1, $x2, $y2);

		if( ($direction =~ /^l(eft)?$/i) || ($direction =~ /^r(ight)?$/i) ){
			# horizontal
			$y1 = $y2 = int(3*$h/4);
		} else {
			# vertical
			$x1 = $x2 = int($w/2);
		}
		if( ($direction =~ /^r(ight)?$/i) ){
			$x1 = int(0.2*$w);
			$x2 = $w - $x1;
		}
		if( ($direction =~ /^l(eft)?$/i) ){
			$x2 = int(0.2*$w);
			$x1 = $w - $x2;
		}
		if( ($direction =~ /^d(own)?$/i) ){
			$y1 = int(0.2*$h);
			$y2 = $h - $y1;
		}
		if( ($direction =~ /^u(p)?$/i) ){
			$y2 = int(0.2*$h);
			$y1 = $h - $y2;
		}
		@cmd = ('input', 'touchscreen', 'swipe', $x1, $y1, $x2, $y2, $dt);
	}

	# unfortunately Android::ADB uses IPC::Open2::open2() to run the adb shell command
	# which does not capture STDERR, and so either they use open3 or we capture stderr thusly:

	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	return 0; # success

lib/Android/ElectricSheep/Automator.pm  view on Meta::CPAN

# earlier results in $self->apps, and returns them as a hash
# keyed on appname. It also saves the returned hash into $self->apps
# optionally erasing previous entries (if 'make-fresh-apps-list'==1)
# The big question is whether to enquire the device for each package (app)
# installed and create an AppProperties object for each or not.
# The former is expensive and so you can do it on if-and-when-needed-basis
# (lazily). Or you can do it all at once here, it takes like 10-20 seconds.
# In any way, lazy or not, at the end the $self->apps HASH will be filled
# with the names of all packages installed on device (key).
# If 'lazy'==0, then for each installed app the device will be enquired
# and an AppProperties object will be created (it contains MainActivity,
# Permissions, etc.) and set as the value in the apps HASH.
# If 'packages' is specified and 'lazy'==0, then only those packages
# in 'packages' will be enquired, the $self->apps list will be
# refreshed with all the package names (key) but only some of them will
# contain an AppProperties object as the value, the other values
# will be undef.
# If no 'packages' was specified and 'lazy'==0, then all
# packages installed on device will be enquired and an AppProperties object
# will be associated with each of them in $self->apps.
# Default 'lazy' value is 1.
# Input:
#   'packages' => 'packagename' or regex (e.g. qr//) or [...] or { ...} :
#       optionally specify a list of package names to find ONLY,
#       default is to find ALL apps, which can be expensive if lazy>0
#       the 'packages' items can be a scalar string for exact match
#       or a compiled regex (qr//)
#   'force-reload-apps-list' => 0 or 1 : if 1, erase current list and start afresh
#   'lazy' => 1 or 0 : optionally specify not to enquire each package in detail,
#       i.e. creating an AppProperties object for each package,
#       but just add the package name as a key to the hash,
#       leaving the value undef. This value will be
#       created when needed, e.g. in an open_app() call.
#       Default is to be lazy, 1.
# It needs that connect_device() to have been called prior to this call
#
# On failure it returns undef.
# On success it saves the results in $self->apps HASH and returns that.
#
# NOTE: enquiring installed apps entails this:
#  1. adb shell dumpsys package packages
#     which is a general output of installed packages but does not contain
#     activity information.
#  2. For a given installed app name (package) we enquire by
#       adb shell dumpsys package com.example.myapp
#  WARNING: the 2nd step if called for all the installed apps can take some
#           time as it is done for each installed app.
#           Perhaps a better approach is to lazily find apps by
#           specifying the name of the app with the input parameter
#             'packages' => [...]
#           This will append to apps() the result.
sub find_installed_apps {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	# optionally caller can specify a list of app names to enquire
	# either as exact package name (string), a Regexp object (qr//)
	# an ARRAY of package names or a HASH of package names:
	my $packages = exists($params->{'packages'}) && defined($params->{'packages'}) ? $params->{'packages'} : undef;
	my $rr = ref $packages;
	if( ($rr ne '')&&($rr ne 'Regexp')&&($rr ne 'ARRAY')&&($rr ne 'HASH') ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, the type of input parameter 'packages' must be one of scalar string, Regexp, ARRAY or HASH and not '$rr'."); re...

	# NOTE: that all those 'packages', if any,
	# will be non-lazily even if 'lazy' is 1
	# omit 'packages' and you will have all packages according to the 'lazy' param
	# Also even if 'packages' is specified, the list of ALL apps will
	# be saved in the hash, except that those not in 'packages' will have
	# undef value and the others will have an AppProperties object value.
	my $epars = {
		'mother' => $self,
		('packages' => $params->{'packages'})x!!(exists($params->{'packages'}) && defined($params->{'packages'})),
		('lazy' => $params->{'lazy'})x!!(exists($params->{'lazy'}) && defined($params->{'lazy'})),
	};
	my $apps = Android::ElectricSheep::Automator::AppProperties::enquire_installed_apps_factory($epars);
	if( ! defined $apps ){ $epars->{'mother'} = '<redacted>'; $log->error(perl2dump($epars)."${whoami} (via $parent), line ".__LINE__." : error, failed to find all installed apps, call to ".'Android::ElectricSheep::Automator::AppProperties::enquire_inst...

	# erase the current list before adding?
	$self->{'apps'} = {} if exists($params->{'make-fresh-apps-list'})
			     && defined($params->{'make-fresh-apps-list'})
			     && ($params->{'make-fresh-apps-list'}>0)
	;

	# we will append to our apps list, which could have just been emptied
	# but if there is a key with AppProperties value
	# (and now it has undef) then we will keep that value instead of undef:
	for my $k (keys %$apps){
		next if exists($self->{'apps'}->{$k})
		     && defined($self->{'apps'}->{$k})
		;
		$self->{'apps'}->{$k} = $apps->{$k};
	}
	#my @k = keys %$apps; @{ $self->{'apps'} }{@k} = @{ $apps }{@k};

	return $self->apps; # success
}

# searches the current list of installed apps in $self->{'apps'}
# for the input app name or name regex.
# Note that a call to find_installed_apps() will populate
# the $self->{'apps'} with the keys (package names)
# but the expensive query for each and every key (package name)
# will be done lazily on a if-and-when-needed-basis
# but ALL the package names exist in $self->apps.

# Inputs parameters:
#   'package' => required package name as a SCALAR (for an exact search)
#     or a regex (qr//) object for regex search including case-insensitive.
#   'force-reload-apps-list' => 0,1 : optionally call find_installed_apps() if > 0
#     but restricted only to the packages match 'package' NOT ALL.
#   'lazy' => 0,1 : pass this lazy value to the find_installed_apps()
#     and be lazy (i.e. without enquiring on each app's specifics
#     and creating an AppProperties object) if ==1
#     or not be lazy if ==0 ...
#     ... (which means an AppProperties object is created for EACH package in the device)

lib/Android/ElectricSheep/Automator.pm  view on Meta::CPAN

# It searches the process table (using pidof) for the
# specified app name to check if it is running.
# Inputs parameters:
#   'appname' => an exact app name to find if it is running
# It returns 1 if the specified app is running, 0 if it is not
# It returns undef on failure.
sub is_app_running {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	my $appname;
	if( ! exists($params->{'appname'}) || ! defined($appname=$params->{'appname'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'appname' was not specified. It can be an exact app name or an extended regular expres...

	my $res = $self->pidof({'name' => $appname});
	if( ! defined $res ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'pidof()'." has failed."); return undef }

	# it returns undef on failure
	# it returns -1 if it is not found in the process table
	# it retuns >0 as the pid of the running app
	#if( $res == -1 ){ return 0 } # no it is not running
	if( $res =~ /^\d+$/ ){ return 1 } # yes it is running
	return 0; # no it is not running
}

# Inputs parameters:
#   'package' : required package name as a SCALAR (for an exact search)
#     or a regex (qr//) object for regex search including case-insensitive.
#    'activity' : optional activity name to start additionally to the app/package name.
#     If not present, we will try to find the MAIN activities of the package via
#     AppProperties. There could be several MAIN activities and there are heuristics
#     to pick one. See AppProperties::enquire().
#     The spec must yield exactly 1 match, it will complain if more than 1 matches found.
#   'force-reload-apps-list' => 0,1 : optionally call find_installed_apps() if > 0
#     but restricted only to the packages match 'package' NOT ALL.
#   'lazy' => 0,1 : pass this lazy value to the find_installed_apps()
#     and be lazy (i.e. without enquiring on each app's specifics
#     and creating an AppProperties object) if ==1
#     or not be lazy if ==0 ...
#     ... (which means an AppProperties object is created for each found package)
#     Default is force-reload-apps-list=>0
#     THIS APPLIES ONLY TO THE MATCHED 'package'
# On success it returns a hash of {appname => appproperties} of the opened app
#    (which will be created if not existing). It may return {} if no match
#    was found for the specified 'package' name.
# On failure it returns undef
# it needs that connect_device() to have been called prior to this call
sub open_app {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my ($package);
	if( ! exists($params->{'package'}) || ! defined($package=$params->{'package'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'package' is required."); return undef }
	if( (ref($package)ne'') && (ref($package)ne'Regexp') ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, the type of input parameter 'package' must be a scalar string (the package name) or a Regexp object (compiled regex via ".'qr//'...

	# optional activity, else we will see if we find one
	my $activity = (exists($params->{'activity'}) && defined($params->{'activity'})) ? $params->{'activity'} : undef;

	my $force_reload = (exists($params->{'force-reload-apps-list'}) && defined($params->{'force-reload-apps-list'})) ? $params->{'force-reload-apps-list'} : 0;
	my $lazy = (exists($params->{'lazy'}) && defined($params->{'lazy'})) ? $params->{'lazy'} : 1;

	my $apps = $self->apps();
	if( (0 == scalar(keys %$apps))
	 || ($force_reload>0)
	){
		my $fpars = {
			'packages' => $package,
			'force-reload-apps-list' => $force_reload,
			'lazy' => $lazy
		};
		if( ! defined($apps=$self->find_installed_apps($fpars)) ){ $log->error(perl2dump($fpars)."${whoami} (via $parent), line ".__LINE__." : error, failed to load list of installed apps, call to ".'find_installed_apps()'." has failed with above parameter...
		if( 0 == scalar(keys %$apps) ){
			$log->error("${whoami} (via $parent), line ".__LINE__." : error, there are no installed apps, even after enquiring them. The target device has no apps installed. Weird.");
			return undef
		}
	}

	# by now we are sure we have the list of installed apps updated
	# but it is likely that there is no AppProperties object for each
	# app in the list, but we need it, so make a search and if
	# AppProperties is undef, then we need to call find_installed_apps() again.
	my $searchres = $self->search_app({
		'package' => $package,
		'force-reload-apps-list' => 0,
	});
	if( ! defined $searchres ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'search_app()'." has failed for this search term (package) : ${package}"); return undef }
	my $num_searchres = scalar keys %$searchres;
	if( $num_searchres == 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, no app was found for this search term (package) : ${package}"); return undef }
	elsif( $num_searchres > 1 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, more than one app was found for this search term (package) : ${package} . Apps found: '".join("', '", sort keys %$searchres)."'."); return undef }
	# only 1 tupple in the hash, get it:
	my ($found_app_name, $found_app_properties) = %$searchres;

	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : app to open has been matched to '${found_app_name}'.") }

	if( ! defined $found_app_properties ){
		# the app is there in the list but it does not have AppProperties yet.
		# So get just this one package and non-lazily because we need the AppProperties object:
		my $fpars = {
			'force-reload-apps-list' => 0,
			'lazy' => 1, # << lazy for all other packages except our 'package'
			'packages' => $found_app_name,
		};
		if( ! defined $self->find_installed_apps($fpars) ){ $log->error(perl2dump($fpars)."${whoami} (via $parent), line ".__LINE__." : error, failed to load list of installed apps, call to ".'find_installed_apps()'." has failed with above parameters."); r...
		$apps = $self->apps();
		$found_app_properties = $apps->{$found_app_name};
	}

	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : opening app '".$found_app_properties->get('packageName')."' ...") }

	my ($MainActivity, $fact, @cmd);
	if( ! defined($MainActivity=$found_app_properties->get('MainActivity'))
	 || ! exists($MainActivity->{'name-fully-qualified'})
	 || ! defined($fact=$MainActivity->{'name-fully-qualified'})
	){
		$log->warn("${whoami} (via $parent), line ".__LINE__." : error, above app (package '${package}') does not contain a 'MainActivity' entry. Launching without it ...");
		$fact = $found_app_properties->get('packageName');
		@cmd = ('am', 'start', $fact);
	} else {
		@cmd = ('am', 'start', '-n', $fact);
	}

	# open it
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	# we are returning a hash of name=>appproperties
	# but because we allow 1 match only, this hash will only contain 1 item
	# but it will be easier to allow more apps in the future if
	# we return a hash here
	return { $found_app_name => $found_app_properties };
}

# Inputs parameters:
#   'package' => required package name as a SCALAR (for an exact search)
#     or a regex (qr//) object for regex search including case-insensitive.
#     The spec must yield exactly 1 match, it will complain if more than 1 matches found.
#   'force-reload-apps-list' => 0,1 : optionally call find_installed_apps() if > 0
#     but restricted only to the packages match 'package' NOT ALL.
#   'lazy' => 0,1 : pass this lazy value to the find_installed_apps()
#     and be lazy (i.e. without enquiring on each app's specifics
#     and creating an AppProperties object) if ==1
#     or not be lazy if ==0 ...
#     ... (which means an AppProperties object is created for each found package)
#     Default is force-reload-apps-list=>0
#     THIS APPLIES ONLY TO THE MATCHED 'package'
# On success it returns the a hash of {appname => appproperties} of the closed app
#    (which will be created if not existing). It may return {} if no match.
# On failure it returns undef
# it needs that connect_device() to have been called prior to this call
sub close_app {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my ($package);
	if( ! exists($params->{'package'}) || ! defined($package=$params->{'package'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'package' is required."); return undef }
	if( (ref($package)ne'') && (ref($package)ne'Regexp') ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, the type of input parameter 'package' must be a scalar string (the package name) or a Regexp object (compiled regex via ".'qr//'...

	# optional activity, else we will see if we find one
	my $activity = (exists($params->{'activity'}) && defined($params->{'activity'})) ? $params->{'activity'} : undef;

	my $force_reload = (exists($params->{'force-reload-apps-list'}) && defined($params->{'force-reload-apps-list'})) ? $params->{'force-reload-apps-list'} : 0;
	my $lazy = (exists($params->{'lazy'}) && defined($params->{'lazy'})) ? $params->{'lazy'} : 1;

	my $apps = $self->apps();
	if( (0 == scalar(keys %$apps))
	 || ($force_reload>0)
	){
		my $fpars = {
			'packages' => $package,
			'force-reload-apps-list' => $force_reload,
			'lazy' => $lazy
		};
		if( ! defined($apps=$self->find_installed_apps($fpars)) ){ $log->error(perl2dump($fpars)."${whoami} (via $parent), line ".__LINE__." : error, failed to load list of installed apps, call to ".'find_installed_apps()'." has failed with above parameter...
		if( 0 == scalar(keys %$apps) ){
			$log->error("${whoami} (via $parent), line ".__LINE__." : error, there are no installed apps, even after enquiring them. The target device has no apps installed. Weird.");
			return undef
		}
	}

	# by now we are sure we have the list of installed apps updated
	# but it is likely that there is no AppProperties object for each
	# app in the list, but we need it, so make a search and if
	# AppProperties is undef, then we need to call find_installed_apps() again.
	my $searchres = $self->search_app({
		'package' => $package,
		'force-reload-apps-list' => 0,
	});
	if( ! defined $searchres ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'search_app()'." has failed for this search term (package) : ${package}"); return undef }
	my $num_searchres = scalar keys %$searchres;
	if( $num_searchres == 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, no app was found for this search term (package) : ${package}"); return {} }
	elsif( $num_searchres > 1 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, more than one app was found for this search term (package) : ${package} . Apps found: '".join("', '", sort keys %$searchres)."'."); return undef }
	# only 1 tupple in the hash, get it:
	my ($found_app_name, $found_app_properties) = %$searchres;

	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : app to open has been matched to '${found_app_name}'.") }

	if( ! defined $found_app_properties ){
		# the app is there in the list but it does not have AppProperties yet.
		# So get just this one package and non-lazily because we need the AppProperties object:
		my $fpars = {
			'force-reload-apps-list' => 0,
			'lazy' => 1, # << lazy for all other packages except our 'package'
			'packages' => $found_app_name,
		};
		if( ! defined $self->find_installed_apps($fpars) ){ $log->error(perl2dump($fpars)."${whoami} (via $parent), line ".__LINE__." : error, failed to load list of installed apps, call to ".'find_installed_apps()'." has failed with above parameters."); r...
		$apps = $self->apps();
		$found_app_properties = $apps->{$found_app_name};
	}

	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : closing app '".$found_app_properties->get('packageName')."' ...") }

	# adb shell am force-stop com.my.app
	my @cmd = ('am', 'force-stop', $found_app_properties->get('packageName'));
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	# we are returning a hash of name=>appproperties
	# but because we allow 1 match only, this hash will only contain 1 item
	# but it will be easier to allow more apps in the future if
	# we return a hash here
	return { $found_app_name => $found_app_properties };
}

# returns pid (as a non-negative integer) of the specified app by its exact name
# or -1 if nothing was matched in the process table.
# on error it returns undef
# Note: if you do not know the exact app name e.g. com.viber.voip
# use pgrep()
sub pidof {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	if( ! exists($params->{'name'}) || ! defined($params->{'name'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, missing parameter 'name' is required, it must be the exact app name, if you do not have the exact name then use 'pgre...

	my @cmd = ('pidof', $params->{'name'});
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...

	# if not found it exits with 1
	if( $res->[0] == 1 ){ return -1 } # nothing matched

	# an error
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	my $pid = $res->[1];
	if( ! defined $pid ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, because the result got back was undef:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }
	# this is not happening, it exits with 1
	if( $pid =~ /^\s*$/ ){ return -1 } # nothing matched

	# wrong pid format found
	if( $pid !~ /^\s*(\d+)\s*$/m ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, because the result got back was not a pid (as numbers+spaces) but '$pid':\nSTDOUT:\n".$res->[1]."\n\nS...
	$pid = $1;
	return $pid; # success, this is the pid
}

# returns an array of pids of the specified app(s) by part of its name,
# There may be more than 1 items in the array if the specified part of its name
# matches many apps.
# on error it returns undef
sub pgrep {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	if( ! exists($params->{'name'}) || ! defined($params->{'name'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, missing parameter 'name' is required, it must be the exact app name, if you do not have the exact name then use 'pgre...

	my $dont_show_command_name = (exists($params->{'dont-show-command-name'}) && defined($params->{'dont-show-command-name'}) && ($params->{'dont-show-command-name'}>0)) ? 1 : 0;
	# -f will search the full command name
	# -l will include the command name which is the default
	my @cmdparams = ('-f');
	push(@cmdparams, '-l') unless $dont_show_command_name;

	my @cmd = ('pgrep', @cmdparams, $params->{'name'});
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	# we get something like "954\n995\n1217\n1372\n1392\n1642\n1741\n1856\n1898\n2549\n3236\n4456\n6570\n9245\n10115\n10746\n"

	my $xx = $res->[1];
	if( ! defined $xx ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, because the result got back was undef:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	my @results;
	while( $xx =~ /^\s*(\d+)(\s+(.+?))?\s*$/smg ){
		push @results, {
			'pid' => $1,
			'command' => $2 // ''
		};
	}

	return \@results; # success
	# a list of pids, can have 0, 1 or more items
}

# it takes the position on screen to tap either as a
# 'position' => [x,y]
# or
# 'bounds' => [ [topleftX,topleftY], [bottomrightX, bottomrightY] ]
# in which case, the tap position will be the mid-point of the 'bounds'
# rectangle.
# It returns 1 on failure, 0 on success
# it needs that connect_device() to have been called prior to this call
sub tap {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my (@position, $m);
	if( exists($params->{'position'}) && defined($m=$params->{'position'}) ){ 
		@position = ($m->[0], $m->[1]);
	} elsif( exists($params->{'bounds'}) && defined($m=$params->{'bounds'}) ){
		@position = ( int(($m->[1]->[0] + $m->[0]->[0])/2), int(($m->[1]->[1] + $m->[0]->[1])/2) );
	} else { $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'position' (as ['x','y']) or 'bounds' (as [lefttopX,lefttopY],[bottomrightX,bottomrighY]) was not specified."); return 1 }

	my @cmd = ('input', 'tap', @position);
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return 1 }
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }

	return 0; # success
}

# "type" the specified message into a text box expected to
# be found on the specified position.
# the message will be sanitised: spaces will be replaced with %s
# unicode in text is not supported.
# It returns 1 on failure, 0 on success
# it needs that connect_device() to have been called prior to this call
sub input_text {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my (@position, $m);
	if( exists($params->{'position'}) && defined($m=$params->{'position'}) ){ 
		@position = ($m->[0], $m->[1]);
	} elsif( exists($params->{'bounds'}) && defined($m=$params->{'bounds'}) ){
		@position = ( int(($m->[1]->[0] + $m->[0]->[0])/2), int(($m->[1]->[1] + $m->[0]->[1])/2) );
	} else { $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'position' (as ['x','y']) or 'bounds' (as [lefttopX,lefttopY],[bottomrightX,bottomrighY]) was not specified."); return 1 }
	# optional text, else we send just '' (but we clicked on it)
	my $text = (exists($params->{'text'}) && defined($params->{'text'})) ? $params->{'text'} : '';

	# sanitise the text a bit
	# replace spaces with %s,
	# also newlines seem not to be supported so replaces these as well
	$text =~ s/[\n \t]/%s/g;

	# first tap on the text edit widget at the specified coordinates to get focus
	if( $self->tap($params) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to tap on the position of the recipient of the text input"); return 1 }
	usleep(0.8);

	# and send the text
	# adb shell input text 'hello%sworld'
	# does not support unicode and also spaces must be converted to %s (already have done this)
	my @cmd = ('input', 'text', $text);
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return 1 }
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }

	return 0; # success
}

# returns 1 on failure, 0 on success
# it needs that connect_device() to have been called prior to this call
sub clear_input_field {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my (@position, $m);
	if( exists($params->{'position'}) && defined($m=$params->{'position'}) ){ 
		@position = ($m->[0], $m->[1]);
	} elsif( exists($params->{'bounds'}) && defined($m=$params->{'bounds'}) ){
		@position = ( int(($m->[1]->[0] + $m->[0]->[0])/2), int(($m->[1]->[1] + $m->[0]->[1])/2) );
	} else { $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'position' (as ['x','y']) or 'bounds' (as [lefttopX,lefttopY],[bottomrightX,bottomrighY]) was not specified."); return 1 }

	# first tap on the text edit widget at the specified coordinates to get focus
	if( $self->tap($params) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to tap on the position of the recipient of the text input"); return 1 }
	usleep(0.8);

	# from: https://stackoverflow.com/questions/32433303/clear-edit-text-adb
	# the simplest way is input keycombination 113 29 && input keyevent 67
	# but may not work
	# then we try the lame way by erasing all chars one after the other
	# part1:
	my @cmd = ('input', 'keycombination', '113', '29');
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined($res) || ($res->[0] != 0) || ($res->[1]=~/Error: Unknown command/) || ($res->[2]=~/Error: Unknown command/) ){
		$log->warn(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed which happens and now will try an alternative ...");
		# alternative/part1
		@cmd = ('input', 'keyevent', 'KEYCODE_MOVE_END');
		if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
		$res = $self->adb->shell(@cmd);
		if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening. Info: this is...
		if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed (Info: this is alternative part1), with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }
		# alternative/part2
		# optional number of chars in the text-edit box, meaning how many
		# times to press backspace, default is here (250)
		# this is only needed for the second method (the failsafe)
		my $numchars = (exists($params->{'num-characters'}) && defined($params->{'num-characters'}) && ($params->{'num-characters'}=~/^\d+$/) ) ? $params->{'num-characters'} : 250;
		@cmd = ('input', 'keyevent', '--longpress', ('KEYCODE_DEL')x$numchars);
		if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
		$res = $self->adb->shell(@cmd);
		if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening. Info: this is...
		if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed (Info: this is alternative part1), with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }
	} else {
		# part2
		# it worked, now on to part 2
		@cmd = ('input', 'keyevent', 'KEYCODE_DEL');
		if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
		$res = $self->adb->shell(@cmd);
		if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening. Info: this is...
		if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed (Info: this is part2), with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }
	}
	return 0; # success
}

# Finds the running processes on device (using a `ps`),
# optionally can save the (parsed) `ps`
# results as JSON to the specified 'filename'.
# It needs that connect_device() to have been called prior to this call
# It returns undef on failure.
# On success, it returns a hash with keys
#   'raw' : the raw output of `ps` as a string
#   'perl': the parsed output of `ps` as a Perl hash of hashes,
#           each process
#           is represented by a hashref of items, e.g. %CPU
#           (basically all the items from the header of the `ps` command)
#           keyed on the full process command and its arguments (verbatim from `ps` output).
#   'json': the above perl data structure converted to JSON.
# NOTE: it uses _ps_parse_output() which is copied verbatim from System::Process
#       I wish they would load ps info from a string rather than running their own `ps`
sub list_running_processes {
	my ($self, $params) = @_;

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];

	my $log = $self->log();
	my $verbosity = $self->verbosity;

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'device_connected()'." before calling this."); return undef }

	my $filename = exists($params->{'filename'}) && defined($params->{'filename'}) ? $params->{'filename'} : undef;
	my $extrafields = exists($params->{'extra-fields'}) && defined($params->{'extra-fields'}) ? $params->{'extra-fields'} : [];

	my ($FH, $tmpfilename) = tempfile(CLEANUP=>$self->cleanup);
	close $FH;

	# WARNING, you need to wake up the phone before dumping !!!!
	my $devicefile = File::Spec->catfile('/', 'data', 'local', 'tmp', $$.'.csv');

	my @cmd = ('ps', '-O', '%CPU', '-O', 'CPU');
	for my $ef (@$extrafields){ push @cmd, '-O', $ef }
	push @cmd, '-f', '-l', '>', $devicefile;

	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	$res = $self->adb->pull($devicefile, $tmpfilename);
	if( ! defined $res ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$devicefile' into local file '$tmpfilename', because undefined was returned, this should not be happening."); return undef }
	if( $res->[0] != 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$devicefile' into local file '$tmpfilename' with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	@cmd = ("rm", "-f", $devicefile);
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	$res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	# parse
	if( ! open($FH, '<', $tmpfilename) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to open file with dump for reading '$tmpfilename', $!"); return undef }
	my $contents;
	{ local $/ = undef; $contents = <$FH> } close $FH;
	$contents =~ s/\R+//;
	my @rows = split /\R+/, $contents;
	my @header = split /\s+/, shift @rows;
	my %headerh = map { $_ => 1 } @header;
	my $id = 'PID';
	my $posid = exists($headerh{$id}) ? $headerh{$id} : undef;
	if( ! defined $posid ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to find column name '$id' in above 'ps' output, where is it? what is it called?"); return undef }
	$id = 'CMD';
	my $poscmd = exists($headerh{$id}) ? $headerh{$id} : undef;

	my $n = scalar @header;

	my %psdata;
	while( my $row = shift @rows ){
		$row =~ s/^\s*//;
		my @rowitems = split /\s+/, $row, $n+1;
		my $k = $rowitems[defined($poscmd) ? $poscmd : $posid];
		@{ $psdata{$k} }{@header} = splice @rowitems, 0, $n;
		if( defined $poscmd ){
			$psdata{$k}->{'CMD'} = [ Text::ParseWords::shellwords($psdata{$k}->{'CMD'}) ];
			# now CMD is an arrayref
		}
	}

	my $jsonstr = perl2json(\%psdata);
	if( ! defined $jsonstr ){ $log->error(perl2dump(\%psdata)."${whoami} (via $parent), line ".__LINE__." : error, failed to convert above perl data hash to JSON string."); return undef }
	if( defined $filename ){
		if( ! open($FH, '>:raw', $filename) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to open file '$filename' for writing, $!"); return undef }
		print $FH $jsonstr;
		close $FH;
	}
	return {
		'raw' => $contents,
		'json' => $jsonstr,
		'perl' => \%psdata
	}
}

# ONLY FOR EMULATORS, it fixes the Geolocation to the
# specified coordinates (with 'latitude' and 'longitude').
# returns 1 on failure, 0 on success
# it needs that connect_device() to have been called prior to this call
sub geofix {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	for ('latitude', 'longitude'){
		if( ! exists $params->{$_} ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter '$_' was not specified (as [x,y])."); return 1 }
	}

	my @cmd = ('emu', 'geo', 'fix', $params->{'longitude'}, $params->{'latitude'});
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->run(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return 1 }
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }

	return 0; # success
}

# Get the current GPS location of the device
# according to ALL the GPS providers as a HASH
# keyed on GPS provider name with the information
# the provider provided including lat/lon
# It returns undef undef on failure or the above hash on success.
# NOTE: some providers may exist but have the Location[...] string as null
# meaning not available (e.g. 'network provider' when no internet exists)
# in this case lat,lon etc. will be '<na>' and the strings will be 'null'.
# it needs that connect_device() to have been called prior to this call
sub dump_current_location {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my @cmd = ('dumpsys', 'location');
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return 1 }
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }
	my $content = $res->[1];
	# these are the GPS providers in order of preference:
	my $gps;
	for my $prov ('gps provider', 'fused provider', 'passive provider', 'network provider'){
		if( $content =~ /Geofences\:\s+Location Providers\:.*?\n\s+\Q${prov}\E\:\s+last location=(.+?)\R\s+last coarse location=(.+?)\R/sm ){
			my $last_location = $1;
			my $last_coarse_location = $2;
			$gps //= {};
			my ($realprov, $lat, $lon, $las);
			if( $last_location =~ /\s*null\s*$/ ){
				$realprov = '<na>';
				$lat = '<na>';
				$lon = '<na>';
			} else {
				if( $last_location !~ /Location\[(.+?)\s+([-+]?\d+(?:\.\d+)?)\s*[:,]\s*([-+]?\d+(?:\.\d+)?)/ ){ $log->error("--BEGIN content:\n${content}\n--END content\n${whoami} (via $parent), line ".__LINE__." : error, failed to parse last location string: ${...
				$realprov = $1;
				$lat = $2;
				$lon = $3;
			}
			$gps->{$prov} = {
				'provider' => $realprov,
				'latitude' => $lat,
				'longitude' => $lon,
				'last-location-string' => $last_location
			};
			if( ($last_coarse_location !~ /\s*null\s*$/)
			 && ($last_coarse_location !~ /Location\[(.+?)\s+([-+]?\d+(?:\.\d+)?)\s*[:,]\s*([-+]?\d+(?:\.\d+)?)/)
			){ $log->error("--BEGIN content:\n${content}\n--END content\n${whoami} (via $parent), line ".__LINE__." : error, failed to parse last coarse location string: ${last_coarse_location}"); return undef }
			$gps->{$prov}->{'last-coarse-location-string'} = $last_coarse_location;
		}
	}
	if( ! defined $gps ){ $log->error("--BEGIN content:\n${content}\n--END content\n${whoami} (via $parent), line ".__LINE__." : error, location not found in above dumpsys, perhaps it is not enabled?"); return undef }
	return $gps;
}

# It lists the IDs of all the physical displays connected to the
# device, including the main one and returns these back as a HASH
# keyed on display ID.
# it needs that connect_device() to have been called prior to this call
sub list_physical_displays {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my @cmd = ('dumpsys', 'SurfaceFlinger', '--display-id');
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }
	my $content = $res->[1];
	my %ids;
	while( $content =~ /^(Display\s+(.+?)\s+.+?)$/gsm ){
		$ids{$2} = $1
	}
	return \%ids;
}

# It takes a screendump of current screen on device and returns it as
# a Image::PNG object, optionally saving it to the specified file.
# it needs that connect_device() to have been called prior to this call
# It returns undef on failure or the screenshot as an Image::PNG object
# on success.
# it needs that connect_device() to have been called prior to this call
sub dump_current_screen_shot {
	my ($self, $params) = @_;

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];

	my $log = $self->log();
	my $verbosity = $self->verbosity;

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'device_connected()'." before calling this."); return undef }

	my $filename = exists($params->{'filename'}) && defined($params->{'filename'}) ? $params->{'filename'} : undef;

	my $FH;
	if( ! defined $filename ){
		($FH, $filename) = tempfile(CLEANUP=>$self->cleanup);
		close $FH;
	}

	# optional display-id (TODO: confirm that this display id is valid with
	#   dumpsys SurfaceFlinger --display-id
	my @options;
	if( exists($params->{'display-id'}) && defined($params->{'display-id'}) ){
		push @options, '--display-id', $params->{'display-id'}
	}

	# WARNING, you need to wake up the phone before dumping !!!!
	my $devicefile = File::Spec->catfile('/', 'data', 'local', 'tmp', $$.'.png');

	my @cmd = ('screencap', @options, '-p', $devicefile);
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	$res = $self->adb->pull($devicefile, $filename);
	if( ! defined $res ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$devicefile' into local file '$filename', because undefined was returned, this should not be happening."); return undef }
	if( $res->[0] != 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$devicefile' into local file '$filename' with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	@cmd = ("rm", "-f", $devicefile);
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	$res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	# let's return the string content back, no we return back an Image::PNG
	#my $contents;
	#if( ! open($FH, '<', $filename) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to open file with dump for reading '$filename', $!"); return undef }
	#{ local $/ = undef; $contents = <$FH> } close $FH;

	# create an Image::PNG to return back
	my $img = Image::PNG->new();
	if( ! defined $img ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'Image::PNG->new()'." has failed."); return undef }
	if( ! $img->read($filename) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to read local file '$filename' as PNG (call to ".'Image::PNG->read()'." has failed)."); return undef }

	return $img;
}

# It takes a video recording of current screen on device and
# saves its to the specified file ($filename).
# Optionally specify 'time-limit' or a default of 10s is used.
# Optionally specify 'bit-rate'.
# Optionally specify %size = ('width' => ..., 'height' => ...)
# Optionally specify if $bugreport==1, then Android will overlay debug info on movie.
# Optionally specify 'display-id'.
# Output format of recording is MP4.
# It returns 1 on failure, 0 on success.
# it needs that connect_device() to have been called prior to this call
sub dump_current_screen_video {
	my ($self, $params) = @_;

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];

	my $log = $self->log();
	my $verbosity = $self->verbosity;

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'device_connected()'." before calling this."); return 1 }

	my $filename = exists($params->{'filename'}) && defined($params->{'filename'}) ? $params->{'filename'} : undef;
	if( ! defined $filename ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'filename' is not specified, an output filename must be specified."); return 1 }

	my @options;
	# optional duration or default of 10 seconds. (Android default is 180 which is stupidly huge for us)
	if( exists($params->{'time-limit'}) && defined($params->{'time-limit'}) ){
		push @options, '--time-limit', $params->{'time-limit'}
	} else { push @options, '--time-limit', '10' }

	# optional bitrate (don't know what Android default is)
	if( exists($params->{'bit-rate'}) && defined($params->{'bit-rate'}) ){
		push @options, '--bit-rate', $params->{'bit-rate'}
	}

	# optional bugreport (Android overlays timestamp etc.)
	if( exists($params->{'bugreport'}) && defined($params->{'bugreport'})
	 && ($params->{'bugreport'} > 0)
	){
		push @options, '--bugreport'
	}

	# optional size
	if( exists($params->{'size'}) && defined($params->{'size'}) ){
		if( (ref($params->{'size'}) ne 'HASH')
		 || (! exists $$params->{'size'}->{'width'})
		 || (! defined $$params->{'size'}->{'width'})
		 || (! exists $$params->{'size'}->{'height'})
		 || (! defined $$params->{'size'}->{'height'}) ){ $log->info("${whoami} (via $parent), line ".__LINE__." : error, specified parameter 'size' is either not a HASHref or it does not contain the two required keys 'width' and 'height'."); return 1 }
		push @options, '--size', $params->{'size'}->{'width'},  $params->{'size'}->{'height'}
	}

	# optional display-id (TODO: confirm that this display id is valid with
	#   dumpsys SurfaceFlinger --display-id
	if( exists($params->{'display-id'}) && defined($params->{'display-id'}) ){
		push @options, '--display-id', $params->{'display-id'}
	}

	# WARNING, you need to wake up the phone before dumping !!!!
	my $devicefile = File::Spec->catfile('/', 'data', 'local', 'tmp', $$.'.mp4');

	my @cmd = ('screenrecord', @options, $devicefile);
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);

	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return 1 }
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }

	$res = $self->adb->pull($devicefile, $filename);
	if( ! defined $res ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$devicefile' into local file '$filename', because undefined was returned, this should not be happening."); return 1 }
	if( $res->[0] != 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to pull remote file '$devicefile' into local file '$filename' with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }

	@cmd = ("rm", "-f", $devicefile);
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	$res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return 1 }
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return 1 }

	# let's return the string content back, no we return back 1 or 0, output must be saved to file
	#my $contents;
	#if( ! open($FH, '<', $filename) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to open file with dump for reading '$filename', $!"); return 1 }
	#{ local $/ = undef; $contents = <$FH> } close $FH;

	return 0; # success
}

# returns 1 on failure, 0 on success
# it needs that connect_device() to have been called prior to this call
sub wake_up {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my @cmd = qw/input keyevent KEYCODE_WAKEUP/;
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	return 0; # success
}
# goes to the home screen
# returns 1 on success, 0 on failure
# it needs that connect_device() to have been called prior to this call
sub home_screen {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	$self->wake_up();

	my @cmd = qw/am start -a android.intent.action.MAIN -c android.intent.category.HOME/;
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	return 0; # success
}
# It swipes right basically
# it returns 0 on success, 1 on failure
# it needs that connect_device() to have been called prior to this call
sub	next_screen {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	if( $self->swipe({'direction' => 'right', dt => 100}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'swipe()'." has failed."); return undef }

	return 0; # success
}

# It swipes left basically
# it returns 0 on success, 1 on failure
# it needs that connect_device() to have been called prior to this call
sub	previous_screen {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	if( $self->swipe({'direction' => 'left', dt => 100}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'swipe()'." has failed."); return undef }

	return 0; # success
}

# it returns 0 on success, 1 on failure
# it needs that connect_device() to have been called prior to this call
# the left-triangle button (see http://developer.android.com/reference/android/view/KeyEvent.html)
sub	navigation_menu_back_button {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my @cmd = ('input', 'keyevent', 'KEYCODE_BACK');
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	return 0; # success
}

# it returns 0 on success, 1 on failure
# it needs that connect_device() to have been called prior to this call
# the round button, it goes to home (see http://developer.android.com/reference/android/view/KeyEvent.html)
sub	navigation_menu_home_button {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my @cmd = ('input', 'keyevent', 'KEYCODE_HOME');
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	return 0; # success
}

# it returns 0 on success, 1 on failure
# it needs that connect_device() to have been called prior to this call
# the square button, aka overview, shows all apps running in some sort of gallery view
# (see http://developer.android.com/reference/android/view/KeyEvent.html)
sub	navigation_menu_overview_button {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my @cmd = ('input', 'keyevent', 'KEYCODE_APP_SWITCH');
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : sending command to adb: @cmd") }
	my $res = $self->adb->shell(@cmd);
	if( ! defined $res ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, got undefined result, most likely shell command did not run at all, this should not be happening."); return unde...
	if( $res->[0] != 0 ){ $log->error(join(" ", @cmd)."\n${whoami} (via $parent), line ".__LINE__." : error, above shell command has failed, with:\nSTDOUT:\n".$res->[1]."\n\nSTDERR:\n".$res->[2]."\nEND."); return undef }

	return 0; # success
}

# it swipes up and lists all the apps found in that "drawer"
# with their icon bounds (which is where you "tap").
# It returns undef on failure
# It returns a hashref of all the apps found with their name as key and bounds content etc.
# it needs that connect_device() to have been called prior to this call
sub find_all_apps_roundabout_way {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	# go to home, swipe up and all apps will be revealed
	# then dump the UI
	# then swipe down
	if( $self->home_screen() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'home_screen()'." has failed."); return undef }
	usleep(300);

	if( $self->swipe({'direction'=>'up'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'swipe()'." has failed."); return undef }
	usleep(300);

	my $xmlstr = $self->dump_current_screen_ui();
	if( ! defined $xmlstr ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'dump_current_screen_ui()'." has failed."); return undef }

	if( $self->swipe({'direction'=>'down'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'swipe()'." has failed."); return undef }

	my $apps = Android::ElectricSheep::Automator::XMLParsers::XMLParser_find_all_apps({
		'xml-string' => $xmlstr
	});
	if( ! defined $apps ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'Android::ElectricSheep::Automator::XMLParsers::XMLParser_find_all_apps()'." has failed."); return undef }

	$self->{'apps-roundabout-way'} = $apps;

	return $apps; # success
}

# It opens the specified (by 'name' or 'bounds' - for tapping) app
# and returns the a hashref with appname (only if name was specified) and bounds
# it needs that connect_device() to have been called prior to this call
sub open_app_roundabout_way {
	my ($self, $params) = @_;
	$params //= {};

	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( ! $self->is_device_connected() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, you need to connect a device to the desktop and ALSO explicitly call ".'connect_device()'." before calling this."); return undef }

	my $apps = $self->apps_roundabout_way();
	if( (! defined($apps))
	 || (exists($params->{'force-reload-apps-list'}) && defined($params->{'force-reload-apps-list'}) && ($params->{'force-reload-apps-list'}>0))
	){
		if( ! defined $self->find_all_apps_roundabout_way() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to load list of installed apps, call to ".'find_all_apps_roundabout_way()'." has failed."); return undef }
		$apps = $self->apps_roundabout_way();
	}

	my ($position, $appname, $bounds, $name);
	if( exists($params->{'name'}) && defined($name=$params->{'name'}) ){
		my $app;
		# this is an exact match! i don't think a regex match should be allowed,
		# although a case insensitive match would be a good idea
		# so make a new hash of apps, keyed on uppercase names
		if( ! exists($apps->{$name})
		 || ! defined($app=$apps->{$name})
		){
			# case insensitive match
			my $ucname = uc $name;
			my $ucapps = { map { uc $_ => $apps->{$_} } keys %$apps };
			if( ! exists($ucapps->{$ucname})
			 || ! defined($app=$ucapps->{$ucname})
			){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, app with name '".$params->{'name'}."' does not exist either as it is or with a case insensitive search. These are the apps found so far: '".join("', '", sort keys %$apps)."'."); r...
		}
		$bounds = $app->{'bounds'};
		$appname = $app->{'name'};
		$position = [ int(($bounds->[0] + $bounds->[2])/2), int(($bounds->[1] + $bounds->[3])/2) ];
	} elsif( exists($params->{'position'}) ){
		$position = $params->{'position'};
	} else {  $log->error("${whoami} (via $parent), line ".__LINE__." : error, one of 'name' or 'position' must be specified in the input parameters, but not both."); return undef }

	# go to home, swipe up and all apps will be revealed
	# click the app (what if it is not visible?)
	# then swipe down
	if( $self->home_screen() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'home_screen()'." has failed."); return undef }
	if( $self->swipe({'direction'=>'up'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'swipe()'." has failed."); return undef }
	if( $self->tap({'position'=>$position}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'position()'." has failed."); return undef }
	if( $self->swipe({'direction'=>'down'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'swipe()'." has failed."); return undef }

	return {
		'name' => $appname, # can be undef in only position was specified, TODO: we can find that too
		'bounds' => defined($bounds) ? $bounds : [@$position, 0, 0], # this can leave x2,y2 0
	}; # success
}

sub apps { return $_[0]->{'apps'} }
sub apps_roundabout_way { return $_[0]->{'apps-roundabout-way'} }
sub adb { return $_[0]->{'_private'}->{'Android::ADB'} }
sub log { return $_[0]->{'_private'}->{'log'}->{'logger-object'} }
# returns the current verbosity level optionally setting its value
# Value must be an integer >= 0
# setting a verbosity level will also spawn a chain of other debug subs,
sub verbosity {
	my ($self, $m) = @_;
	my $log = $self->log();
	if( defined $m ){
		my $parent = ( caller(1) )[3] || "N/A";
		my $whoami = ( caller(0) )[3];
		$self->{'_private'}->{'debug'}->{'verbosity'} = $m;

lib/Android/ElectricSheep/Automator.pm  view on Meta::CPAN

	}

        # Now we have a logger
        my $log = $self->log();
        $log->short(1);

	# Set verbosity and cleanup as follows:
	#  1. check if exists in params
	#  2. check if exists in confighash
	#  3. set default value
	my $v;
	if( exists($params->{'verbosity'}) && defined($params->{'verbosity'}) ){
		$v = $params->{'verbosity'};
	} elsif( exists($confighash->{'debug'}) && exists($confighash->{'debug'}->{'verbosity'}) && defined($confighash->{'debug'}->{'verbosity'}) ){
		$v = $confighash->{'debug'}->{'verbosity'};
	} else {
		$v = 0; # default
	}
	if( $self->verbosity($v) < 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to 'verbosity()' has failed for value '$v'."); return 1 }

	if( exists($params->{'cleanup'}) && defined($params->{'cleanup'}) ){
		$v = $params->{'cleanup'};
	} elsif( exists($confighash->{'debug'}) && exists($confighash->{'debug'}->{'cleanup'}) && defined($confighash->{'debug'}->{'cleanup'}) ){
		$v = $confighash->{'debug'}->{'cleanup'};
	} else {
		$v = 0; # default
	}
	if( $self->cleanup($v) < 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to 'cleanup()' has failed for value '$v'."); return 1 }

	return 0 # success
}

# initialises module-specific things, no need to copy this to other modules
# returns 1 on failure, 0 on success
sub init_module_specific {
	my ($self, $params) = @_;
	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();
	my $confighash = $self->confighash();
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : called ...") }

	# set or create the Android::ADB object
	if( exists($params->{'adb'}) && defined($params->{'adb'}) ){
		# caller supplied an existing ADB object
		$self->{'_private'}->{'Android::ADB'} = $params->{'adb'}
	} else {
		# no, we need to instantiate one, we need params in confighash
		my $pathtoadb = (exists($params->{'adb-path-to-executable'}) && defined($params->{'adb-path-to-executable'}))
			? $params->{'adb-path-to-executable'}
			: $self->confighash->{'adb'}->{'path-to-executable'}
		;
		if( ! -x $pathtoadb ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, specified adb executable '${pathtoadb}' is not an executable or does not exist."); return 1; }
		if( ! defined ($self->{'_private'}->{'Android::ADB'}=Android::ElectricSheep::Automator::ADB->new(
			path => $pathtoadb,
			verbosity => $self->verbosity
		)) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'Android::ElectricSheep::Automator::ADB->new()'." has failed (path to executable was specified as '$pathtoadb')."); return 1; }
	}

	# does caller have a device connected to the desktop and wants us to
	# target it?
	# if just one device, we don't need serial etc:
	my $device_params;
	if( exists($params->{'device-is-connected'}) && defined($params->{'device-is-connected'}) && ($params->{'device-is-connected'}>0) ){
		# just one device, we don't need serial of the device
		$device_params = {};
	} elsif( exists($params->{'device-serial'}) && defined($params->{'device-serial'}) ){
		$device_params = {'serial' => $params->{'device-serial'}};
	} elsif( exists($params->{'device-object'}) && defined($params->{'device-object'}) ){
		$device_params = {'device-object' => $params->{'device-object'}};
	}
	if( defined $device_params ){
		my $devobj = $self->connect_device($device_params);
		if( ! defined $devobj ){ $log->error(perl2dump($device_params)."${whoami} (via $parent), line ".__LINE__." : error, failed to connect to device with above parameters (call to ".'connect_device()'." has failed."); return undef }
		if( $verbosity > 0 ){ $log->info($devobj."\n${whoami} (via $parent), line ".__LINE__." : device set as above.") }
	}

	# done
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : ".__PACKAGE__." has been initialised ...") }
	return 0 # success
}

sub disconnect_device {
	my ($self, $params) = @_;
	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();

	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : called ...") }

	$self->{'device-properties'} = undef;
	$self->{'device-object'} = undef;

	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : done.") }
	return 0; # success
}
# prefer to return 1 0 than '', 1
sub is_device_connected { return defined($_[0]->{'device-properties'}) ? 1 : 0 }

# enquire screen properties (e.g. w, h) and save them to our $self
# if already exists a DeviceProperties object, then we just return that
# and do nothing UNLESS param 'force' => 1
# On failure it returns undef
# On success it creates a new DeviceProperties Object which is saved in $self
# and also returned to caller
sub find_current_device_properties {
	my ($self, $params) = @_;
	my $parent = ( caller(1) )[3] || "N/A";
	my $whoami = ( caller(0) )[3];
	my $log = $self->log();
	my $verbosity = $self->verbosity();
	my $confighash = $self->confighash();
	if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : called ...") }

	my $sl;
	if( defined($sl=$self->device_properties())
	 || (
		   exists($params->{'force'})
		&& defined($params->{'force'})
		&& ($params->{'force'}==0)
	    )
	){
		# there is no need to re-enquire, we have them already
		# and no 'force' was specified
		return $sl
	}

	$sl = Android::ElectricSheep::Automator::DeviceProperties->new({'mother'=>$self});
	if( ! defined $sl ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'Android::ElectricSheep::Automator::DeviceProperties->new()'." has failed."); return undef }
	if( $sl->enquire() ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'Android::ElectricSheep::Automator::DeviceProperties->enquire()'." has failed."); return undef }

	$self->{'device-properties'} = $sl;

	return $sl;
}

# only pod below
=pod

=encoding utf8

=head1 NAME

Android::ElectricSheep::Automator - Do Androids Dream of Electric Sheep? Smartphone control from your desktop.

=head1 VERSION

Version 0.09

=head1 WARNING

Current distribution is extremely alpha. API may change. 

=head1 SYNOPSIS

The present package fascilitates the control
of a USB-debugging-enabled
Android device, e.g. a real smartphone,
or an emulated (virtual) Android device,
from your desktop computer using Perl.
It's basically a thickishly-thin wrapper
to the omnipotent Android Debug Bridge (adb)
program.

B<Note that absolutely nothing is
installed on the connected device,
neither any of its settings will be modified by this package>.
See L</WILL ANYTHING BE INSTALLED ON THE DEVICE?>.

    use Android::ElectricSheep::Automator;

    my $mother = Android::ElectricSheep::Automator->new({
      # optional as there is a default, but you may have
      # problems with the location of the adb executable
      'configfile' => $configfile,
      'verbosity' => 1,
      # we already have a device connected and ready to control
      'device-is-connected' => 1,
    });

    # find the devices connected to desktop and set one.
    my @devices = $mother->adb->devices;
    $mother->connect_device({'serial' => $devices->[0]->serial})
	or die;
    # no device identification is required for the method call
    # if there is only one connected device:
    $mother->connect_device() if scalar(@devices)==0;

    # Go Home
    $mother->home_screen() or die;

    # swipe up/down/left/right
    $mother->swipe({'direction'=>up}) or die;
    # dt is the time to swipe in millis,
    # the shorter the faster the swipe
    $mother->swipe({'direction'=>left, 'dt'=>100}) or die;

    # tap
    $mother->tap({'position'=>[100,200]});

    # uses swipe() to move in screens (horizontally):
    $mother->next_screen() or die;
    $mother->previous_screen() or die;

    # bottom navigation:
    # the "triangle" back button
    $mother->navigation_menu_back_button() or die;
    # the "circle" home button
    $mother->navigation_menu_home_button() or die;
    # the "square" overview button
    $mother->navigation_menu_overview_button() or die;

    # open/close apps
    $mother->open_app({'package'=>qr/calendar$/i}) or die;
    $mother->close_app({'package'=>qr/calendar$/i}) or die;

    # push pull files
    $mother->adb->pull($deviceFile, $localFile);
    $mother->adb->push($localFile, $deviceFileOrDir);

    # guess what!
    my $xmlstr = $mother->dump_current_screen_ui();

    # Pull the apk(s) for an app from device and save locally
    my $res = $mother->pull_app_apk_from_device({
      package => 'com.google.android.calendar'
	# or qr/calendar/i
      'output-dir' => '/tmp/apks-of-calendar-app',
    });
    print $res->{'com.google.android.calendar'}->[0]->['local-path'};

    # Install apk(s) for an app onto the device
    $mother->install_app({
      'apk-filename' => ['/tmp/apks/base.apk', '/tmp/apks/config.apk'],
        # or just a string scalar '/tmp/apks/1.apk'
      # optional params to the adb install command
      'install-parameters' => ['-r', '-g']
    });

=head1 CONSTRUCTOR

=head2 B<C<new($params)>>

Creates a new C<Android::ElectricSheep::Automator> object. C<$params>
is a hash reference used to pass initialization options which may
or should include the following:

=over 4

=item * B<C<confighash>> or B<C<configfile>>

the configuration
file holds
configuration parameters. Its format is "enhanced" JSON
(see L<Config::JSON::Enhanced>) which is basically JSON
which allows comments between C< E<lt>/* > and C< */E<gt> >.

Here is an example configuration file to get you started:

  {
    "adb" : {
        "path-to-executable" : "/usr/local/android-sdk/platform-tools/adb"
    },
    "debug" : {
        "verbosity" : 0,
        </* cleanup temp files on exit */>
        "cleanup" : 1
    },
    "logger" : {
        </* log to file if you uncomment this, else console */>
        "filename" : "my.log"
    }
  }

All sections in the configuration are mandatory.
Setting C<"adb"> to the wrong path will yield problems.

C<confighash> is a hash of configuration options with
structure as above and can be supplied to the constructor
instead of the configuration file.

If no configuration is specified, then a default
configuration will be used. In this case please
specify B<C<adb-path-to-executable>> to point
to the location of C<adb>. Most likely
the default path will not work for you.

=item * B<C<adb-path-to-executable>>

optionally specify the path to the C<adb> executable in
your desktop system. This will override the setting
C< 'adb'-E<gt>'path-to-executable' > in the configuration,
if it was provided. Use this option if you are not
providing any configuration and so the default configuration
will be used. But it will most likely fail because of this
path not being correct for your system. So, if you are going
to omit providing a configuration and the default configuration
will be used do specify the C<adb> path via this option (but you
don't have to and your mileage may vary).

=item * B<C<device-serial>> or B<C<device-object>>

optionally specify the serial
of a device to connect to on instantiation,
or a L<Android::ElectricSheep::Automator::DeviceProperties>
object you already have handy. Alternatively,
use L</connect_device($params)> to set the connected device at a later
time. Note that there is no need to specify a
device if there is exactly one connected device.

=item * B<C<adb>>

optionally specify an already created L<Android::ADB> object.
Otherwise, a fresh object will be created based
on the configuration under the C<adb> section of the configuration.

=item * B<C<device-is-connected>>

optionally set it to 1
in order to communicate with the device
and get some information about it like
screen size, resolution, orientation, etc.
And also allow use of
functionality which needs communicating with a device
like L</swipe($params)>, L</home_screen($params)>,
L</open_app($params)>, etc.
After instantiation, you can use the
method L</connect_device($params)> and
L</disconnect_device()> for conveying
this information to the module.
Also note that if there are
more than one devices connected to the desktop, make sure
you specify which one with the C<device> parameter.
Default value is 0.

=item * B<C<logger>>

optionally specify a logger object
to be used (instead of creating a fresh one). This object
must implement these methods: C<info()>, C<warn()>, C<error()>.
L<Mojo::Log> fits perfectly.

=item * B<C<logfile>>

optionally specify a file to
save logging output to. This overrides the C<filename>
key under section C<logger> of the configuration.

=item * B<C<verbosity>>

optionally specify a verbosity level
which will override what the configuration contains. Default
is C<0>.

=item * B<C<cleanup>>

optionally specify a flag to clean up
any temp files after exit which will override what the
configuration contains. Default is C<1>, meaning Yes!.

=back

=head1 METHODS

Note:

=over 4

=item  * B<C<ARRAY_REF>> : C<my $ar = [1,2,3]; my $ar = \@ahash; my @anarray = @$ar;>

=item * B<C<HASH_REF>> : C<my $hr = {1=>1, 2=>2}; my $hr = \%ahash; my %ahash = %$hr;>

=item * In this module parameters to functions are passed as a HASH_REF.
Functions return back objects, ARRAY_REF or HASH_REF.

=back


=head2 B<C<devices()>>

Lists all Android devices connected to your
desktop and returns these as an ARRAY_REF which can be empty.

It returns C<undef> on failure.

=head2 B<C<connect_device($params)>>

Specifies the current Android device to control. Its use is
required only if you have more than one devices connected.
C<$params> is a HASH_REF which should contain exactly
one of the following:

=over 4

=item * B<C<serial>> should contain
the serial (string) of the connected device as returned
by L</devices()>.

=item * B<C<device-object>> should be
an already existing L<Android::ElectricSheep::Automator::DeviceProperties>
object.

=back

It returns C<0> on success, C<1> on failure.

=head2 B<C<dump_current_screen_ui($params)>>

It dumps the current screen as XML and returns that as
a string, optionally saving it to the specified file.

C<$params> is a HASH_REF which may or should contain:

=over 4

=item * B<C<filename>>

optionally save the returned XML string to the specified file.

=back

It returns C<undef> on failure or the UI XML dump, as a string, on success.

=head2 B<C<dump_current_screen_shot($params)>>

It dumps the current screen as a PNG image and returns that as
a L<Image::PNG> object, optionally saving it to the specified file.

C<$params> is a HASH_REF which may or should contain:

=over 4

=item * B<C<filename>>

optionally save the returned XML string to the specified file.

=back

It returns C<undef> on failure or a L<Image::PNG> image, on success.

=head2 B<C<dump_current_screen_video($params)>>

lib/Android/ElectricSheep/Automator.pm  view on Meta::CPAN


=item * B<C<apk-filename>>

The APK filename to install onto the device. It must
exist locally, obviously.

=item * B<C<install-parameters>>

Optional parameters to be passed on to the C<adb install>
command. Nothing is expected here. Refer to the
L<adb documentation|https://developer.android.com/tools/adb>
for what parameters are supported. For example, C<-r> is for
re-installation of an existing app and retaining its
previous data.

=back

It returns C<1> on failure.
It returns C<0> on success.

Note that there is a script available
which utilises this method, see L<electric-sheep-install-app.pl>.


=head2 B<C<is_app_running($params)>>

It checks if the specified app is running on the device.
The name of the app must be exact.
Note that you can search for running apps / commands
with extended regular expressions using L/pgrep()>

C<$params> is a HASH_REF which should contain:

=over 4

=item * B<C<appname>>

the name of the app to check if it is running.
It must be its exact name. Basically it checks the
output of L<pidof()>.

=back

It returns C<undef> on failure,
C<1> if the app is running or C<0> if the app is not running.

=head2 B<C<find_current_device_properties($params)>>

It enquires the device currently connected,
and specified with L</connect_device($params)>, if needed,
and returns back an L<Android::ElectricSheep::Automator::DeviceProperties>
object containing this information, for example screen size,
resolution, serial number, etc.

It returns L<Android::ElectricSheep::Automator::DeviceProperties>
object on success or C<undef> on failure.

=head2 B<C<connect_device()>>

It signals to our object that there is now
a device connected to the desktop and its
enquiry and subsequent control can commence.
If this is not called and neither C<device-is-connected =E<gt> 1>
is specified as a parameter to the constructor, then
the functionality will be limited and access
to functions like L</swipe($params)>, L</open_app($params)>, etc.
will be blocked until the caller signals that
a device is now connected to the desktop.

Using L</connect_device($params)> to specify which device
to target in the case of multiple devices
connected to the desktop will also call this
method.

This method will try to enquire the connected device
about some of its properties, like screen size,
resolution, orientation, serial number etc.
This information will subsequently be available
via C<$self-E<gt>>device_properties()>.

It returns C<0> on success, C<1> on failure.

=head2 B<C<disconnect_device()>>

Signals to our object that it should consider
that there is currently no device connected to
the desktop (irrespective of that is true or not)
which will block access to L</swipe($params)>, L</open_app($params)>, etc.

=head2 B<C<device_properties()>>

It returns the currently connected device properties
as a L<Android::ElectricSheep::Automator::DeviceProperties>
object or C<undef> if there is no connected device.
The returned object is constructed during a call
to L</find_current_device_properties($params)>
which is called via L</connect_device($params)> and will persist
for the duration of the connection.
However, after a call to L</disconnect_device()>
this object will be discarded and C<undef> will be
returned.

=head2 B<C<swipe($params)>>

Emulates a "swipe" in four directions.
Sets the current Android device to control. It is only
required if you have more than one device connected.
C<$params> is a HASH_REF which may or should contain:

=over 4

=item * B<C<direction>>

should be one of

=over 4

=item up

=item down

=item left

=item right

=back

=item * B<C<dt>>

denotes the time taken for the swipe
in milliseconds. The smaller its value the faster
the swipe. A value of C<100> is fast enough to swipe to
the next screen.

=back

It returns C<0> on success, C<1> on failure.

=head2 B<C<tap($params)>>

Emulates a "tap" at the specified location.
C<$params> is a HASH_REF which must contain one
of the following items:

=over 4

=item * B<C<position>>

lib/Android/ElectricSheep/Automator.pm  view on Meta::CPAN


Dump the GPS / geo-location position for the device from its various providers, if enabled.

    electric-sheep-dump-current-location.pl --configfile config/myapp.conf --output geolocation.json

=head2 B<C<electric-sheep-emulator-geofix.pl>>

Set the GPS / geo-location position to the specified coordinates.

    electric-sheep-dump-ui.pl --configfile config/myapp.conf --latitude 12.3 --longitude 45.6

=head2 B<C<electric-sheep-dump-screen-shot.pl>>

Take a screenshot of the device (current screen) and save to a PNG file.

    electric-sheep-dump-screen-shot.pl --configfile config/myapp.conf --output screenshot.png

=head2 B<C<electric-sheep-dump-screen-video.pl>>

Record a video of the device's current screen and save to an MP4 file.

    electric-sheep-dump-screen-video.pl --configfile config/myapp.conf --output video.mp4 --time-limit 30

=head2 B<C<electric-sheep-pull-app-apk.pl>>

Extract the APK file (java bytecode) for an app installed on the device and save locally, perhaps, for disassembly and/or modification and/or re-installation.

    electric-sheep-pull-app-apk.pl --package calendar2 --wildcard --output anoutdir --configfile config/myapp.conf --device Pixel_2_API_30_x86_

=head2 B<C<electric-sheep-install-app>>

Install an APK file onto the device, passing extra installation
parameters C<-r> (for re-install) and C<-g> (for granting permissions),

    electric-sheep-install-app --apk-filename test.apk -p '-r' -p '-g' --configfile config/myapp.conf --device Pixel_2_API_30_x86_


=head2 B<C<electric-sheep-viber-send-message.pl>>

Send a message using the Viber app.

    electric-sheep-viber-send-message.pl --message 'hello%sthere' --recipient 'george' --configfile config/myapp.conf --device Pixel_2_API_30_x86_

This one saves a lot of debugging information to C<debug> which can be used to
deal with special cases or different versions of Viber:

    electric-sheep-viber-send-message.pl --outbase debug --verbosity 1 --message 'hello%sthere' --recipient 'george' --configfile config/myapp.conf --device Pixel_2_API_30_x86_


=head1 TESTING

The normal tests under the C<t/> directory, initiated with C<make test> command,
are quite limited in scope because they do not assume
a connected device. That is, they do not check any
functions which require interaction with a connected
device.

The I<live tests> under the C<xt/live> directory, initiated with
C<make livetest> command, require
an Android emulator or real device (the latter B<is not recommended>)
connected to your desktop computer on which you are doing the testing.
Note that testing
with your smartphone is not a good idea, please do not do this,
unless it is some phone which you do not store important data.
It is very easy to get an emulated Android device running on any OS.

So, prior to C<make livetest> make sure you have an android
emulator up and running with, for example,
C<emulator -avd Pixel_2_API_30_x86_> . See section
L<Android Emulators> for how to install, list and run them
buggers.

At least one of the I<author tests> under the C<xt/author> directory,
initiated with
C<make authortest> command, require an APK file (to be installed on the connected
device) which is quite large and it is not included in the distribution
bundle of this module. Anyway, it is not a good idea to install
an unknown APK to your device. But if you want to make this test
then pull an APK of an existing app on your connected device
with L<electric-sheep-pull-app-apk.pl> and point the test file
to this APK.

Testing will not send any messages via the device's apps.
E.g. the plugin L<Android::ElectricSheep::Automator::Plugins::Apps::Viber>
will not send a message via Viber but it will mock it.

The live tests will sometimes fail because, so far,
something unexpected happened in the device. For example,
in testing sending input text to a text-edit widget,
the calendar will be opened and a new entry will be added
and its text-edit widget will be targeted. Well, sometimes
the calendar app will give you some notification
on startup and this messes up with the focus.
Other times, the OS will detect that some app is taking too
long to launch and pops up a notification about
"I<something is not responding, shall I close it>".
This steals the focus and sometimes it causes
the tests to fail.

=head1 PREREQUISITES

=head2 Android Studio

This is not a prerequisite but it is
highly recommended to install it
(from L<https://developer.android.com/studio>)
on your desktop computer because it contains
all the executables you will need,
saved in a well documented file system hierarchy,
which can then be accessed from the command line.
You will not be using the IDE or anything, just
the accompaniying binaries and libraries it comes with.

Additionally, Android Studio offers possibly the
easiest way to create Android Virtual Devices (AVD) which emulate
an Android phone of various specifications, phone models and sizes,
API levels, etc.
I mention this because one can install apps
on an AVD and control them from your desktop
as long as you are able to receive sms verification
codes from a real phone. Perhaps you will need an Android
emulator image which comes with Google Play Services,
if you are installing apps from their store.
This is great for
experimenting without plugging in your real
smartphone on your desktop.

The bottom line is that by installing Android Studio,
you have all the executables you need for running things
from the command line and, additionally, you have
the easiest way for creating Android
Virtual Devices, which emulate Android devices: phones,
tablets, automotive displays. Once you have this set up, you
will not need to open Android Studio ever again unless you
want to update your kit. All the functionality
will be accessible from the command line.

=head2 ADB

Android Debug Bridge (ADB) is the program
which communicates with your smartphone or
an Android Virtual Device from
your desktop (Linux, osx and the unnamed C<0$>).

If you do not want to install Android Studio, the C<adb> executable
is included in the package called
"Android SDK Platform Tools" available from
the Android official site, here:
L<https://developer.android.com/tools/releases/platform-tools#downloads>

You will need the C<adb> executable to be on your path
or specify its fullpath in the configuration file
supplied to L<Android::ElectricSheep::Automator>'s constructor.

=head2 USB Debugging

The targeted smartphone must have "USB Debugging" enabled
via the "Developer mode".
This is not
to be confused with 'rooted' or 'jailbroken' modes, none of
these are required for experimenting with the current module.

In order to enable "USB Debugging", you need
to set the smartphone to enter "Developer" mode by
following this procedure:

Go to C<Settings-E<gt>System-E<gt>About Phone>
Tap on C<Build Number> 7 times [sic!].
Enter your phone pin and you are in developer mode.

You can exit Developer Mode by going to
C<Settings-E<gt>System-E<gt>Developer> and turn it off.
It is highly advised to turn off Developer Mode
for everyday use of your phone.
B<Do not connect your smartphone
to public WIFI networks with Developer Mode ON>.

B<Do not leave home with Developer Mode ON>.

Once you have enabled "USB Debugging", you have
two options for making your device visible to
your desktop and, consequently, to ADB and to this module:

=over 4

=item * connect your android device via a USB cable
to your desktop computer. I am not sure if you also
need to tap on the USB charging options and allow
"Transfer Files".

=item * connect your device to the same WIFI network
as your desktop computer. Then follow instructions
from, e.g., here L<https://developer.android.com>.
This requires a newer Android version.

=back

=head2 Android Emulators

It is possible to do most things your
smartphone does with an Android Virtual Device.
You can install apps on the the virtual device which
you can register by supplying your real smartphone
number.

List all virtual devices currently available
in your desktop computer,  with C<emulator -list-avds>
which outputs something like:

    Pixel_2_API_27_x86_
    Pixel_2_API_30_x86_

Start a virtual device with C<emulator -avd Pixel_2_API_30_x86_>

And hey, you have an android phone running on your
desktop in its own space, able to access the network
but not the telephone network (no SIM card).

It is possible to create a virtual device
from the command line.
But perhaps it is easier if you download Android Studio
from: L<https://developer.android.com/studio> and follow
the setup there using the GUI. You will need to do this just
once for creating the device, you can then uninstall Android Studio.

Android Studio will download all the
required files and will create some Android Virtual
Devices (the "emulators") for you. It will also be easy to
update your stack in the future. Once you have done the above,
you no longer need to run Android Studio except perhaps for
checking for updates and B<all the required executables by this
package will be available from the command line>.

Otherwise, download "Android SDK Platform Tools" available from
the Android official site, here:
L<https://developer.android.com/tools/releases/platform-tools#downloads>
(this download is mentioned in L<ADB> if you already fetched it).

Fetch the required packages with this command:

C<sdkmanager --sdk_root=/usr/local/android-sdk  "platform-tools" "platforms;android-30" "cmdline-tools;latest" "emulator">

Note that C<sdkmanager --list> will list the latest android versions etc.

Now you should have access to C<avdmanager> executable
(it should be located here: C</usr/local/android-sdk/cmdline-tools/latest/bin/avdmanager>)
which you can use to create an emulator.

List all available android virtual devices availabe to you B<to create>: C<avdmanager list target>

List all available devices you can emulate: C<avdmanager list device>

List all available devices which have been created already and are available to boot right now: C<avdmanager list avd>

Create virtual device: C<avdmanager create avd -d "Nexus 6" -n myavd -k "system-images;android-29;google_apis;x86">

(source: L<https://stackoverflow.com/a/77599934>)

In Linux, the Android emulator image files are stored
at C<~/.config/.android/avd> and/or at C<~/.android/avd>
Each image consists of an C<.avd> file and a C<.ini> file.
As said before, you can boot a device with C<emulator -avd 'Pixel_9'>
(the images will be C<Pixel_9.avd> and C<Pixel_9.ini>)

=head1 NO ACCESS TO GOOGLE PLAY?

See here if your Android Emulator has no access to Google's App Store:

L<https://stackoverflow.com/questions/71815181/how-can-i-get-google-play-to-work-on-android-emulator-in-android-studio-bumblebe>

Your mileage will lean on the low side.

=head1 USING YOUR REAL SMARTPHONE

Using your real smartphone
with such a powerful tool may not be such



( run in 1.200 second using v1.01-cache-2.11-cpan-df04353d9ac )