1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.simpligility.maven.plugins.android;
18
19 import com.android.ddmlib.AdbCommandRejectedException;
20 import com.android.ddmlib.AndroidDebugBridge;
21 import com.android.ddmlib.IDevice;
22 import com.android.ddmlib.ShellCommandUnresponsiveException;
23 import com.android.ddmlib.TimeoutException;
24 import com.simpligility.maven.plugins.android.common.DeviceHelper;
25 import com.simpligility.maven.plugins.android.configuration.Emulator;
26
27 import org.apache.commons.lang3.StringUtils;
28 import org.apache.maven.plugin.MojoExecutionException;
29 import org.apache.maven.plugins.annotations.Parameter;
30
31 import java.io.BufferedReader;
32 import java.io.File;
33 import java.io.FileWriter;
34 import java.io.IOException;
35 import java.io.InputStreamReader;
36 import java.io.PrintWriter;
37 import java.net.Socket;
38 import java.util.Arrays;
39 import java.util.List;
40 import java.util.Locale;
41 import java.util.concurrent.Callable;
42 import java.util.concurrent.ExecutorService;
43 import java.util.concurrent.Executors;
44 import java.util.concurrent.Future;
45
46
47
48
49
50
51
52
53
54
55
56
57
58 public abstract class AbstractEmulatorMojo extends AbstractAndroidMojo
59 {
60
61
62
63 public static final String OS_NAME = System.getProperty( "os.name" ).toLowerCase( Locale.US );
64 private static final int MILLIS_TO_SLEEP_BETWEEN_DEVICE_ONLINE_CHECKS = 200;
65
66
67
68
69
70 private static final int MILLIS_TO_SLEEP_BETWEEN_SYS_BOOTED_CHECKS = 5000;
71
72
73
74
75 private static final String[] BOOT_INDICATOR_PROP_NAMES
76 = { "dev.bootcomplete", "sys.boot_completed", "init.svc.bootanim" };
77
78
79
80
81 private static final String[] BOOT_INDICATOR_PROP_TARGET_VALUES = { "1", "1", "stopped" };
82
83
84
85
86
87
88
89 private static final boolean[] BOOT_INDICATOR_PROP_WAIT_FOR = { false, false, true };
90
91
92
93
94
95 private static final long START_TIMEOUT_REMAINING_TIME_WARNING_THRESHOLD = 5000;
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112 @Parameter
113 private Emulator emulator;
114
115
116
117
118
119
120 @Parameter( property = "android.emulator.avd" )
121 private String emulatorAvd;
122
123
124
125
126 @Parameter( property = "android.emulatorUnlock", defaultValue = "false" )
127 private boolean emulatorUnlock;
128
129
130
131
132
133
134 @Parameter( property = "android.emulator.wait" )
135 private String emulatorWait;
136
137
138
139
140
141
142
143
144 @Parameter( property = "android.emulator.options" )
145 private String emulatorOptions;
146
147
148
149
150
151
152 @Parameter( property = "android.emulator.executable" )
153 private String emulatorExecutable;
154
155
156
157
158 @Parameter( property = "android.emulator.location" )
159 private String emulatorLocation;
160
161
162
163
164 private String parsedAvd;
165
166
167
168
169 private String parsedOptions;
170
171
172
173
174 private String parsedWait;
175
176 private String parsedExecutable;
177
178
179
180
181 private String parsedEmulatorLocation;
182
183 private static final String START_EMULATOR_MSG = "Starting android emulator with script: ";
184 private static final String START_EMULATOR_WAIT_MSG = "Waiting for emulator start:";
185
186
187
188
189 private static final String SCRIPT_FOLDER = System.getProperty( "java.io.tmpdir" );
190
191
192
193
194
195
196 private boolean isWindows()
197 {
198 boolean result;
199 if ( OS_NAME.toLowerCase().contains( "windows" ) )
200 {
201 result = true;
202 }
203 else
204 {
205 result = false;
206 }
207 getLog().debug( "isWindows: " + result );
208 return result;
209 }
210
211
212
213
214
215
216
217
218
219
220 protected void startAndroidEmulator() throws MojoExecutionException
221 {
222 parseParameters();
223
224 CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
225 executor.setLogger( this.getLog() );
226
227 try
228 {
229 String filename;
230 if ( isWindows() )
231 {
232 filename = writeEmulatorStartScriptWindows();
233 }
234 else
235 {
236 filename = writeEmulatorStartScriptUnix();
237 }
238
239 final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge();
240 if ( androidDebugBridge.isConnected() )
241 {
242 waitForInitialDeviceList( androidDebugBridge );
243 List<IDevice> devices = Arrays.asList( androidDebugBridge.getDevices() );
244 int numberOfDevices = devices.size();
245 getLog().info( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge" );
246
247 IDevice existingEmulator = findExistingEmulator( devices );
248 if ( existingEmulator == null )
249 {
250 getLog().info( START_EMULATOR_MSG + filename );
251 executor.executeCommand( filename, null );
252
253 getLog().info( START_EMULATOR_WAIT_MSG + parsedWait );
254
255 boolean booted = waitUntilDeviceIsBootedOrTimeout( androidDebugBridge );
256 if ( booted )
257 {
258 getLog().info( "Emulator is up and running." );
259 unlockEmulator( androidDebugBridge );
260 }
261 else
262 {
263 throw new MojoExecutionException( "Timeout while waiting for emulator to startup." );
264 }
265
266 }
267 else
268 {
269 getLog().info( String.format(
270 "Emulator already running [Serial No: '%s', AVD Name '%s']. " + "Skipping start and wait.",
271 existingEmulator.getSerialNumber(), existingEmulator.getAvdName() ) );
272 }
273 }
274 }
275 catch ( Exception e )
276 {
277 throw new MojoExecutionException( "", e );
278 }
279 }
280
281
282
283
284
285 void unlockEmulator( AndroidDebugBridge androidDebugBridge )
286 {
287 if ( emulatorUnlock )
288 {
289 IDevice myEmulator = findExistingEmulator( Arrays.asList( androidDebugBridge.getDevices() ) );
290 int devicePort = extractPortFromDevice( myEmulator );
291 if ( devicePort == -1 )
292 {
293 getLog().info( "Unable to retrieve port to unlock emulator "
294 + DeviceHelper.getDescriptiveName( myEmulator ) );
295 }
296 else
297 {
298 getLog().info( "Unlocking emulator "
299 + DeviceHelper.getDescriptiveName( myEmulator ) );
300
301 sendEmulatorCommand( devicePort,
302 "event send EV_KEY:KEY_SOFT1:1" );
303 sendEmulatorCommand( devicePort,
304 "event send EV_KEY:KEY_SOFT1:0" );
305 sendEmulatorCommand( devicePort,
306 "event send EV_KEY:KEY_SOFT1:1" );
307 sendEmulatorCommand( devicePort,
308 "event send EV_KEY:KEY_SOFT1:0" );
309 }
310 }
311 }
312
313
314
315 boolean waitUntilDeviceIsBootedOrTimeout( AndroidDebugBridge androidDebugBridge )
316 throws MojoExecutionException
317 {
318 final long timeout = System.currentTimeMillis() + Long.parseLong( parsedWait );
319 IDevice myEmulator;
320 boolean devOnline;
321 boolean sysBootCompleted = false;
322 long remainingTime = 0;
323
324
325 boolean waitingForConnection = false;
326 do
327 {
328 myEmulator = findExistingEmulator( Arrays.asList( androidDebugBridge.getDevices() ) );
329 devOnline = ( myEmulator != null ) && ( myEmulator.isOnline() );
330 if ( devOnline )
331 {
332 break;
333 }
334 else
335 {
336 myEmulator = null;
337 }
338
339 if ( !waitingForConnection )
340 {
341 waitingForConnection = true;
342 getLog().info( "Waiting for the device to go online..." );
343 }
344 try
345 {
346 Thread.sleep( MILLIS_TO_SLEEP_BETWEEN_DEVICE_ONLINE_CHECKS );
347 }
348 catch ( InterruptedException e )
349 {
350 throw new MojoExecutionException( "Interrupted waiting for device to become ready" );
351 }
352
353 remainingTime = timeout - System.currentTimeMillis();
354 } while ( remainingTime > 0 );
355
356 if ( devOnline )
357 {
358 boolean waitingForBootCompleted = false;
359 final String[] bootIndicatorPropValues = new String[ BOOT_INDICATOR_PROP_NAMES.length ];
360 boolean anyTargetStateReached = false;
361 boolean requiredTargetStatesReached = false;
362
363
364 do
365 {
366 try
367 {
368
369 anyTargetStateReached = false;
370 requiredTargetStatesReached = true;
371
372 for ( int indicatorProp = 0; indicatorProp < BOOT_INDICATOR_PROP_NAMES.length; ++indicatorProp )
373 {
374
375 boolean targetStateReached =
376 (
377 bootIndicatorPropValues[indicatorProp] != null
378 && bootIndicatorPropValues[indicatorProp]
379 .equals( BOOT_INDICATOR_PROP_TARGET_VALUES[indicatorProp] )
380 );
381 if ( !targetStateReached )
382 {
383
384 bootIndicatorPropValues[indicatorProp] =
385 myEmulator.getPropertySync( BOOT_INDICATOR_PROP_NAMES[indicatorProp] );
386 targetStateReached =
387 (
388 bootIndicatorPropValues[indicatorProp] != null
389 && bootIndicatorPropValues[indicatorProp]
390 .equals( BOOT_INDICATOR_PROP_TARGET_VALUES[indicatorProp] )
391 );
392 }
393 anyTargetStateReached |= targetStateReached;
394 requiredTargetStatesReached &=
395 BOOT_INDICATOR_PROP_WAIT_FOR[indicatorProp] ? targetStateReached : true;
396
397 getLog().debug( BOOT_INDICATOR_PROP_NAMES[indicatorProp]
398 + " : " + bootIndicatorPropValues[indicatorProp]
399 + ( targetStateReached ? " == " : " != " )
400 + BOOT_INDICATOR_PROP_TARGET_VALUES[indicatorProp]
401 + " [" + ( targetStateReached ? "OK" : "PENDING" ) + ']'
402 );
403 }
404 }
405 catch ( TimeoutException e )
406 {
407
408
409 }
410 catch ( AdbCommandRejectedException e )
411 {
412
413
414 }
415 catch ( ShellCommandUnresponsiveException e )
416 {
417
418
419 }
420 catch ( IOException e )
421 {
422 throw new MojoExecutionException( "IO error during status request" , e );
423 }
424
425 remainingTime = timeout - System.currentTimeMillis();
426
427 if ( remainingTime > 0 )
428 {
429
430 sysBootCompleted = requiredTargetStatesReached;
431 }
432 else
433 {
434
435 sysBootCompleted = anyTargetStateReached;
436 }
437
438 if ( remainingTime > 0 && !sysBootCompleted )
439 {
440 if ( !waitingForBootCompleted )
441 {
442 waitingForBootCompleted = true;
443 getLog().info( "Waiting for the device to finish booting..." );
444 }
445
446 try
447 {
448 Thread.sleep( MILLIS_TO_SLEEP_BETWEEN_SYS_BOOTED_CHECKS );
449 }
450 catch ( InterruptedException e )
451 {
452 throw new MojoExecutionException(
453 "Interrupted while waiting for the device to finish booting" );
454 }
455 }
456 } while ( !sysBootCompleted && remainingTime > 0 );
457 if ( sysBootCompleted && remainingTime < START_TIMEOUT_REMAINING_TIME_WARNING_THRESHOLD )
458 {
459 getLog().warn(
460 "Boot indicators have been signalled, but remaining time was " + remainingTime + " ms" );
461 }
462 }
463 return sysBootCompleted;
464 }
465
466 private IDevice findExistingEmulator( List<IDevice> devices )
467 {
468 IDevice existingEmulator = null;
469
470 for ( IDevice device : devices )
471 {
472 if ( device.isEmulator() )
473 {
474 if ( isExistingEmulator( device ) )
475 {
476 existingEmulator = device;
477 break;
478 }
479 }
480 }
481 return existingEmulator;
482 }
483
484
485
486
487
488
489
490
491
492 private boolean isExistingEmulator( IDevice device )
493 {
494 return ( ( device.getAvdName() != null ) && ( device.getAvdName().equalsIgnoreCase( parsedAvd ) ) );
495 }
496
497
498
499
500
501
502
503
504 private String writeEmulatorStartScriptWindows() throws MojoExecutionException
505 {
506
507 String filename = SCRIPT_FOLDER + "\\android-maven-plugin-emulator-start.vbs";
508
509 File file = new File( filename );
510 PrintWriter writer = null;
511 try
512 {
513 writer = new PrintWriter( new FileWriter( file ) );
514
515
516
517
518 String command = assembleStartCommandLine();
519 String uniqueWindowTitle = "AndroidMavenPlugin-AVD" + parsedAvd;
520 writer.println( "Dim oShell" );
521 writer.println( "Set oShell = WScript.CreateObject(\"WScript.shell\")" );
522 String cmdPath = System.getenv( "COMSPEC" );
523 if ( cmdPath == null )
524 {
525 cmdPath = "cmd.exe";
526 }
527 String cmd = cmdPath + " /X /C START /SEPARATE \"\"" + uniqueWindowTitle + "\"\" " + command.trim();
528 writer.println( "oShell.run \"" + cmd + "\"" );
529 }
530 catch ( IOException e )
531 {
532 getLog().error( "Failure writing file " + filename );
533 }
534 finally
535 {
536 if ( writer != null )
537 {
538 writer.flush();
539 writer.close();
540 }
541 }
542 file.setExecutable( true );
543 return filename;
544 }
545
546
547
548
549
550
551
552
553 private String writeEmulatorStartScriptUnix() throws MojoExecutionException
554 {
555 String filename = SCRIPT_FOLDER + "/android-maven-plugin-emulator-start.sh";
556
557 File sh;
558 sh = new File( "/bin/bash" );
559 if ( !sh.exists() )
560 {
561 sh = new File( "/usr/bin/bash" );
562 }
563 if ( !sh.exists() )
564 {
565 sh = new File( "/bin/sh" );
566 }
567
568 File file = new File( filename );
569 PrintWriter writer = null;
570 try
571 {
572 writer = new PrintWriter( new FileWriter( file ) );
573 writer.println( "#!" + sh.getAbsolutePath() );
574 writer.print( assembleStartCommandLine() );
575 writer.print( " 1>/dev/null 2>&1 &" );
576 }
577 catch ( IOException e )
578 {
579 getLog().error( "Failure writing file " + filename );
580 }
581 finally
582 {
583 if ( writer != null )
584 {
585 writer.flush();
586 writer.close();
587 }
588 }
589 file.setExecutable( true );
590 return filename;
591 }
592
593
594
595
596
597
598
599 protected void stopAndroidEmulator() throws MojoExecutionException
600 {
601 parseParameters();
602
603 final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge();
604 if ( androidDebugBridge.isConnected() )
605 {
606 List<IDevice> devices = Arrays.asList( androidDebugBridge.getDevices() );
607 int numberOfDevices = devices.size();
608 getLog().info( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge" );
609
610 for ( IDevice device : devices )
611 {
612 if ( device.isEmulator() )
613 {
614 if ( isExistingEmulator( device ) )
615 {
616 stopEmulator( device );
617 }
618 }
619 else
620 {
621 getLog().info( "Skipping stop. Not an emulator. " + DeviceHelper.getDescriptiveName( device ) );
622 }
623 }
624 }
625 }
626
627
628
629
630
631
632
633 protected void stopAndroidEmulators() throws MojoExecutionException
634 {
635 final AndroidDebugBridge androidDebugBridge = initAndroidDebugBridge();
636 if ( androidDebugBridge.isConnected() )
637 {
638 List<IDevice> devices = Arrays.asList( androidDebugBridge.getDevices() );
639 int numberOfDevices = devices.size();
640 getLog().info( "Found " + numberOfDevices + " devices connected with the Android Debug Bridge" );
641
642 for ( IDevice device : devices )
643 {
644 if ( device.isEmulator() )
645 {
646 stopEmulator( device );
647 }
648 else
649 {
650 getLog().info( "Skipping stop. Not an emulator. " + DeviceHelper.getDescriptiveName( device ) );
651 }
652 }
653 }
654 }
655
656
657
658
659
660
661 private void stopEmulator( IDevice device )
662 {
663 int devicePort = extractPortFromDevice( device );
664 if ( devicePort == -1 )
665 {
666 getLog().info( "Unable to retrieve port to stop emulator " + DeviceHelper.getDescriptiveName( device ) );
667 }
668 else
669 {
670 getLog().info( "Stopping emulator " + DeviceHelper.getDescriptiveName( device ) );
671
672 sendEmulatorCommand( devicePort, "avd stop" );
673 boolean killed = sendEmulatorCommand( devicePort, "kill" );
674 if ( !killed )
675 {
676 getLog().info( "Emulator failed to stop " + DeviceHelper.getDescriptiveName( device ) );
677 }
678 else
679 {
680 getLog().info( "Emulator stopped successfully " + DeviceHelper.getDescriptiveName( device ) );
681 }
682 }
683 }
684
685
686
687
688
689
690
691
692
693 private int extractPortFromDevice( IDevice device )
694 {
695 String portStr = StringUtils.substringAfterLast( device.getSerialNumber(), "-" );
696 if ( StringUtils.isNotBlank( portStr ) && StringUtils.isNumeric( portStr ) )
697 {
698 return Integer.parseInt( portStr );
699 }
700
701
702 return -1;
703 }
704
705
706
707
708
709
710
711
712 private boolean sendEmulatorCommand(
713
714
715 final int port, final String command )
716 {
717 Callable<Boolean> task = new Callable<Boolean>()
718 {
719 public Boolean call() throws IOException
720 {
721 Socket socket = null;
722 BufferedReader in = null;
723 PrintWriter out = null;
724 try
725 {
726 socket = new Socket( "127.0.0.1", port );
727 out = new PrintWriter( socket.getOutputStream(), true );
728 in = new BufferedReader( new InputStreamReader( socket.getInputStream() ) );
729 if ( in.readLine() == null )
730 {
731 return false;
732 }
733
734 out.write( command );
735 out.write( "\r\n" );
736 }
737 finally
738 {
739 try
740 {
741 out.close();
742 in.close();
743 socket.close();
744 }
745 catch ( Exception e )
746 {
747
748 }
749 }
750
751 return true;
752 }
753
754 private static final long serialVersionUID = 1L;
755 };
756
757 boolean result = false;
758 try
759 {
760 ExecutorService executor = Executors.newSingleThreadExecutor();
761 Future<Boolean> future = executor.submit( task );
762 result = future.get();
763 }
764 catch ( Exception e )
765 {
766 getLog().error( String.format( "Failed to execute emulator command '%s': %s", command, e ) );
767 }
768
769 return result;
770 }
771
772
773
774
775
776
777
778
779
780
781 private String assembleStartCommandLine() throws MojoExecutionException
782 {
783 String emulatorPath;
784 if ( !"SdkTools".equals( parsedEmulatorLocation ) )
785 {
786 emulatorPath = new File( parsedEmulatorLocation, parsedExecutable ).getAbsolutePath();
787 }
788 else
789 {
790 emulatorPath = new File( getAndroidSdk().getToolsPath(), parsedExecutable ).getAbsolutePath();
791 }
792 StringBuilder startCommandline = new StringBuilder( "\"\"" ).append( emulatorPath ).append( "\"\"" )
793 .append( " -avd " ).append( parsedAvd ).append( " " );
794 if ( !StringUtils.isEmpty( parsedOptions ) )
795 {
796 startCommandline.append( parsedOptions );
797 }
798 getLog().info( "Android emulator command: " + startCommandline );
799 return startCommandline.toString();
800 }
801
802 private void parseParameters()
803 {
804
805 if ( emulator != null )
806 {
807
808 if ( emulator.getAvd() != null )
809 {
810 parsedAvd = emulator.getAvd();
811 }
812 else
813 {
814 parsedAvd = determineAvd();
815 }
816
817 if ( emulator.getOptions() != null )
818 {
819 parsedOptions = emulator.getOptions();
820 }
821 else
822 {
823 parsedOptions = determineOptions();
824 }
825
826 if ( emulator.getWait() != null )
827 {
828 parsedWait = emulator.getWait();
829 }
830 else
831 {
832 parsedWait = determineWait();
833 }
834
835 if ( emulator.getExecutable() != null )
836 {
837 parsedExecutable = emulator.getExecutable();
838 }
839 else
840 {
841 parsedExecutable = determineExecutable();
842 }
843
844 if ( emulator.getLocation() != null )
845 {
846 parsedEmulatorLocation = emulator.getLocation();
847 }
848 else
849 {
850 parsedEmulatorLocation = determineEmulatorLocation();
851 }
852 }
853
854 else
855 {
856 parsedAvd = determineAvd();
857 parsedOptions = determineOptions();
858 parsedWait = determineWait();
859 parsedExecutable = determineExecutable();
860 parsedEmulatorLocation = determineEmulatorLocation();
861 }
862 }
863
864
865
866
867
868
869 private String determineExecutable()
870 {
871 String emulator;
872 if ( emulatorExecutable != null )
873 {
874 emulator = emulatorExecutable;
875 }
876 else
877 {
878 emulator = "emulator";
879 }
880 return emulator;
881 }
882
883
884
885
886
887
888 String determineWait()
889 {
890 String wait;
891 if ( emulatorWait != null )
892 {
893 wait = emulatorWait;
894 }
895 else
896 {
897 wait = "5000";
898 }
899 return wait;
900 }
901
902
903
904
905
906
907 private String determineOptions()
908 {
909 String options;
910 if ( emulatorOptions != null )
911 {
912 options = emulatorOptions;
913 }
914 else
915 {
916 options = "";
917 }
918 return options;
919 }
920
921
922
923
924
925
926 String determineAvd()
927 {
928 String avd;
929 if ( emulatorAvd != null )
930 {
931 avd = emulatorAvd;
932 }
933 else
934 {
935 avd = "Default";
936 }
937 return avd;
938 }
939
940
941
942
943
944
945 String determineEmulatorLocation()
946 {
947 String location;
948 if ( emulatorLocation != null )
949 {
950 location = emulatorLocation;
951 }
952 else
953 {
954 location = "SdkTools";
955 }
956 return location;
957 }
958
959 }