Android-ElectricSheep-Automator
view release on metacpan or search on metacpan
lib/Android/ElectricSheep/Automator/ScreenLayout.pm view on Meta::CPAN
package Android::ElectricSheep::Automator::ScreenLayout;
use 5.006;
use strict;
use warnings;
our $VERSION = '0.09';
use Mojo::Log;
use Config::JSON::Enhanced;
use XML::XPath;
use XML::LibXML;
use Data::Roundtrip qw/perl2dump no-unicode-escape-permanently/;
use overload ( '""' => \&toString );
sub new {
my $class = $_[0];
my $params = $_[1] // {};
my $parent = ( caller(1) )[3] || "N/A";
my $whoami = ( caller(0) )[3];
my $self = {
'_private' => {
'xml' => undef,
'logger-object' => undef,
'verbosity' => 0,
},
'data' => {
'w' => 0,
'h' => 0,
# x1,y1,x2,y2,w,h
'top-area' => [0, 0, 0, 0, 0, 0],
# x1,y1,x2,y2,w,h
'app-icons-area' => [0, 0, 0, 0, 0, 0],
# x1,y1,x2,y2,w,h
'dock-divider-area' => [0, 0, 0, 0, 0, 0],
# x1,y1,x2,y2,w,h
# these are some common apps which don't change if you swipe screens i think
'hotseat-area' => [0, 0, 0, 0, 0, 0],
# x1,y1,x2,y2,w,h
'home-buttons-area' => [0, 0, 0, 0, 0, 0],
'apps' => {}, # appname => [bounds x1,y1,x2,y2]
'screen-name' => '',
}
};
bless $self => $class;
if( exists $params->{'logger-object'} ){ $self->{'_private'}->{'logger-object'} = $params->{'logger-object'} } else { $self->{'_private'}->{'logger-object'} = Mojo::Log->new() }
if( exists $params->{'verbosity'} ){ $self->{'_private'}->{'verbosity'} = $params->{'verbosity'} } else { $self->{'_private'}->{'verbosity'} = Mojo::Log->new() }
# we now have a log and verbosity
my $log = $self->log;
my $verbosity = $self->verbosity;
if( exists $params->{'data'} ){
my $d = $self->{'data'};
my $p = $params->{'data'};
for my $k (sort keys %$d){
if( exists($p->{$k}) && defined($p->{$k}) ){
if( $self->set($k, $p->{$k}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'set()'." has failed for input parameter '$k', is its type as expected (".ref($d->{$k}).")?"); return undef }
}
}
}
if( exists $params->{'xml-string'} ){
if( ! defined $self->fromXML({'xml-string' => $params->{'xml-string'}}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'fromXML()'." has failed."); return undef }
} elsif( exists $params->{'xml-filename'} ){
if( ! defined $self->fromXML({'xml-filename' => $params->{'xml-filename'}}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'fromXML()'." has failed this XML file: '".$params->{'xml-filename'}."'."); return undef }
}
return $self;
}
# parses a UI Automator XML dump either from
# file: 'xml-filename' => ...
# or from a string: 'xml-string' => ...
# It returns 1 on error, 0 on success
# The big problem is incompatibility of the XML between Android API versions
# if you specify 'fully' => 1, it works fine with our real phone
# but it fails with API 30. So, we are happy only to find the width and
# height from that XML. That means that all fields except 'w' and 'h'
# will not be found here.
# my $xmlstring = $self->dump_current_screen_ui();
# my $sl = Android::ElectricSheep::Automator::ScreenLayout->new({'xml-string' => $xmlstring});
# will call fromXML();
# Or make it yourself: adb shell uiautomator dump outfile
# or adb exec-out uiautomator dump /dev/tty | awk '{gsub("UI hierchary dumped to: /dev/tty", "");print}'
sub fromXML {
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 $doc;
if( exists $params->{'xml-filename'} ){
$doc = XML::LibXML->load_xml(location => $params->{'xml-filename'});
} elsif( exists $params->{'xml-string'} ){
$doc = XML::LibXML->load_xml(string => $params->{'xml-string'});
}
if( ! defined $doc ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'XML::XPath->new()'." has failed."); return undef }
# find a node AT THE TOP for setting the width and height
# text=""
# content-desc=""
# resource-id=""
# class="android.view.FrameLayout"
my $xpath = '/hierarchy/node[@text=\'\' and @resource-id=\'\' and @class=\'android.widget.FrameLayout\']';
my $numframes = 0; # paranoid check how many frames found?
my @nodes = eval { $doc->findnodes($xpath) };
if( $@ ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'findnodes()'." has failed for this xpath: $xpath : $@"); return undef };
foreach my $anode (@nodes){
# the whole screen size is in bounds
my $bounds = $anode->getAttribute('bounds');
$bounds =~ s/\s+//;
if( $bounds =~ /^\[(\d+),(\d+)\]\[(\d+),(\d+)\]$/ ){
my ($x1, $y1, $x2, $y2) = ($1, $2, $3, $4);
$self->set('w', $x2-$x1);
$self->set('h', $y2-$y1);
} else { $log->error($anode."\n${whoami} (via $parent), line ".__LINE__." : error, above node has invalid bounds/1."); return undef }
$numframes++;
}
if( $numframes != 1 ){ $log->error($doc."\n${whoami} (via $parent), line ".__LINE__." : error, failed to find exactly one node with XPath=${xpath} but found ${numframes} instead, see above xml."); return undef }
if( exists($params->{'fully'}) && defined($params->{'fully'}) && ($params->{'fully'}==1) ){
# find the app-icons-area, it may not be there
# text=""
# content-desc=""
# resource-id="com.huawei.android.launcher:id/workspace_screen" <<<< we will filterout huawei
# class="android.view.ViewGroup"
# ONLY XPATH1 is supported, so no ends-with or matching regex, we have to filter ourselves
#my $xpath = '//node[@text=\'\' and @class=\'android.view.ViewGroup\' and ends-with(@resource-id, \'/workspace_screen\')]';
$xpath = '//node[@text=\'\' and @content-desc=\'\' and @class=\'android.view.ViewGroup\' and contains(@resource-id, \'/workspace_screen\')]';
$numframes = 0; # paranoid check how many frames found?
@nodes = eval { $doc->findnodes($xpath) };
if( $@ ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'findnodes()'." has failed for this xpath: $xpath"); return undef };
foreach my $anode (@nodes){
my $resource_id = $anode->getAttribute('resource-id');
if( $resource_id =~ /\/workspace_screen$/i ){
my $bounds = $anode->getAttribute('bounds');
$bounds =~ s/\s+//;
if( $bounds =~ /^\[(\d+),(\d+)\]\[(\d+),(\d+)\]$/ ){
my ($x1, $y1, $x2, $y2) = ($1, $2, $3, $4);
$self->set('app-icons-area', [$x1, $y1, $x2, $y2, $x2-$x1, $y2-$y1]);
} else { $log->error($anode."\n${whoami} (via $parent), line ".__LINE__." : error, above node has invalid bounds/2."); return undef }
$numframes++;
} else { $log->error($anode."\n${whoami} (via $parent), line ".__LINE__." : error, above node has unexpected 'resource-id' ($resource_id), does not end in 'workspace_screen'."); return undef }
}
if( $numframes != 1 ){ $log->warn($doc."\n${whoami} (via $parent), line ".__LINE__." : error, failed to find exactly one node with XPath=${xpath} but found ${numframes} instead, see above xml.") }
( run in 0.872 second using v1.01-cache-2.11-cpan-39bf76dae61 )