Android-ElectricSheep-Automator
view release on metacpan or search on metacpan
lib/Android/ElectricSheep/Automator.pm view on Meta::CPAN
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);
lib/Android/ElectricSheep/Automator.pm view on Meta::CPAN
return 0; # success
}
# It finds all installed apps on device and appends with
# 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)
# Default is force-reload-apps-list=>0
# It returns the found packages (full package name (which is a key in $self->{'apps'}))
# as a HASHref if any found, or {} if none found.
# the key to the returned hash is the full package name and the value
# will be AppProperties object or undef if not instantiated for that app
# because of lazy=1
# It returns undef on failure.
sub search_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();
my $package;
if( ! exists($params->{'package'}) || ! defined($package=$params->{'package'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'package' was not specified, it must be a package name or a compiled regex (e.g. via "...
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//'...
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 we have no apps list or we have a force reload, then
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
}
}
# We are returning a hash whose keys are package-names
# and values are AppProperties objects
# if not a regex for package-name then this hash will contain
# 1 item only, otherwise 0, 1 or more.
# we have a package name, so return it, if found
if( ref($package) eq '' ){
# a scalar string : exact match of a package name
return exists($apps->{$package})
? { $package => $apps->{$package} }
: {}
;
}
# otherwise, we have a regex contained in package name,
# return those matched
return { map { $_ => $apps->{$_} }
grep { $_ =~ $package }
sort keys %$apps
};
}
# 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;
if( defined $self->adb ){ $self->adb->{'verbosity'} = $m }
}
return $self->{'_private'}->{'debug'}->{'verbosity'}
}
sub cleanup {
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'}->{'cleanup'} = $m;
}
return $self->{'_private'}->{'debug'}->{'cleanup'}
}
# return configfile or read+check+set a configfile,
# returns undef on failure or the configfile on success
sub configfile {
my ($self, $infile) = @_;
return $self->{'_private'}->{'configfile'} unless defined $infile;
my $parent = ( caller(1) )[3] || "N/A";
my $whoami = ( caller(0) )[3];
# this can be called before the logger is created, so create a temp logger for this
my $log = $self->log() // Mojo::Log->new();
my $ch = parse_configfile($infile, $log);
if( ! defined $ch ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'parse_configfile()'." has failed for configuration file '$infile'."); return undef }
# set it in self, it will also do checks on required keys
if( ! defined $self->confighash($ch) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to load specified confighash, , there must be errors in the configuration."); return undef }
$self->{'_private'}->{'configfile'} = $infile;
return $infile #success
}
# return configfile or read+check+set a configfile,
# returns undef on failure or the configfile on success
sub parse_configfile {
my ($infile, $log) = @_;
my $parent = ( caller(1) )[3] || "N/A";
my $whoami = ( caller(0) )[3];
# this can be called before the logger is created, so create a temp logger for this
$log //= Mojo::Log->new();
my $ch = Config::JSON::Enhanced::config2perl({
'filename' => $infile,
'commentstyle' => 'custom(</*)(*/>)',
'tags' => ['<%','%>'],
'variable-substitutions' => {
'SCRIPTDIR' => Cwd::abs_path($FindBin::Bin),
},
});
if( ! defined $ch ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'Config::JSON::Enhanced::config2perl()'." has failed for configuration file '$infile'."); return undef }
return $ch; #success
}
# returns the confighash stored or if one is supplied
# it checks it and sets it and returns it
# or it returns undef on failure
# NOTE, if param is specified then we assume we do not have any configuration,
# we do not have a logger yet, we have no configuration, no verbosity, etc.
sub confighash {
my ($self, $m) = @_;
if( ! defined $m ){ return $self->{'_private'}->{'confighash'} }
my $parent = ( caller(1) )[3] || "N/A";
my $whoami = ( caller(0) )[3];
#print STDOUT "${whoami} (via $parent), line ".__LINE__." : called ...\n";
# we are storing specified confighash but first check it for some fields
# required fields:
for ('adb', 'debug', 'logger'){
if( ! exists($m->{$_}) || ! defined($m->{$_}) ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : error, configuration does not have key '$_'.\n"; return undef }
}
my $x;
# adb params
$x = $m->{'adb'};
for ('path-to-executable'){
if( ! exists($x->{$_}) || ! defined($x->{$_}) ){ print STDERR "${whoami} (via $parent), line ".__LINE__." : error, configuration does not have key '$_'.\n"; return undef }
}
# debug params
$x = $m->{'debug'};
if( exists($x->{'verbosity'}) && defined($x->{'verbosity'}) ){
$self->verbosity($x->{'verbosity'});
}
if( exists($x->{'cleanup'}) && defined($x->{'cleanup'}) ){
$self->cleanup($x->{'cleanup'});
}
# create logger if specified but only if one does not exist
$x = $m->{'logger'};
if( exists($x->{'filename'}) && defined($x->{'filename'})
&& (! defined($self->log()))
){
$self->log(Mojo::Log->new(path => $x->{'filename'}));
}
# ok!
$self->{'_private'}->{'confighash'} = $m;
return $m
}
# initialises
# do the verbositys
# returns 1 on failure, 0 on success
sub init {
my ($self, $params) = @_;
my $parent = ( caller(1) )[3] || "N/A";
my $whoami = ( caller(0) )[3];
# Confighash
# first see if either user specified a config file or the default is present and read it,
# then we will overwrite with user-specified params if any
my ($configfile, $confighash);
if( exists($params->{'configfile'}) && defined($configfile=$params->{'configfile'}) ){
if( ! -f $configfile ){ print STDERR __PACKAGE__."${whoami} (via $parent), line ".__LINE__." : error, specified configfile '$configfile' does not exist or it is not a file.\n"; return 1 }
# this reads, creates confighash and calls confighash() which will do all the tests
if( ! defined $self->configfile($configfile) ){ print STDERR __PACKAGE__."${whoami} (via $parent), line ".__LINE__." : error, call to ".'configfile()'." has failed for configfile '$configfile'.\n"; return 1 }
$confighash = $self->confighash();
} elsif( exists($params->{'confighash'}) && defined($params->{'confighash'}) ){
$confighash = $params->{'confighash'};
# this sets the confighash and checks it too
if( ! defined $self->confighash($confighash) ){ print STDERR __PACKAGE__."${whoami} (via $parent), line ".__LINE__." : error, call to ".'confighash()'." has failed.\n"; return 1 }
} else {
# use default config
$confighash = Config::JSON::Enhanced::config2perl({
'string' => $_DEFAULT_CONFIG,
'commentstyle' => 'custom(</*)(*/>)',
'tags' => ['<%','%>'],
'variable-substitutions' => {
'SCRIPTDIR' => Cwd::abs_path($FindBin::Bin),
},
});
if( ! defined $confighash ){ print STDERR $_DEFAULT_CONFIG."\n\n".__PACKAGE__."${whoami} (via $parent), line ".__LINE__." : error, failed to parse default configuration string, above.\n"; return undef }
if( ! defined $self->confighash($confighash) ){ print STDERR __PACKAGE__."${whoami} (via $parent), line ".__LINE__." : error, call to ".'confighash()'." has failed.\n"; return 1 }
}
# by now we have a confighash in self or died
# for creating the logger: check
# 1. params if they have logger or logfile
# 2. our own confighash if it contains logfile
# 3. if all else fails, create a vanilla logger
if( exists($params->{'logger'}) && defined($params->{'logger'}) ){
$self->{'_private'}->{'log'}->{'logger-object'} = $params->{'logger'};
#print STDOUT "${whoami} (via $parent), line ".__LINE__." : using user-supplied logger object.\n";
} elsif( exists($params->{'logfile'}) && defined($params->{'logfile'}) ){
$self->{'_private'}->{'log'}->{'logger-object'} = Mojo::Log->new(path => $params->{'logfile'});
#print STDOUT "${whoami} (via $parent), line ".__LINE__." : logging to file '".$params->{'logfile'}."'.\n";
} elsif( ! defined($self->{'_private'}->{'log'}->{'logger-object'}) ){
$self->{'_private'}->{'log'}->{'logger-object'} = Mojo::Log->new();
#print STDOUT "${whoami} (via $parent), line ".__LINE__." : a vanilla logger has been created to log to the console.\n";
}
# 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?>.
( run in 0.983 second using v1.01-cache-2.11-cpan-39bf76dae61 )