# # $Id$ # # 89_AndroidDB # # Version 0.7 # # FHEM Integration for Android Devices # # Dependencies: # # 89_AndroidDBHost # # Prerequisits: # # - Enable developer mode on Android device # - Allow USB debugging on Android device # package main; use strict; use warnings; use SetExtensions; sub AndroidDB_Initialize ($) { my ($hash) = @_; $hash->{DefFn} = "AndroidDB::Define"; $hash->{UndefFn} = "AndroidDB::Undef"; $hash->{SetFn} = "AndroidDB::Set"; $hash->{GetFn} = "AndroidDB::Get"; $hash->{AttrFn} = "AndroidDB::Attr"; $hash->{ShutdownFn} = "AndroidDB::Shutdown"; $hash->{parseParams} = 1; $hash->{AttrList} = 'connect:0,1 macros:textField-long preset presetFile '.$readingFnAttributes; $data{RC_layout}{MagentaTVStick} = "AndroidDB::RCLayoutMagentaTVStick"; $data{RC_layout}{MagentaOne} = "AndroidDB::RCLayoutMagentaOne"; $data{RC_layout}{MagentaTVExtended} = "AndroidDB::RCLayoutMagentaTVExt"; } package AndroidDB; use strict; use warnings; use SetExtensions; use Storable qw(dclone); use GPUtils qw(:all); BEGIN { GP_Import(qw( readingsSingleUpdate readingsBulkUpdate readingsBulkUpdateIfChanged readingsBeginUpdate readingsEndUpdate setDevAttrList CommandDefine CommandSet CommandAttr Log3 AttrVal ReadingsVal AssignIoPort InternalTimer asyncOutput gettimeofday parseParams defs attr modules data init_done )) }; # Remote control presets my $PRESET = { 'MagentaTVStick' => { 'ASSISTANT' => 'KEYCODE_ASSIST', 'APPS' => 'KEYCODE_ALL_APPS', 'BACK' => 'KEYCODE_BACK', 'EPG' => 'KEYCODE_TV_INPUT_HDMI_2', 'HOME' => 'KEYCODE_HOME', 'LEFT' => 'KEYCODE_DPAD_LEFT', 'RIGHT' => 'KEYCODE_DPAD_RIGHT', 'UP' => 'KEYCODE_DPAD_UP', 'DOWN' => 'KEYCODE_DPAD_DOWN', 'INFO' => 'KEYCODE_INFO', 'MEGATHEK' => 'KEYCODE_TV_INPUT_HDMI_3', 'MUTE' => 'KEYCODE_VOLUME_MUTE', 'OK' => 'KEYCODE_DPAD_CENTER', 'PLAYPAUSE' => 'KEYCODE_MEDIA_PLAY_PAUSE', 'POWER' => 'KEYCODE_POWER', 'PROG+' => 'KEYCODE_CHANNEL_UP', 'PROG-' => 'KEYCODE_CHANNEL_DOWN', 'RECORD' => 'KEYCODE_MEDIA_RECORD', 'SEARCH' => 'KEYCODE_TV_INPUT_HDMI_1', 'STREAMINFO' => '--longpress,KEYCODE_INFO', 'TV' => 'KEYCODE_TV_INPUT_HDMI_4', 'VOL+' => 'KEYCODE_VOLUME_UP', 'VOL-' => 'KEYCODE_VOLUME_DOWN' }, 'MagentaOne' => { 'ASSISTANT' => 'KEYCODE_ASSIST', 'APPS' => 'KEYCODE_ALL_APPS', 'BACK' => 'KEYCODE_BACK', 'EPG' => 'KEYCODE_TV_INPUT_HDMI_2', 'HOME' => 'KEYCODE_HOME', 'LEFT' => 'KEYCODE_DPAD_LEFT', 'RIGHT' => 'KEYCODE_DPAD_RIGHT', 'UP' => 'KEYCODE_DPAD_UP', 'DOWN' => 'KEYCODE_DPAD_DOWN', 'INFO' => 'KEYCODE_INFO', 'MEGATHEK' => 'KEYCODE_TV_INPUT_HDMI_3', 'MUTE' => 'KEYCODE_VOLUME_MUTE', 'OK' => 'KEYCODE_DPAD_CENTER', 'PLAYPAUSE' => 'KEYCODE_MEDIA_PLAY_PAUSE', 'POWER' => 'KEYCODE_POWER', 'PROG+' => 'KEYCODE_CHANNEL_UP', 'PROG-' => 'KEYCODE_CHANNEL_DOWN', 'RECORD' => 'KEYCODE_MEDIA_RECORD', 'SEARCH' => 'KEYCODE_TV_INPUT_HDMI_1', 'STREAMINFO' => '--longpress,KEYCODE_INFO', 'TV' => 'KEYCODE_TV_INPUT_HDMI_4', 'VOL+' => 'KEYCODE_VOLUME_UP', 'VOL-' => 'KEYCODE_VOLUME_DOWN' }, 'AndroidTV' => { 'APPS' => 'KEYCODE_ALL_APPS', 'BACK' => 'KEYCODE_BACK', 'EPG' => 'KEYCODE_GUIDE', 'HOME' => 'KEYCODE_HOME', 'LEFT' => 'KEYCODE_DPAD_LEFT', 'RIGHT' => 'KEYCODE_DPAD_RIGHT', 'UP' => 'KEYCODE_DPAD_UP', 'DOWN' => 'KEYCODE_DPAD_DOWN', 'INFO' => 'KEYCODE_INFO', 'MUTE' => 'KEYCODE_VOLUME_MUTE', 'OK' => 'KEYCODE_DPAD_CENTER', 'PLAYPAUSE' => 'KEYCODE_MEDIA_PLAY_PAUSE', 'POWER' => 'KEYCODE_POWER', 'PROG+' => 'KEYCODE_CHANNEL_UP', 'PROG-' => 'KEYCODE_CHANNEL_DOWN', 'RECORD' => 'KEYCODE_MEDIA_RECORD', 'SEARCH' => 'KEYCODE_SEARCH', 'VOL+' => 'KEYCODE_VOLUME_UP', 'VOL-' => 'KEYCODE_VOLUME_DOWN', 'RED' => 'KEYCODE_PROG_RED', 'GREEN' => 'KEYCODE_PROG_GREEN', 'BLUE' => 'KEYCODE_PROG_BLUE', 'YELLOW' => 'KEYCODE_PROG_YELLOW' } }; # Command presets my $MACRO = { }; sub Define ($$$) { my ($hash, $a, $h) = @_; my $usage = "define $hash->{NAME} AndroidDB {NameOrIP[:Port]}"; return $usage if (scalar(@$a) < 3); # Set parameters my ($devName, $devPort) = split (':', $$a[2]); $hash->{ADBDevice} = $devName.':'.($devPort // '5555'); AssignIoPort ($hash); $attr{$hash->{NAME}}{webCmd} = 'remoteControl'; # Clone predefined presets and macros $hash->{adb}{preset} = dclone $PRESET; $hash->{adb}{macro} = dclone $MACRO; InitAfterStart ($hash) if ($init_done); return undef; } sub InitAfterStart ($) { my ($hash) = @_; my @presets = map { $_ eq '_custom_' ? () : $_ } keys %{$hash->{adb}{preset}}; if (scalar(@presets) > 0) { my $attrPreset = 'preset:select,'.join(',',@presets); my $attributes = $modules{AndroidDB}{AttrList}; $attributes =~ s/preset/$attrPreset/; setDevAttrList ($hash->{NAME}, $attributes); } } sub Undef ($$) { my ($hash, $name) = @_; AndroidDBHost::Disconnect ($hash); return undef; } sub Shutdown ($) { my ($hash) = @_; AndroidDBHost::Disconnect ($hash); } sub Set ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; my $opt = shift @$a // return 'No set command specified'; # # Preprare list of available commands # # Standard commands my $options = 'exportPresets reboot rollHorz rollVert sendKey sendNumKeys sendText shell tap'; # Add remote control key presets to command remoteControl my @presetList = (); my $preset = AttrVal ($name, 'preset', ''); push @presetList, sort keys %{$hash->{adb}{preset}{$preset}} if ($preset ne '' && exists($hash->{adb}{preset}{$preset})); push @presetList, sort keys %{$hash->{adb}{preset}{_custom_}} if (exists($hash->{adb}{preset}{_custom_})); my %e1; $options .= ' remoteControl:'.join(',', sort grep { !$e1{$_}++ } @presetList) if (scalar(@presetList) > 0); # Add remote control layouts to command createRemote my @layouts = keys %{$data{RC_layout}}; $options .= ' createRemote:'.join(',', sort @layouts) if (scalar(@layouts) > 0); # Add command macros to command list my @macroList = (); push @macroList, sort keys %{$hash->{adb}{macro}{$preset}} if ($preset ne '' && exists($hash->{adb}{macro}{$preset})); push @macroList, sort keys %{$hash->{adb}{macro}{_custom_}} if (exists($hash->{adb}{macro}{_custom_})); my %e2; $options .= ' '.join(' ', sort grep { !$e2{$_}++ } @macroList) if (scalar(@macroList) > 0); my $lcopt = lc($opt); if ($lcopt eq 'sendkey') { return "Usage: set $name $opt [--longpress] KeyCode [...]" if (scalar(@$a) == 0); return "Only one KeyCode allowed when option '--longpress' is specified" if ($$a[0] eq '--longpress' && scalar(@$a) > 2); my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'keyevent', @$a); return $error if ($rc == 0); } elsif ($lcopt eq 'sendnumkeys') { my $number = shift @$a // return "Usage: set $name $opt Number"; return 'Parameter Number must be in range 1-9999' if ($number !~ /^[0-9]+$/ || $number < 1 || $number > 9999); my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'text', $number); return $error if ($rc == 0); } elsif ($lcopt eq 'sendtext') { return "Usage: set $name $opt Text" if (scalar(@$a) < 1); my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'text', join(' ',@$a)); return $error if ($rc == 0); } elsif ($lcopt eq 'reboot') { my ($rc, $result, $error) = AndroidDBHost::Run ($hash, $opt); return $error if ($rc == 0); } elsif ($lcopt eq 'shell') { return "Usage: set $name $opt ShellCommand" if (scalar(@$a) == 0); my ($rc, $result, $error) = AndroidDBHost::Run ($hash, $opt, '.*', @$a); return $result.$error, } elsif ($lcopt eq 'remotecontrol') { my $macroName = shift @$a // return "Usage: set $name $opt MacroName"; $preset = '_custom_' if (exists($hash->{adb}{preset}{_custom_}) && exists($hash->{adb}{preset}{_custom_}{$macroName})); return "Preset and/or macro $macroName not defined in preset $preset" if ($preset eq '' || !exists($hash->{adb}{preset}{$preset}{$macroName})); my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'keyevent', split (',', $hash->{adb}{preset}{$preset}{$macroName})); return $error if ($rc == 0); } elsif ($lcopt eq 'tap') { my ($x, $y) = @$a; return "Usage: set $name $opt tap X Y" if (!defined($y)); my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'tap', $x, $y); return $error if ($rc == 0); } elsif ($lcopt eq 'rollhorz' || $lcopt eq 'rollvert') { my $delta = shift @$a // return "Usage: set $name $opt Delta"; my ($dx, $dy) = $opt eq 'rollhorz' ? ($delta, 0) : (0, $delta); my ($rc, $result, $error) = AndroidDBHost::Run ($hash, 'shell', '.*', 'input', 'roll', $dx, $dy); return $error if ($rc == 0); } elsif ($lcopt eq 'createremote') { my $layout = shift @$a // return "Usage: set $name $opt LayoutName"; my $rcName = $name.'_RC'; return "$name: Can't create remotecontrol device $rcName" if (CommandDefine (undef, "$rcName remotecontrol")); Log3 $name, 2, "$name: Created remotecontrol device $rcName"; return "$name: Can't select layout $layout for remotecontrol device $rcName" if (CommandSet (undef, "$rcName layout $layout")); Log3 $name, 2, "Selected layout $layout for $rcName"; my $room = AttrVal ($name, 'room', ''); if ($room ne '') { Log3 $name, 2, "$name: Assigning $rcName to room $room"; CommandAttr (undef, "$rcName room $room"); } CommandSet (undef, "$rcName makenotify $name"); CommandAttr (undef, "$name group $name"); CommandAttr (undef, "$rcName group $name"); Log3 $name, 2, "Created notify device notify_$rcName"; } elsif ($lcopt eq 'exportpresets') { my $filename = shift @$a // return "Usage: set $name $opt Filename"; my $rc = ExportPresets ($hash, $filename); return "Error while saving presets to file $filename" if ($rc == 0); return "Presets saved to file $filename"; } elsif (exists($hash->{adb}{macro}{_custom_}) && exists($hash->{adb}{macro}{_custom_}{$opt})) { my ($args, $pars) = parseParams ($hash->{adb}{macro}{_custom_}{$opt}); my $cmd = shift @$args; my ($rc, $result, $error) = AndroidDBHost::Run ($hash, $cmd, '.*', @$args); return $rc == 0 ? $error : $result; } elsif ($preset ne '' && exists($hash->{adb}{macro}{$preset}) && exists($hash->{adb}{macro}{$preset}{$opt})) { my ($args, $pars) = parseParams ($hash->{adb}{macro}{$preset}{$opt}); my $cmd = shift @$args; my ($rc, $result, $error) = AndroidDBHost::Run ($hash, $cmd, '.*', @$args); return $rc == 0 ? $error : $result; } else { return "Unknown argument $opt, choose one of $options"; } } sub Get ($@) { my ($hash, $a, $h) = @_; my $name = shift @$a; my $opt = shift @$a // return 'No get command specified'; my $options = 'keyPreset'; my @presetList = sort keys %{$hash->{adb}{preset}}; $options .= ':'.join(',', @presetList) if (scalar(@presetList) > 0); my @macroList = sort keys %{$hash->{adb}{macro}}; $options .= ' cmdPreset:'.join(',', @macroList) if (scalar(@macroList) > 0); my $attrPreset = AttrVal ($name, 'preset', ''); my $lcopt = lc($opt); if ($lcopt eq 'keypreset') { my $preset = shift @$a // $attrPreset; return "Usage: get $name $opt PresetName" if ($preset eq ''); return "Key preset $preset not found" if (!exists($hash->{adb}{preset}{$preset})); my $presetDef = "Definition of key preset $preset:

"; foreach my $macro (sort keys %{$hash->{adb}{preset}{$preset}}) { $presetDef .= "$macro = $hash->{adb}{preset}{$preset}{$macro}
"; } return $presetDef; } elsif ($lcopt eq 'cmdpreset') { my $preset = shift @$a // $attrPreset; return "Usage: get $name $opt PresetName" if ($preset eq ''); return "Command preset $preset not found" if (!exists($hash->{adb}{macro}{$preset})); my $macroDef = "Definition of command preset $preset:

"; foreach my $macro (sort keys %{$hash->{adb}{macro}{$preset}}) { $macroDef .= "$macro = $hash->{adb}{macro}{$preset}{$macro}
"; } return $macroDef; } else { return "Unknown argument $opt, choose one of $options"; } } sub Attr ($@) { my ($cmd, $name, $attrName, $attrVal) = @_; my $hash = $defs{$name}; if ($cmd eq 'set') { if ($attrName eq 'macros') { foreach my $macroDef (split /;/, $attrVal) { my ($macroName, $macroPar) = split (':', $macroDef); if (!defined($macroDef)) { Log3 $name, 2, "Missing defintion for macro $macroName"; return "Missing definition for macro $macroName"; } if ($macroPar =~ /^[0-9]+/ || $macroPar =~ /^KEYCODE_/) { $hash->{adb}{preset}{_custom_}{$macroName} = $macroPar; } else { $hash->{adb}{macro}{_custom_}{$macroName} = $macroPar; } } } elsif ($attrName eq 'presetFile') { if (!LoadPresets ($hash, $attrVal)) { return "Cannot load presets from file $attrVal"; } } elsif ($attrName eq 'connect') { AndroidDBHost::Connect ($hash) if (!$init_done && $attrVal eq '1'); } } elsif ($cmd eq 'del') { delete $hash->{adb}{preset}{_custom_} if (exists($hash->{adb}{preset}{_custom_})); } return undef; } ############################################################################## # Load macro definitions from file # File format: # - Lines starting with a # are treated as comments # - Empty lines are ignored # - Lines containing a single word are setting the preset name for the # following lines # - Lines in format Name:KeyList are defining a macro. KeyList is a comma # separated list of keycodes. # - Lines in format Name:Command:Parameters are defining a command macro. ############################################################################## sub LoadPresets ($$) { my ($hash, $fileName) = @_; # Read file my @lines; if (open (PRESETFILE, "<$fileName")) { @lines = ; close (PRESETFILE); } else { ShowMessage ($hash, 2, "Can't open file $fileName"); return 0; } # Delete old presets my @presets = keys %{$hash->{adb}{preset}}; my @macros = keys %{$hash->{adb}{macro}}; foreach my $e (@presets) { delete $hash->{adb}{preset}{$e} if ($e ne '_custom_'); } foreach my $e (@macros) { delete $hash->{adb}{macro}{$e} if ($e ne '_custom_'); } chomp @lines; my $presetName = ''; foreach my $l (@lines) { next if ($l =~ /^#/); # Comments are allowed my ($macroName, $macroPar) = split (':', $l); if (!defined($macroPar)) { next if (!defined($macroName) || $macroName eq ''); if ($macroName !~ /^[a-zA-Z0-9-_]+$/) { ShowMessage ($hash, 2, "Invalid character in macro name $macroName in file $fileName"); return 0; } $presetName = $macroName; } else { if ($presetName eq '') { ShowMessage ($hash, 2, "No preset name set for macro name $macroName in file $fileName"); return 0; } if ($macroPar =~ /^[0-9]+/ || $macroPar =~ /^(KEYCODE_|--longpress)/) { $hash->{adb}{preset}{$presetName}{$macroName} = $macroPar; } else { $hash->{adb}{macro}{$presetName}{$macroName} = $macroPar; } } } # Init options of attribute 'preset' InitAfterStart ($hash); return 1; } sub ExportPresets ($$) { my ($hash, $filename) = @_; if (open (PRESETFILE, ">$filename")) { foreach my $preset (sort keys %{$hash->{adb}{preset}}) { next if ($preset eq '_custom_'); print PRESETFILE "#\n# Preset $preset\n#\n$preset\n#\n"; foreach my $macro (sort keys %{$hash->{adb}{preset}{$preset}}) { print PRESETFILE "$macro:$hash->{adb}{preset}{$preset}{$macro}\n"; } if (exists($hash->{adb}{macro}{$preset})) { foreach my $macro (sort keys %{$hash->{adb}{macro}{$preset}}) { print PRESETFILE "$macro:$hash->{adb}{macro}{$preset}{$macro}\n"; } } } foreach my $preset (sort keys %{$hash->{adb}{macro}}) { next if ($preset eq '_custom_' || exists($hash->{adb}{preset}{$preset})); print PRESETFILE "#\n# Preset $preset\n#\n$preset\n#\n"; foreach my $macro (sort keys %{$hash->{adb}{macro}{$preset}}) { print PRESETFILE "$macro:$hash->{adb}{macro}{$preset}{$macro}\n"; } } close (PRESETFILE); return 1; } return 0; } sub ShowMessage ($$$) { my ($hash, $level, $msg) = @_; Log3 $hash->{NAME}, $level, $msg; if ($init_done && exists($hash->{CL})) { my $cl = $hash->{CL}; InternalTimer (gettimeofday()+1, sub { asyncOutput ($cl, $msg) }, undef, 1); } } ############################################################################## # Remote control layout for Magenta TV Stick ############################################################################## sub RCLayoutMagentaTVStick () { my @row = ( 'sendKey KEYCODE_POWER:POWEROFF,:blank,sendKey KEYCODE_VOLUME_MUTE:MUTE', ':blank,sendKey KEYCODE_TV_INPUT_HDMI_3:PS3Rectangle,:blank', ':blank,sendKey KEYCODE_ALL_APPS:TOOLS,:blank', 'sendKey KEYCODE_TV_INPUT_HDMI_4:TV,:blank,sendKey KEYCODE_TV_INPUT_HDMI_2:GUIDE', ':blank,sendKey KEYCODE_DPAD_UP:UP,:blank', 'sendKey KEYCODE_DPAD_LEFT:LEFT,sendKey KEYCODE_DPAD_CENTER:OK,sendKey KEYCODE_DPAD_RIGHT:RIGHT', ':blank,sendKey KEYCODE_DPAD_DOWN:DOWN,:blank', 'sendKey KEYCODE_BACK:BACKDroid,sendKey KEYCODE_HOME:HOMEDroid,sendKey KEYCODE_INFO:INFO', 'sendKey KEYCODE_MEDIA_RECORD:REC,sendKey KEYCODE_TV_INPUT_HDMI_1:SEARCH,sendKey KEYCODE_VOLUME_UP:VOLUP', 'sendKey KEYCODE_MEDIA_PLAY_PAUSE:PLAYPAUSE,sendKey KEYCODE_ASSIST:SOURCE,sendKey KEYCODE_VOLUME_DOWN:VOLDOWN', 'attr rc_iconpath icons/remotecontrol', 'attr rc_iconprefix black_btn_' ); return @row; } ############################################################################## # Remote control layout for Magenta One ############################################################################## sub RCLayoutMagentaOne () { my @row = ( 'sendKey KEYCODE_POWER:POWEROFF,:blank,sendKey KEYCODE_VOLUME_MUTE:MUTE', ':blank,:blank,:blank', 'sendKey KEYCODE_TV_INPUT_HDMI_1:SEARCH,sendKey KEYCODE_ASSIST:SOURCE,sendKey KEYCODE_GUIDE:GUIDE', 'sendKey KEYCODE_BACK:BACKDroid,sendKey KEYCODE_HOME:HOMEDroid,sendKey KEYCODE_ALL_APPS:MENUDroid', ':blank,sendKey KEYCODE_DPAD_UP:UP,:blank', 'sendKey KEYCODE_DPAD_LEFT:LEFT,sendKey KEYCODE_DPAD_CENTER:OK,sendKey KEYCODE_DPAD_RIGHT:RIGHT', ':blank,sendKey KEYCODE_DPAD_DOWN:DOWN,:blank', 'sendKey KEYCODE_VOLUME_UP:VOLUP,sendKey KEYCODE_MEDIA_PLAY_PAUSE:PLAYPAUSE,sendKey KEYCODE_CHANNEL_UP:CHUP', 'sendKey KEYCODE_VOLUME_DOWN:VOLDOWN,:blank,sendKey KEYCODE_CHANNEL_DOWN:CHDOWN', 'sendKey KEYCODE_1:1,sendKey KEYCODE_2:2,sendKey KEYCODE_3:3', 'sendKey KEYCODE_4:4,sendKey KEYCODE_5:5,sendKey KEYCODE_6:6', 'sendKey KEYCODE_7:7,sendKey KEYCODE_8:8,sendKey KEYCODE_9:9', 'sendKey KEYCODE_INFO:INFO,sendKey KEYCODE_0:0,sendKey KEYCODE_MEDIA_RECORD:REC', 'attr rc_iconpath icons/remotecontrol', 'attr rc_iconprefix black_btn_' ); return @row; } ############################################################################## # Extended remote control layout for Magenta TV ############################################################################## sub RCLayoutMagentaTVExt () { my @row = ( 'sendKey KEYCODE_POWER:POWEROFF,:blank,sendKey KEYCODE_VOLUME_MUTE:MUTE,:blank', ':blank,:blank,:blank,:blank', 'sendKey KEYCODE_TV_INPUT_HDMI_1:SEARCH,sendKey KEYCODE_ASSIST:SOURCE,sendKey KEYCODE_GUIDE:GUIDE,sendKey KEYCODE_TV_INPUT_HDMI_4:TV', 'sendKey KEYCODE_BACK:BACKDroid,sendKey KEYCODE_HOME:HOMEDroid,sendKey KEYCODE_ALL_APPS:MENUDroid,:blank', ':blank,sendKey KEYCODE_DPAD_UP:UP,:blank,:blank', 'sendKey KEYCODE_DPAD_LEFT:LEFT,sendKey KEYCODE_DPAD_CENTER:OK,sendKey KEYCODE_DPAD_RIGHT:RIGHT,:blank', ':blank,sendKey KEYCODE_DPAD_DOWN:DOWN,:blank,:blank', 'sendKey KEYCODE_VOLUME_UP:VOLUP,sendKey KEYCODE_MEDIA_PLAY_PAUSE:PLAYPAUSE,sendKey KEYCODE_CHANNEL_UP:CHUP,:blank', 'sendKey KEYCODE_VOLUME_DOWN:VOLDOWN,:blank,sendKey KEYCODE_CHANNEL_DOWN:CHDOWN,:blank', 'sendKey KEYCODE_1:1,sendKey KEYCODE_2:2,sendKey KEYCODE_3:3,:blank', 'sendKey KEYCODE_4:4,sendKey KEYCODE_5:5,sendKey KEYCODE_6:6,:blank', 'sendKey KEYCODE_7:7,sendKey KEYCODE_8:8,sendKey KEYCODE_9:9,:blank', 'sendKey KEYCODE_INFO:INFO,sendKey KEYCODE_0:0,sendKey KEYCODE_MEDIA_RECORD:REC,:blank', 'sendKey KEYCODE_PROG_RED:RED,sendKey KEYCODE_PROG_GREEN:GREEN,sendKey KEYCODE_PROG_YELLOW:YELLOW,sendKey KEYCODE_PROG_BLUE:BLUE', 'attr rc_iconpath icons/remotecontrol', 'attr rc_iconprefix black_btn_' ); return @row; } 1; =pod =item device =item summary Allows to control an Android device via ADB (Android Debug Bridge) =begin html

AndroidDB

Set

Get

Attributes

=end html =cut