Android-ElectricSheep-Automator
view release on metacpan or search on metacpan
lib/Android/ElectricSheep/Automator/Plugins/Apps/Viber.pm view on Meta::CPAN
package Android::ElectricSheep::Automator::Plugins::Apps::Viber;
use strict;
use warnings;
use parent 'Android::ElectricSheep::Automator::Plugins::Apps::Base';
use Time::HiRes qw/usleep/;
use Encode qw/encode_utf8 decode_utf8/;
use Data::Roundtrip qw/perl2dump no-unicode-escape-permanently/;
use Android::ElectricSheep::Automator::XMLParsers;
sub new {
my ($class, $params) = @_;
my $self = $class->SUPER::new({
%$params,
'child-class' => $class,
});
$self->{'_private'}->{'appname'} = 'com.viber.voip';
return $self;
}
# keeps pressing the back-arrow at the top of the app to hopefully
# arrive at the main activity of the app,
# TODO: is there a way to tell it to go to main activity ? WelcomeActivity does not seem to work
# returns 1 on failure, 0 on success.
sub navigate_to_viber_home_activity {
my ($self, $params) = @_;
my $parent = ( caller(1) )[3] || "N/A";
my $whoami = ( caller(0) )[3];
my $log = $self->log();
my $verbosity = $self->verbosity();
my ($outbase, $outfile);
# for debugging purposes, save each UI we get here
$outbase = exists($params->{'outbase'}) ? $params->{'outbase'} : undef;
my ($ui, $dom, $xc, $asel, @nodes, $N, $node, $boundstr, $bounds);
my $repeats = 3;
my $repeatsUI = 3;
ONBACKARROW:
while(--$repeats > 0){
# we assume the app is open and at the foreground
# get the UI
$outfile = defined($outbase) ? $outbase.'_main.xml' : undef;
do {
$ui = $self->mother->dump_current_screen_ui({'filename'=>$outfile});
usleep(0.75);
} while( ($repeatsUI-- > 0) && ! defined($ui) );
if( ! defined $ui ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to dump the UI, call to ".'dump_current_screen_ui()'." has failed after a number of repeats. I am not sure what the problem is, most likely a race conditio...
$dom = $ui->{'XML::LibXML'};
$xc = $ui->{'XML::LibXML::XPathContext'};
$asel = '//node'
. '['
. ' matches(@content-desc,\'navigate\s+up\',"i")'
. ' and matches(@class,\'ImageButton\',"i")'
. ' and @package="com.viber.voip"'
. ' and matches(@bounds,\'^\[\',"i")'
. ']'
;
@nodes = $xc->findnodes($asel);
$N = scalar @nodes;
if( $N == 0 ){
if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : no nodes matching this XPath selector (for getting the 'back-arrow' icon, hopefully, this means we reached home-page of the app): ${asel}") }
last ONBACKARROW;
} elsif( $N > 1 ){ $log->error("--begin matched nodes:\n".join("\n", @nodes)."\n--end nodes matched.\n\n${whoami} (via $parent), line ".__LINE__." : error, matched more than one node (see above) with this XPath selector (for getting the 'Chats' ico...
$node = $nodes[0];
# click the 'back-arrow' at the top
$boundstr = $node->getAttribute('bounds');
if( ! defined $boundstr ){ $log->error("${node}\n\n${whoami} (via $parent), line ".__LINE__." : error, above node does not have attribute 'bounds'."); return 1 }
if( $boundstr !~ /\[\s*(\d+)\s*,\s*(\d+)\]\s*\[\s*(\d+)\s*,\s*(\d+)\]/ ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to parse bounds string: '$boundstr'."); return 1 }
$bounds = [[$1,$2],[$3,$4]];
# click it
if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : clicking the back-arrow ...") }
if( $self->mother->tap({'bounds' => $bounds}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to tap on $bounds."); return 1 }
usleep(0.75);
}
if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : hopefully we are now at the very first screen of the app.") }
return 0 # success
}
# "type" the specified message into
# the message will be sanitised in input_text(), spaces will be replaced with %s
# unicode is not supported
sub send_message {
my ($self, $params) = @_;
my $parent = ( caller(1) )[3] || "N/A";
my $whoami = ( caller(0) )[3];
my $log = $self->log();
my $verbosity = $self->verbosity();
my ($recipient, $message, $outbase, $outfile);
if( ! exists($params->{'recipient'}) || ! defined($recipient=$params->{'recipient'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'recipient' was not specified."); return undef }
if( ! exists($params->{'message'}) || ! defined($message=$params->{'message'}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, input parameter 'message' was not specified."); return undef }
# for debugging purposes, save each UI we get here
$outbase = exists($params->{'outbase'}) ? $params->{'outbase'} : undef;
# do everything except clicking the send button
my $mock = exists($params->{'mock'}) ? $params->{'mock'} : 0;
my ($dom, $xc, $asel, @nodes, $N, $node, $boundstr, $bounds);
if( $self->navigate_to_viber_home_activity({'outbase'=>$outbase}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, call to ".'navigate_to_viber_home_activity()'." has failed."); return undef }
usleep(1.5);
# we assume the app is open and at the foreground
# get the UI
$outfile = defined($outbase) ? $outbase.'_main.xml' : undef;
# there is always the chance you get 'Is taking too long'...
# and all will fail TODO!
my $ui;
my $repeatsUI = 3;
do {
if( $verbosity > 0 ){ $log->info("${whoami} (via $parent), line ".__LINE__." : calling ".'dump_current_screen_ui()'." for at repeat $repeatsUI ...") }
$ui = $self->mother->dump_current_screen_ui({'filename'=>$outfile});
usleep(0.75);
} while( ($repeatsUI-- > 0) && (! defined($ui)) );
if( ! defined $ui ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to dump the UI, call to ".'dump_current_screen_ui()'." has failed after a number of repeats. I am not sure what the problem is, most likely a race condition...
$dom = $ui->{'XML::LibXML'};
$asel = '//node[@text="Chats" and @resource-id="com.viber.voip:id/bottomBarItemTitle"]';
@nodes = $dom->findnodes($asel);
$N = scalar @nodes;
if( $N == 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to find any nodes matching this XPath selector (for getting the 'Chats' icon): ${asel}"); return undef }
elsif( $N > 1 ){ $log->error("--begin matched nodes:\n".join("\n", @nodes)."\n--end nodes matched.\n\n${whoami} (via $parent), line ".__LINE__." : error, matched more than one node (see above) with this XPath selector (for getting the 'Chats' icon):...
$node = $nodes[0];
# click the 'Chats' at the bottom
$boundstr = $node->getAttribute('bounds');
if( ! defined $boundstr ){ $log->error("${node}\n\n${whoami} (via $parent), line ".__LINE__." : error, above node does not have attribute 'bounds'."); return undef }
if( $boundstr !~ /\[\s*(\d+)\s*,\s*(\d+)\]\s*\[\s*(\d+)\s*,\s*(\d+)\]/ ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to parse bounds string: '$boundstr'."); return undef }
$bounds = [[$1,$2],[$3,$4]];
# click it
if( $self->mother->tap({'bounds' => $bounds}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to tap on $bounds."); return undef }
sleep(1);
# now we are on the chats screen, in the centre pane there are all our contacts
# get the ui for this screen
$outfile = defined($outbase) ? $outbase.'_chats.xml' : undef;
$ui = $self->mother->dump_current_screen_ui({'filename'=>$outfile});
$dom = $ui->{'XML::LibXML'};
$xc = $ui->{'XML::LibXML::XPathContext'};
$asel = '//node'
. '['
. '@text'
. ' and matches(@text,\''.$params->{'recipient'}.'\',"i")'
. ' and @resource-id="com.viber.voip:id/from"'
. ']'
;
@nodes = $xc->findnodes($asel);
$N = scalar @nodes;
if( $N == 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to find any nodes matching this XPath selector (for getting the recipient (".$params->{'recipient'}.") from the contacts on the central pane: ${asel}"); return un...
elsif( $N > 1 ){ $log->error("--begin matched nodes:\n".join("\n", @nodes)."\n--end nodes matched.\n\n${whoami} (via $parent), line ".__LINE__." : error, matched more than one node (see above) with this XPath selector (for getting the recipient (".$...
$node = $nodes[0];
# click the Recipient contact name at the bottom
$boundstr = $node->getAttribute('bounds');
if( ! defined $boundstr ){ $log->error("${node}\n\n${whoami} (via $parent), line ".__LINE__." : error, above node does not have attribute 'bounds'."); return undef }
if( $boundstr !~ /\[\s*(\d+)\s*,\s*(\d+)\]\s*\[\s*(\d+)\s*,\s*(\d+)\]/ ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to parse bounds string: '$boundstr'."); return undef }
$bounds = [[$1,$2],[$3,$4]];
# click it
if( $self->mother->tap({'bounds' => $bounds}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to tap on $bounds."); return undef }
usleep(1.5);
# Put the text into the text-edit Message... (note: ... is unicode ellipses something)
# get the UI
$outfile = defined($outbase) ? $outbase.'_chat.xml' : undef;
$ui = $self->mother->dump_current_screen_ui({'filename'=>$outfile});
$dom = $ui->{'XML::LibXML'};
$xc = $ui->{'XML::LibXML::XPathContext'};
$asel = '//node'
. '['
. ' matches(@class,\'EditText$\',"i")'
. ' and @resource-id="com.viber.voip:id/send_text"'
. ' and @package="com.viber.voip"'
. ']'
;
@nodes = $xc->findnodes($asel);
$N = scalar @nodes;
if( $N == 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to find any nodes matching this XPath selector (for getting the specified recipient (".$params->{'recipient'}."): ${asel}"); return undef }
elsif( $N > 1 ){ $log->error("--begin matched nodes:\n".join("\n", @nodes)."\n--end nodes matched.\n\n${whoami} (via $parent), line ".__LINE__." : error, matched more than one node (see above) with this XPath selector (for getting the specified reci...
$node = $nodes[0];
$boundstr = $node->getAttribute('bounds');
if( ! defined $boundstr ){ $log->error("${node}\n\n${whoami} (via $parent), line ".__LINE__." : error, above node does not have attribute 'bounds'."); return undef }
if( $boundstr !~ /\[\s*(\d+)\s*,\s*(\d+)\]\s*\[\s*(\d+)\s*,\s*(\d+)\]/ ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to parse bounds string: '$boundstr'."); return undef }
$bounds = [[$1,$2],[$3,$4]];
# add the text in the node
# the message will be sanitised in input_text(), spaces will be replaced with %s
# unicode is not supported
# TODO: perhaps warn about unicode in message?
if( $self->mother->input_text({'bounds' => $bounds, 'text' => $message}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to input text on ${boundstr}. Note that there may be a maximum length for the message. Your message w...
sleep(1);
# and send!
# on the same XML we search for the send button
$asel = '//node'
. '['
. ' matches(@class,\'FrameLayout$\',"i")'
. ' and @resource-id="com.viber.voip:id/btn_send"'
. ' and @package="com.viber.voip"'
. ']'
;
@nodes = $xc->findnodes($asel);
$N = scalar @nodes;
if( $N == 0 ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to find any nodes matching this XPath selector (for getting the recipient (".$params->{'recipient'}.") from the contacts on the central pane: ${asel}"); return un...
elsif( $N > 1 ){ $log->error("--begin matched nodes:\n".join("\n", @nodes)."\n--end nodes matched.\n\n${whoami} (via $parent), line ".__LINE__." : error, matched more than one node (see above) with this XPath selector (for getting the recipient (".$...
$node = $nodes[0];
# click the Send button
$boundstr = $node->getAttribute('bounds');
if( ! defined $boundstr ){ $log->error("${node}\n\n${whoami} (via $parent), line ".__LINE__." : error, above node does not have attribute 'bounds'."); return undef }
if( $boundstr !~ /\[\s*(\d+)\s*,\s*(\d+)\]\s*\[\s*(\d+)\s*,\s*(\d+)\]/ ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to parse bounds string: '$boundstr'."); return undef }
$bounds = [[$1,$2],[$3,$4]];
# I know it does not support sending unicode to a textbox but ...
# TODO: check with a unicode recipient handle to see if this works
if( $verbosity > 0 ){ $log->info("To: '".Encode::decode_utf8($recipient)."'\nMessage:\n".Encode::decode_utf8($message)."\n--end message.\n${whoami} (via $parent), line ".__LINE__." : about to send the above message ...") }
if( $mock == 0 ){
# click it if not mocking
if( $self->mother->tap({'bounds' => $bounds}) ){ $log->error("${whoami} (via $parent), line ".__LINE__." : error, failed to tap on $bounds."); return undef }
} else {
$log->warn("${whoami} (via $parent), line ".__LINE__." : The 'Send' button was not clicked because mock is ON.")
}
usleep(0.8);
return {};
}
# only pod below
=pod
=encoding utf8
=head1 NAME
Android::ElectricSheep::Automator::Plugins::Apps::Viber - Control the Viber app from your desktop via the ElectricSheep Automator
=head1 VERSION
Version 0.09
=head1 WARNING
Current distribution is extremely alpha. API may change.
=head1 SYNOPSIS
An L<Android::ElectricSheep::Automator> plugin which
interacts with the Viber app from the desktop.
use Android::ElectricSheep::Automator::Plugins::Apps::Viber;
my $viber = Android::ElectricSheep::Automator::Plugins::Apps::Viber->new({
'configfile' => $configfile,
'verbosity' => 1,
# we already have a device connected and ready to control
'device-is-connected' => 1,
});
# go to home screen to start fresh
$plugobj->mother->home_screen();
# open the viber app
$plugobj->open_app() or die
# is the app running now?
$plugobj->is_app_running() or die
$plugobj->send_message({
'recipient' => 'My Notes', # some of your contacts
# 1) no unicode, 2) each space must be converted to '%s'
'message' => 'thank%syou'
}) or die;
=head1 MOTIVATION
I wanted to send viber messages from my beloved Linux Desktop
using the command line. I did not want to install Viber app on
my smartphone. I do not use a smartphone except for
reading the news.
=head1 INSTALLING VIBER
Firstly, I installed an Android emulator
in my desktop computer, as described here
L<https://metacpan.org/pod/Android::ElectricSheep::Automator#Android-Emulators>
Note that some Android emulators do not allow access to
Google App Store (whatever is called). You can only install apps
via third party which is not very safe (even for your emulator).
See this for how to enable the App Store:
L<https://stackoverflow.com/questions/71815181/how-can-i-get-google-play-to-work-on-android-emulator-in-android-studio-bumblebe>
Fortunately, Viber offers a version of its app from its website,
calling it an "update". And you will not need to give any personal
details to Google to download it.
It is located here
L<https://download.cdn.viber.com/android/android_32_64/viber-lst.apk>
(via L<https://www.viber.com/en/download-android-update>).
=head1 CONSTRUCTOR
=head2 new($params)
Creates a new C<Android::ElectricSheep::Automator::Plugins::Apps::Viber> object. C<$params>
is a hash reference used to pass initialization options. These options are
passed onto the main constructor.
Refer to the documentation of
L<Android::ElectricSheep::Automator#new($params)> for
A configuration file or hash is required.
Here is an example configuration file to get you started:
</* $VERSION = '0.01'; */>
</* comments are allowed */>
</* and <% vars %> and <% verbatim sections %> */>
{
"Android::ElectricSheep::Automator" : {
"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) */>
},
"Android::ElectricSheep::Automator::Plugins::Apps::Viber" : {
}
}
All sections are mandatory. Setting C<"adb"> to the wrong path will
yield in problems.
=head1 METHODS
=head2 send_message($params)
It sends a message to one of your contacts.
It returns C<undef> on failure or a hashref on success.
C<$params> is a HASH_REF which may or should contain:
=over 4
=item B<C<recipient>>
Required name of the recipient which must be in your contacts.
(Note, I am not sure if a recipient not in the contacts can
still be used).
=item B<C<message>>
Require, the message to send. At the moment B<the message can not be unicode>.
And it can not contain any space character unless it is encoded as C< %s >.
=item B<C<mock>>
Optionally, set this flag to C<1> in order to do everything except hitting
the send button. No message will be sent. For debugging purposes.
=item B<C<outbase>>
Optionally, specify the basename to form filenames for saving UI
dumps. For debugging purposes.
=back
It needs that L<Android::ElectricSheep::Automator#connect_device($params)> to have been called prior to this call.
It returns C<undef> on failure or an (empty) hash on success.
=head2 navigate_to_viber_home_activity($params)
It navigates to the viber app's home screen. E.g.
if you are in your contacts screen, calling this method
will land you to the app's home screen.
It achieves this by continually pressing
the app's back-button (the left arrow at the top)
until it arrives at the home screen of the viber app.
It returns C<1> on failure or a C<0> on success.
C<$params> is a HASH_REF which may or should contain:
=over 4
=item B<C<outbase>>
Optionally, specify the basename to form filenames for saving UI
dumps. For debugging purposes.
=back
It needs that connect_device() to have been called prior to this call.
It returns C<1> on failure or C<0> on success.
=head1 AUTHOR
Andreas Hadjiprocopis, C<< <bliako at cpan.org> >>
=head1 BUGS
Please report any bugs or feature requests to C<bug-Android-ElectricSheep-Automator at rt.cpan.org>, or through
the web interface at
L<https://rt.cpan.org/NoAuth/ReportBug.html?Queue=Android-ElectricSheep-Automator>.
I will be notified, and then you'll
automatically be notified of progress on your bug as I make changes.
=head1 SUPPORT
( run in 1.363 second using v1.01-cache-2.11-cpan-39bf76dae61 )