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 }