Android-ElectricSheep-Automator

 view release on metacpan or  search on metacpan

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

package Android::ElectricSheep::Automator;

# see also https://www.reddit.com/r/privacytoolsIO/comments/fit0tr/taking_almost_full_control_of_your_unrooted/
# swipe adb shell input touchscreen swipe 300 1200 100 1200 100

use 5.006;
use strict;
use warnings;

our $VERSION = '0.09';

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

	my $verbosity = $self->verbosity;

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

	return $self;
}

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

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

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

	my ($what_device, $m);

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

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

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

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

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

	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 }

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

	$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 {

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

# prefer to return 1 0 than '', 1
sub is_device_connected { return defined($_[0]->{'device-properties'}) ? 1 : 0 }

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

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

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

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

	return $sl;
}

# only pod below
=pod

=encoding utf8

=head1 NAME

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

=head1 VERSION

Version 0.09

=head1 WARNING

Current distribution is extremely alpha. API may change. 

=head1 SYNOPSIS

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

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

    use Android::ElectricSheep::Automator;

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

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

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

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

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

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

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

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

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

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

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

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

=head1 CONSTRUCTOR

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

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

=over 4

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

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

Here is an example configuration file to get you started:

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

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

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

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

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

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

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

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

=item * B<C<adb>>

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

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

optionally set it to 1
in order to communicate with the device
and get some information about it like
screen size, resolution, orientation, etc.
And also allow use of
functionality which needs communicating with a device

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


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

=over 4

=item * B<C<filename>>

optionally save the returned XML string to the specified file.

=back

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

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

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

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

=over 4

=item * B<C<filename>>

optionally save the returned XML string to the specified file.

=back

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

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

It dumps the current screen as MP4 video and saves that
in specified file.

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

=over 4

=item * B<C<filename>>

save the recorded video to the specified file in MP4 format. This
is required.

=item * B<C<time-limit>>

optionally specify the duration of the recorded video, in seconds. Default is 10 seconds.

=item * B<C<bit-rate>>

optionally specify the bit rate of the recorded video in bits per second. Default is 20Mbps.

=item * B<C<size>>

optionally specify the size (geometry) of the recorded video as a
HASH_REF with keys C<width> and C<height>, in pixels. Default is "I<the
device's main display resolution>".

=item * B<C<bugreport>>

optionally set this flag to 1 to have Android overlay debug information
on the recorded video, e.g. timestamp.

=item * B<C<display-id>>

for a device set up with multiple physical displays, optionally
specify which one to record -- if not the main display -- by providing the
display id. You can find display ids with L</list_physical_displays()>
or, from the CLI, by C<adb shell dumpsys SurfaceFlinger --display-id>

=back

C<adb shell screenrecord --help> contains some more documentation.

=head2 B<C<list_physical_displays()>>

It lists the IDs of all the physical displays connected to the
device, including the main one and returns these back as a HASH_REF
keyed on display ID. It needs that connect_device() to have been called prior to this call

It returns C<undef> on failure or the results as a HASH_REF keyed on display ID.

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

It finds the running processes on device (using a `ps`),
optionally can save the (parsed) `ps`
results as JSON to the specified 'filename'.
It returns C<undef> on failure or the results as a hash of hashes on success.

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

=over 4

=item * B<C<extra-fields>>

optionally add more fields (columns) to the report by C<ps>, as an ARRAY_REF.
For example, C<['TTY','TIME']>.

=back

It needs that connect_device() to have been called prior to this call

It returns C<undef> on failure or a hash with these keys on success:

=over 4

=item * B<C<raw>> : contains the raw `ps` output as a string.

=item * B<C<perl>> : contains the parsed raw output as a Perl hash with
each item corresponding to one process, keyed on process command and arguments
(as reported by C<ps>, verbatim), as a hash keyed on each field (column)
of the C<ps> output.

=item * B<C<json>> : the above data converted into a JSON string.

=back

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

It returns the PID of the specified command name.
The specified command name must match the app or command

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

Close an app by its exact name or a keyword matching it (uniquely):

    electric-sheep-close-app.pl --configfile config/myapp.conf --name com.android.settings

    electric-sheep-close-app.pl --configfile config/myapp.conf --keyword 'clock'

Note that it constructs a regular expression from escaped user input.

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

Dump the current screen UI as XML to STDOUT or to a file:

    electric-sheep-dump-ui.pl --configfile config/myapp.conf --output ui.xml

Note that it constructs a regular expression from escaped user input.

=head2 B<C<electric-sheep-dump-current-location.pl>>

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

Send a message using the Viber app.

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

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

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


=head1 TESTING

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

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

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

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

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

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

=head1 PREREQUISITES

=head2 Android Studio

This is not a prerequisite but it is
highly recommended to install it
(from L<https://developer.android.com/studio>)
on your desktop computer because it contains



( run in 0.729 second using v1.01-cache-2.11-cpan-5511b514fd6 )