com.jayway.maven.plugins.android.standalonemojos.RunMojo Maven / Gradle / Ivy
/*
* Copyright (C) 2011 Lorenzo Villani
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jayway.maven.plugins.android.standalonemojos;
import java.io.IOException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.NullOutputReceiver;
import com.android.ddmlib.ShellCommandUnresponsiveException;
import com.android.ddmlib.TimeoutException;
import com.jayway.maven.plugins.android.AbstractAndroidMojo;
import com.jayway.maven.plugins.android.DeviceCallback;
import org.apache.maven.plugin.Mojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Runs the first Activity shown in the top-level launcher as determined by its Intent filters.
*
*
* Android provides a component-based architecture, which means that there is no "main" function which serves as an
* entry point to the APK. There's an homogeneous collection of Activity(es), Service(s), Receiver(s), etc.
*
*
*
* The Android top-level launcher (whose purpose is to allow users to launch other applications) uses the Intent
* resolution mechanism to determine which Activity(es) to show to the end user. Such activities are identified by at
* least:
*
* - Action type:
android.intent.action.MAIN
* - Category:
android.intent.category.LAUNCHER
*
*
*
* And are declared in AndroidManifest.xml
as such:
*
*
* <activity android:name=".ExampleActivity">
* <intent-filter>
* <action android:name="android.intent.action.MAIN" />
* <category android:name="android.intent.category.LAUNCHER" />
* </intent-filter>
* </activity>
*
*
* This {@link Mojo} will try to to launch the first activity of this kind found in AndroidManifest.xml
. In
* case multiple activities satisfy the requirements listed above only the first declared one is run. In case there are
* no "Launcher activities" declared in the manifest or no activities declared at all, this goal aborts throwing an
* error.
*
*
* The device parameter is taken into consideration so potentially the Activity found is started on all attached
* devices. The application will NOT be deployed and running will silently fail if the application is not deployed.
*
* @author Lorenzo Villani
* @see "http://developer.android.com/guide/topics/fundamentals.html"
* @see "http://developer.android.com/guide/topics/intents/intents-filters.html"
*
* @goal run
*/
public class RunMojo
extends AbstractAndroidMojo
{
// ----------------------------------------------------------------------
// Mojo types
// ----------------------------------------------------------------------
/**
* Thrown when no "Launcher activities" could be found inside AndroidManifest.xml
*
* @author Lorenzo Villani
*/
private static class ActivityNotFoundException
extends Exception
{
private static final long serialVersionUID = 1L;
/**
* Constructor.
*/
public ActivityNotFoundException()
{
super( "Unable to determine Launcher activity" );
}
}
/**
* Holds information about the "Launcher" activity.
*
* @author Lorenzo Villani
*/
private static class LauncherInfo
{
public String packageName;
public String activity;
}
// ----------------------------------------------------------------------
// Public methods
// ----------------------------------------------------------------------
/**
* {@inheritDoc}
*/
@Override
public void execute()
throws MojoExecutionException, MojoFailureException
{
try
{
LauncherInfo launcherInfo;
launcherInfo = getLauncherActivity();
launch( launcherInfo );
}
catch ( Exception ex )
{
throw new MojoFailureException( "Unable to run launcher Activity", ex );
}
}
// ----------------------------------------------------------------------
// Private methods
// ----------------------------------------------------------------------
/**
* Gets the first "Launcher" Activity by running an XPath query on AndroidManifest.xml
.
*
* @return A {@link LauncherInfo}
* @throws MojoExecutionException
* @throws ParserConfigurationException
* @throws IOException
* @throws SAXException
* @throws XPathExpressionException
* @throws ActivityNotFoundException
*/
private LauncherInfo getLauncherActivity()
throws ParserConfigurationException, SAXException, IOException, XPathExpressionException,
ActivityNotFoundException
{
Document document;
DocumentBuilder documentBuilder;
DocumentBuilderFactory documentBuilderFactory;
Object result;
XPath xPath;
XPathExpression xPathExpression;
XPathFactory xPathFactory;
//
// Setup JAXP stuff
//
documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilder = documentBuilderFactory.newDocumentBuilder();
document = documentBuilder.parse( androidManifestFile );
xPathFactory = XPathFactory.newInstance();
xPath = xPathFactory.newXPath();
xPathExpression =
xPath.compile( "//manifest/application/activity/intent-filter[action[@name=\"android.intent.action.MAIN\"] "
+ "and category[@name=\"android.intent.category.LAUNCHER\"]]/.." );
//
// Run XPath query
//
result = xPathExpression.evaluate( document, XPathConstants.NODESET );
if ( result instanceof NodeList )
{
NodeList activities;
activities = (NodeList) result;
if ( activities.getLength() > 0 )
{
// Grab the first declared Activity
LauncherInfo launcherInfo;
launcherInfo = new LauncherInfo();
launcherInfo.activity =
activities.item( 0 ).getAttributes().getNamedItem( "android:name" ).getNodeValue();
launcherInfo.packageName = document.getDocumentElement().getAttribute( "package" ).toString();
return launcherInfo;
}
else
{
// If we get here, we couldn't find a launcher activity.
throw new ActivityNotFoundException();
}
}
else
{
// If we get here we couldn't find any Activity
throw new ActivityNotFoundException();
}
}
/**
* Executes the "Launcher activity".
*
* @param info A {@link LauncherInfo}.
* @throws MojoFailureException
* @throws MojoExecutionException
*/
private void launch(final LauncherInfo info) throws MojoExecutionException, MojoFailureException
{
final String command;
command = String.format( "am start -n %s/%s", info.packageName, info.activity );
doWithDevices( new DeviceCallback()
{
@Override
public void doWithDevice( IDevice device )
throws MojoExecutionException, MojoFailureException
{
try
{
getLog().info("Attempting to start " + info.packageName + info.activity + " on device "
+ device.getSerialNumber() + " (avdName = " + device.getAvdName() + ")");
device.executeShellCommand( command, new NullOutputReceiver() );
}
catch ( IOException ex )
{
throw new MojoFailureException( "Input/Output error", ex );
}
catch ( TimeoutException ex )
{
throw new MojoFailureException( "Command timeout", ex );
}
catch ( AdbCommandRejectedException ex )
{
throw new MojoFailureException( "ADB rejected the command", ex );
}
catch ( ShellCommandUnresponsiveException ex )
{
throw new MojoFailureException( "Unresponsive command", ex );
}
}
} );
}
}