1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 package com.simpligility.maven.plugins.android.standalonemojos;
17
18 import com.android.ddmlib.AdbCommandRejectedException;
19 import com.android.ddmlib.AndroidDebugBridge;
20 import com.android.ddmlib.CollectingOutputReceiver;
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.AbstractAndroidMojo;
25 import com.simpligility.maven.plugins.android.DeviceCallback;
26 import com.simpligility.maven.plugins.android.common.DeviceHelper;
27 import com.simpligility.maven.plugins.android.config.ConfigHandler;
28 import com.simpligility.maven.plugins.android.config.ConfigPojo;
29 import com.simpligility.maven.plugins.android.config.PullParameter;
30 import com.simpligility.maven.plugins.android.configuration.Run;
31
32 import org.apache.maven.plugin.MojoExecutionException;
33 import org.apache.maven.plugin.MojoFailureException;
34 import org.apache.maven.plugins.annotations.Mojo;
35 import org.apache.maven.plugins.annotations.Parameter;
36 import org.w3c.dom.Document;
37 import org.w3c.dom.NodeList;
38 import org.xml.sax.SAXException;
39
40 import javax.xml.parsers.DocumentBuilder;
41 import javax.xml.parsers.DocumentBuilderFactory;
42 import javax.xml.parsers.ParserConfigurationException;
43 import javax.xml.xpath.XPath;
44 import javax.xml.xpath.XPathConstants;
45 import javax.xml.xpath.XPathExpression;
46 import javax.xml.xpath.XPathExpressionException;
47 import javax.xml.xpath.XPathFactory;
48
49 import java.io.IOException;
50
51 import static com.simpligility.maven.plugins.android.common.AndroidExtension.APK;
52
53 import java.io.BufferedReader;
54 import java.io.StringReader;
55 import java.lang.reflect.InvocationTargetException;
56 import java.lang.reflect.Method;
57 import java.net.InetSocketAddress;
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99 @Mojo( name = "run" )
100 public class RunMojo extends AbstractAndroidMojo
101 {
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120 @Parameter
121 @ConfigPojo
122 private Run run;
123
124
125
126
127
128
129
130 @Parameter( property = "android.run.debug" )
131 protected String runDebug;
132
133
134 @PullParameter( defaultValue = "false" )
135 private String parsedDebug;
136
137
138
139
140
141
142 private static class LauncherInfo
143 {
144 private String packageName;
145
146 private String activity;
147
148 public String getPackageName()
149 {
150 return packageName;
151 }
152
153 public void setPackageName( String packageName )
154 {
155 this.packageName = packageName;
156 }
157
158 public String getActivity()
159 {
160 return activity;
161 }
162
163 public void setActivity( String activity )
164 {
165 this.activity = activity;
166 }
167 }
168
169
170
171
172
173
174
175
176 @Override
177 public void execute() throws MojoExecutionException, MojoFailureException
178 {
179 if ( project.getPackaging().equals( APK ) )
180 {
181 try
182 {
183 LauncherInfo launcherInfo;
184
185 launcherInfo = getLauncherActivity();
186
187 ConfigHandler configHandler = new ConfigHandler( this, this.session, this.execution );
188 configHandler.parseConfiguration();
189
190 launch( launcherInfo );
191 }
192 catch ( Exception ex )
193 {
194 getLog().info( "Unable to run launcher Activity" );
195 getLog().debug( ex );
196 }
197 }
198 else
199 {
200 getLog().info( "Project packaging is not apk, skipping run action." );
201 }
202 }
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218 private LauncherInfo getLauncherActivity()
219 throws ParserConfigurationException, SAXException, IOException, XPathExpressionException,
220 MojoFailureException
221 {
222 Document document;
223 DocumentBuilder documentBuilder;
224 DocumentBuilderFactory documentBuilderFactory;
225 Object result;
226 XPath xPath;
227 XPathExpression xPathExpression;
228 XPathFactory xPathFactory;
229
230
231
232
233 documentBuilderFactory = DocumentBuilderFactory.newInstance();
234
235 documentBuilder = documentBuilderFactory.newDocumentBuilder();
236
237 document = documentBuilder.parse( destinationManifestFile );
238
239 xPathFactory = XPathFactory.newInstance();
240
241 xPath = xPathFactory.newXPath();
242
243 xPathExpression = xPath.compile(
244 "//manifest/application/activity/intent-filter[action[@name=\"android.intent.action.MAIN\"] "
245 + "and category[@name=\"android.intent.category.LAUNCHER\"]]/.." );
246
247
248
249
250 result = xPathExpression.evaluate( document, XPathConstants.NODESET );
251
252 if ( result instanceof NodeList )
253 {
254 NodeList activities;
255
256 activities = ( NodeList ) result;
257
258 if ( activities.getLength() > 0 )
259 {
260
261 LauncherInfo launcherInfo;
262
263 launcherInfo = new LauncherInfo();
264 String activityName = activities.item( 0 ).getAttributes().getNamedItem( "android:name" )
265 .getNodeValue();
266
267 if ( ! activityName.contains( "." ) )
268 {
269 activityName = "." + activityName;
270 }
271
272 if ( activityName.startsWith( "." ) )
273 {
274 String packageName = document.getElementsByTagName( "manifest" ).item( 0 ).getAttributes()
275 .getNamedItem( "package" ).getNodeValue();
276 activityName = packageName + activityName;
277 }
278
279 launcherInfo.activity = activityName;
280
281 launcherInfo.packageName = renameManifestPackage != null
282 ? renameManifestPackage
283 : document.getDocumentElement().getAttribute( "package" ).toString();
284
285 return launcherInfo;
286 }
287 else
288 {
289
290 throw new MojoFailureException( "Could not find a launcher activity in manifest" );
291 }
292 }
293 else
294 {
295
296 throw new MojoFailureException( "Could not find any activity in manifest" );
297 }
298 }
299
300
301
302
303
304
305
306
307 private void launch( final LauncherInfo info ) throws MojoExecutionException, MojoFailureException
308 {
309 final String command;
310
311 final int debugPort = findDebugPort();
312
313 command = String.format( "am start %s-n %s/%s", debugPort >= 0 ? "-D " : "", info.packageName, info.activity );
314
315 doWithDevices( new DeviceCallback()
316 {
317 @Override
318 public void doWithDevice( IDevice device ) throws MojoExecutionException, MojoFailureException
319 {
320 String deviceLogLinePrefix = DeviceHelper.getDeviceLogLinePrefix( device );
321
322 try
323 {
324 getLog().info( deviceLogLinePrefix + "Attempting to start " + info.packageName + "/"
325 + info.activity );
326
327 CollectingOutputReceiver shellOutput = new CollectingOutputReceiver();
328 device.executeShellCommand( command, shellOutput );
329 if ( shellOutput.getOutput().contains( "Error" ) )
330 {
331 throw new MojoFailureException( shellOutput.getOutput() );
332 }
333 if ( debugPort > 0 )
334 {
335 int pid = findPid( device, "ps" );
336 if ( pid == -1 )
337 {
338 pid = findPid( device, "ps -Af" );
339 }
340 if ( pid == -1 )
341 {
342 throw new MojoFailureException( "Cannot find stated process " + info.packageName );
343 }
344 getLog().info(
345 deviceLogLinePrefix + "Process " + debugPort + " launched"
346 );
347 try
348 {
349 createForward( device, debugPort, pid );
350 getLog().info(
351 deviceLogLinePrefix + "Debugger listening on " + debugPort
352 );
353 }
354 catch ( Exception ex )
355 {
356 throw new MojoFailureException(
357 "Cannot create forward tcp: " + debugPort
358 + " jdwp: " + pid, ex
359 );
360 }
361 }
362 }
363 catch ( IOException ex )
364 {
365 throw new MojoFailureException( deviceLogLinePrefix + "Input/Output error", ex );
366 }
367 catch ( TimeoutException ex )
368 {
369 throw new MojoFailureException( deviceLogLinePrefix + "Command timeout", ex );
370 }
371 catch ( AdbCommandRejectedException ex )
372 {
373 throw new MojoFailureException( deviceLogLinePrefix + "ADB rejected the command", ex );
374 }
375 catch ( ShellCommandUnresponsiveException ex )
376 {
377 throw new MojoFailureException( deviceLogLinePrefix + "Unresponsive command", ex );
378 }
379 }
380
381 private int findPid( IDevice device, final String cmd )
382 throws IOException, TimeoutException, AdbCommandRejectedException, ShellCommandUnresponsiveException
383 {
384 CollectingOutputReceiver processOutput = new CollectingOutputReceiver();
385 device.executeShellCommand( cmd, processOutput );
386 BufferedReader r = new BufferedReader( new StringReader( processOutput.getOutput() ) );
387 int pid = -1;
388 for ( ;; )
389 {
390 String line = r.readLine();
391 if ( line == null )
392 {
393 break;
394 }
395 if ( line.endsWith( info.packageName ) )
396 {
397 String[] values = line.split( " +" );
398 if ( values.length > 2 )
399 {
400 pid = Integer.valueOf( values[1] );
401 break;
402 }
403 }
404 }
405 r.close();
406 return pid;
407 }
408 } );
409 }
410
411 private static void createForward( IDevice device, int debugPort, int pid )
412 throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException
413 {
414 Method m = Class.forName( "com.android.ddmlib.AdbHelper" ).
415 getDeclaredMethod(
416 "createForward", InetSocketAddress.class,
417 device.getClass(), String.class, String.class
418 );
419 m.setAccessible( true );
420 m.invoke(
421 null, AndroidDebugBridge.getSocketAddress(), device,
422 String.format( "tcp:%d", debugPort ), String.format( "jdwp:%d", pid )
423 );
424 }
425
426 private int findDebugPort()
427 {
428 int debugPort;
429 if ( "true".equals( parsedDebug ) )
430 {
431 debugPort = 0;
432 }
433 else
434 {
435 try
436 {
437 debugPort = Integer.parseInt( parsedDebug );
438 }
439 catch ( NumberFormatException ex )
440 {
441 debugPort = -1;
442 }
443 }
444 return debugPort;
445 }
446 }