Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Issue 97 #89

Merged
merged 10 commits into from

2 participants

Pappy STĂNESCU Manfred Moser
Pappy STĂNESCU

Here is an workaround addressing the problem with missing resources in META-INF.

As already stated, the problem is not in android-maven-plugin itself but in com.android.sdklib.build.ApkBuilder which filters any path starting with META-INF .

This patch copies the META-INF based resources from dependencies into the APK as specified in the plugin configuration by ${android.metaIncludes}.

There are still issues with the obfuscated code because proguard is executed in an earlier stage and any resource supposed to be modified by proguard will remain unchanged

pom.xml
@@ -19,14 +19,14 @@
<modelVersion>4.0.0</modelVersion>
Manfred Moser Owner
mosabua added a note

Please update your pull request to not include these pom changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...way/maven/plugins/android/phase09package/ApkMojo.java
@@ -368,8 +482,9 @@ public boolean accept(File dir, String name) {
}
private File removeDuplicatesFromJar(File in, List<String> duplicates) {
- File target = new File(project.getBasedir(), "target");
- File tmp = new File(target, "unpacked-embedded-jars");
+// File target = new File(project.getBasedir(), "target");
+ String target = project.getBuild().getOutputDirectory();
+ File tmp = new File(target, "unpacked-wembedded-jars");
Manfred Moser Owner
mosabua added a note

is this a typo?

yes, it was a typo

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...way/maven/plugins/android/phase09package/ApkMojo.java
((24 lines not shown))
+ /**
+ * <p>
+ * Pattern for additional META-INF resources to be packaged into the apk.
+ * </p>
+ * <p>
+ * The APK builder filters these resources and doesn't include them into the apk.
+ * </p>
+ * <p>
+ * This leads to bad behaviour of dependent libraries relying on these resources, for instance service discovery
+ * doesn't work.
+ * </p>
+ * <p>
+ * See also <a href="http://code.google.com/p/maven-android-plugin/issues/detail?id=97">Issue 97</a>
+ * </p>
+ *
+ * @parameter expression="${android.metaIncludes}" default-value=""
Manfred Moser Owner
mosabua added a note

This should be updated to follow the config parameter convention as documented in code conventions on the wiki with an Apk config class probably and an expression of android.apk.metaIncludes

I'm aware of that convention, but I followed the convention used by other configuration parameters used by this mojo, see for instance extractDuplicates.

Creating a Dex configuration should cover the rest of the Apk configurations as well - whichever is applicable

Manfred Moser Owner
mosabua added a note

There is a dex one already... I would have to look. In general though the old "convention" is only there because we have not gotten around to completely clean it up yet. So any new parameter should follow the new approach..

I meant an Apk configuration

I added it with the last commit and also included two "old" configurations plus a deprecation mechanism

Manfred Moser Owner
mosabua added a note

Thats great. I hope to test this out a bit more soon and merge it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...way/maven/plugins/android/phase09package/ApkMojo.java
@@ -255,8 +256,121 @@ void createApkFile(File outputFile, boolean signWithDebugKeyStore) throws MojoEx
doAPKWithCommand(outputFile, dexFile, zipArchive, sourceFolders, jarFiles,
nativeFolders, signWithDebugKeyStore);
}
+
+// ISSUE-97
Manfred Moser Owner
mosabua added a note

remove these comments

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
...way/maven/plugins/android/phase09package/ApkMojo.java
((19 lines not shown))
}
+// ISSUE-97
+// vvv
Manfred Moser Owner
mosabua added a note

remove this comment please

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Manfred Moser
Owner

So sorry it has taken so long to update. I just got the proguard pull request and the related config work on the proguard, run, pull and push mojos done. There is still lots outstanding ... could you refactor your approach here to use the new ConfigHandler that is now in master?

Manfred Moser
Owner

Are you going to refactor this as requested to be based off the latest master so we can pull this in?

Manfred Moser
Owner

Ping?

Pappy STĂNESCU

Pong!

Back to this, been busy last weeks and I'm glad to see my ConfigHelper has been improved

This patch is far behind the original master (3.2.x)... How should I proceed with?

Manfred Moser
Owner

Maybe pull down master to your machine. Create a new branch off master and apply a patch with the required changes to it. Once you got it tested create a new pull request. And we will just close this one without merging.

pa314159 added some commits
Pappy STĂNESCU pa314159 Merge branch 'master' into issue-97 69e347f
Pappy STĂNESCU pa314159 @ConfigPojo improvement
added ability to specify the parsed parameter prefix in the @ConfigPojo
annotation
87a2d05
Pappy STĂNESCU

Done, I managed to merge changes from the original master, so we can keep this pull request as is...

You may also have a look at the "modified" branch of this fork, which completely solves the issues with META-INF inclusions. Fill free to get anything you want from there.

Pappy STĂNESCU pa314159 Updated Apk with the new ConfigHandler
Previous code used the ConfigHelper which was able to handle
configuration parameters deprecation.

Since the new ConfigHandler doesn't provide this mechanism - or I could
not find it - only the new "metaIncludes" parameter follows the POJO
configuration rules.

Other parameters are kept as they are and should be modified by the actual
maintainer of this class.
8c86102
Manfred Moser
Owner

Looks good. Pulling in.

Manfred Moser mosabua merged commit 5198b8c into from
Manfred Moser
Owner

Btw. can you reveal your name and website or so for the changelog?

Manfred Moser
Owner

Oh and in terms of further enhancement please create a new pull request and we can discuss there..

Pappy STĂNESCU
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Commits on Jan 8, 2012
  1. Pappy STĂNESCU

    Changed the artefact version and pointed the repository location to m…

    pa314159 authored
    …y own nexus... DO NOT merge this change!
  2. Pappy STĂNESCU

    Add resources specified by ${android.metaIncludes} to the final APK,

    pa314159 authored
    right after apkbuilder invocation
    
    TODO: must investigate proguard invocation as well
  3. Pappy STĂNESCU
Commits on Jan 17, 2012
  1. Pappy STĂNESCU
  2. Pappy STĂNESCU
Commits on Jan 18, 2012
  1. Pappy STĂNESCU

    - created APK configuration for metaIncludes, extractDuplicates and s…

    pa314159 authored
    …ourceDirectories
    
    - added deprecation mechanism for extractDuplicates and sourceDirectories
    - added ConfigHelper to parse mojo configurations + two simple tests
Commits on Jan 24, 2012
  1. Pappy STĂNESCU
Commits on May 23, 2012
  1. Pappy STĂNESCU
  2. Pappy STĂNESCU

    @ConfigPojo improvement

    pa314159 authored
    added ability to specify the parsed parameter prefix in the @ConfigPojo
    annotation
  3. Pappy STĂNESCU

    Updated Apk with the new ConfigHandler

    pa314159 authored
    Previous code used the ConfigHelper which was able to handle
    configuration parameters deprecation.
    
    Since the new ConfigHandler doesn't provide this mechanism - or I could
    not find it - only the new "metaIncludes" parameter follows the POJO
    configuration rules.
    
    Other parameters are kept as they are and should be modified by the actual
    maintainer of this class.
This page is out of date. Refresh to see the latest.
6 src/main/java/com/jayway/maven/plugins/android/config/ConfigHandler.java
View
@@ -22,8 +22,7 @@
private Object mojo;
private Object configPojoInstance;
private String configPojoName;
-
- private static final String PARSED_PARAMETER_PREFIX = "parsed";
+ private String configPojoPrefix;
public ConfigHandler(Object mojo) {
this.mojo = mojo;
@@ -152,7 +151,7 @@ private String toFirstLetterUppercase(String s) {
}
private String getFieldNameWithoutParsedPrefix(Field field) {
- return getFieldNameWithoutPrefix(field, PARSED_PARAMETER_PREFIX);
+ return getFieldNameWithoutPrefix(field, configPojoPrefix);
}
private void initConfigPojo() {
@@ -160,6 +159,7 @@ private void initConfigPojo() {
Field configPojo = findPropertiesByAnnotation(ConfigPojo.class).iterator().next();
configPojoName = configPojo.getName();
configPojoInstance = configPojo.get(mojo);
+ configPojoPrefix = configPojo.getAnnotation( ConfigPojo.class ).prefix();
}
catch (Exception e) {
// ignore, we can live without a config pojo
1  src/main/java/com/jayway/maven/plugins/android/config/ConfigPojo.java
View
@@ -17,4 +17,5 @@
@Retention(RetentionPolicy.RUNTIME)
public @interface ConfigPojo {
+ String prefix() default "parsed";
}
21 src/main/java/com/jayway/maven/plugins/android/configuration/Apk.java
View
@@ -0,0 +1,21 @@
+
+package com.jayway.maven.plugins.android.configuration;
+
+import java.io.File;
+
+import com.jayway.maven.plugins.android.phase09package.ApkMojo;
+
+/**
+ * Embedded configuration of {@link com.jayway.maven.plugins.android.phase09package.ApkMojo}.
+ *
+ * @author <a href="mailto:pa314159&#64;gmail.com">Pappy Răzvan STĂNESCU &lt;pa314159&#64;gmail.com&gt;</a>
+ */
+@SuppressWarnings( "unused" )
+public class Apk
+{
+
+ /**
+ * Mirror of {@link com.jayway.maven.plugins.android.phase09package.ApkMojo#apkMetaIncludes}.
+ */
+ private String[] metaIncludes;
+}
89 src/main/java/com/jayway/maven/plugins/android/configuration/ConfigHelper.java
View
@@ -0,0 +1,89 @@
+
+package com.jayway.maven.plugins.android.configuration;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.Field;
+
+import org.apache.maven.plugin.AbstractMojo;
+import org.apache.maven.plugin.MojoExecutionException;
+
+/**
+ * Helper for parsing the embedded configuration of a mojo.
+ *
+ * @author <a href="mailto:pa314159&#64;gmail.com">Pappy Răzvan STĂNESCU &lt;pa314159&#64;gmail.com&gt;</a>
+ */
+public final class ConfigHelper
+{
+
+ static public void copyValues( AbstractMojo mojo, String confFieldName )
+ throws MojoExecutionException
+ {
+ try {
+ final Class<? extends AbstractMojo> mojoClass = mojo.getClass();
+ final Field confField = mojoClass.getDeclaredField( confFieldName );
+
+ confField.setAccessible( true );
+
+ final Object conf = confField.get( mojo );
+
+ if( conf == null ) {
+ return;
+ }
+
+ for( final Field field : conf.getClass().getDeclaredFields() ) {
+ field.setAccessible( true );
+
+ final Object value = field.get( conf );
+
+ if( value == null ) {
+ continue;
+ }
+
+ final Class<?> cls = value.getClass();
+
+ if( (cls == String.class) && (((String) value).length() == 0) ) {
+ continue;
+ }
+ if( cls.isArray() && (Array.getLength( value ) == 0) ) {
+ continue;
+ }
+
+ {
+ String mojoFieldName = field.getName();
+
+ mojoFieldName = Character.toUpperCase( mojoFieldName.charAt( 0 ) ) + mojoFieldName.substring( 1 );
+ mojoFieldName = confFieldName + mojoFieldName;
+
+ try {
+ final Field mojoField = mojoClass.getDeclaredField( mojoFieldName );
+
+ mojoField.setAccessible( true );
+ mojoField.set( mojo, value );
+ }
+ catch( final NoSuchFieldException e ) {
+ ;
+ }
+ }
+
+ // handle deprecated parameters
+ {
+ try {
+ final Field mojoField = mojoClass.getDeclaredField( field.getName() );
+
+ mojoField.setAccessible( true );
+ mojoField.set( mojo, value );
+ }
+ catch( final NoSuchFieldException e ) {
+ ;
+ }
+ catch( final IllegalArgumentException e ) {
+ // probably not a deprecated parameter, see Proguard configuration;
+ }
+ }
+ }
+ }
+ catch( final Exception e ) {
+ throw new MojoExecutionException( e.getMessage(), e );
+ }
+ }
+}
146 src/main/java/com/jayway/maven/plugins/android/phase09package/ApkMojo.java
View
@@ -34,21 +34,26 @@
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
-import com.jayway.maven.plugins.android.common.AetherHelper;
-import com.jayway.maven.plugins.android.common.NativeHelper;
-import com.jayway.maven.plugins.android.configuration.Sign;
import org.apache.commons.lang.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.factory.ArtifactFactory;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.plexus.util.AbstractScanner;
+import org.codehaus.plexus.util.DirectoryScanner;
+import org.codehaus.plexus.util.SelectorUtils;
import com.jayway.maven.plugins.android.AbstractAndroidMojo;
import com.jayway.maven.plugins.android.AndroidSigner;
import com.jayway.maven.plugins.android.CommandExecutor;
import com.jayway.maven.plugins.android.ExecutionException;
-import org.codehaus.plexus.util.DirectoryScanner;
+import com.jayway.maven.plugins.android.common.NativeHelper;
+import com.jayway.maven.plugins.android.config.ConfigHandler;
+import com.jayway.maven.plugins.android.config.ConfigPojo;
+import com.jayway.maven.plugins.android.config.PullParameter;
+import com.jayway.maven.plugins.android.configuration.Apk;
+import com.jayway.maven.plugins.android.configuration.ConfigHelper;
+import com.jayway.maven.plugins.android.configuration.Sign;
/**
@@ -182,6 +187,47 @@
*/
protected ArtifactFactory artifactFactory;
+ /**
+ * Pattern for additional META-INF resources to be packaged into the apk.
+ * <p>
+ * The APK builder filters these resources and doesn't include them into the apk.
+ * This leads to bad behaviour of dependent libraries relying on these resources,
+ * for instance service discovery doesn't work.<br/>
+ * By specifying this pattern, the android plugin adds these resources to the final apk.
+ * </p>
+ * <p>The pattern is relative to META-INF, i.e. one must use
+ * <pre>
+ * <code>
+ * &lt;apkMetaIncludes&gt;
+ * &lt;metaInclude>services/**&lt;/metaInclude&gt;
+ * &lt;/apkMetaIncludes&gt;
+ * </code>
+ * </pre>
+ * ... instead of
+ * <pre>
+ * <code>
+ * &lt;apkMetaIncludes&gt;
+ * &lt;metaInclude>META-INF/services/**&lt;/metaInclude&gt;
+ * &lt;/apkMetaIncludes&gt;
+ * </code>
+ * </pre>
+ * <p>
+ * See also <a href="http://code.google.com/p/maven-android-plugin/issues/detail?id=97">Issue 97</a>
+ * </p>
+ *
+ * @parameter expression="${android.apk.metaIncludes}" default-value=""
+ */
+ @PullParameter( defaultValueGetterMethod = "getDefaultMetaIncludes" )
+ private String[] apkMetaIncludes;
+
+ /**
+ * Embedded configuration of this mojo.
+ *
+ * @parameter
+ */
+ @ConfigPojo( prefix="apk" )
+ private Apk apk;
+
private static final Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$", 2);
public void execute() throws MojoExecutionException, MojoFailureException {
@@ -191,6 +237,10 @@ public void execute() throws MojoExecutionException, MojoFailureException {
return;
}
+ ConfigHandler cfh = new ConfigHandler( this );
+
+ cfh.parseConfiguration();
+
generateIntermediateAp_();
// Initialize apk build configuration
@@ -255,8 +305,90 @@ void createApkFile(File outputFile, boolean signWithDebugKeyStore) throws MojoEx
doAPKWithCommand(outputFile, dexFile, zipArchive, sourceFolders, jarFiles,
nativeFolders, signWithDebugKeyStore);
}
+
+ if( this.apkMetaIncludes != null && this.apkMetaIncludes.length > 0 ) {
+ try {
+ addMetaInf( outputFile, jarFiles );
+ }
+ catch( IOException e ) {
+ throw new MojoExecutionException("Could not add META-INF resources.", e);
+ }
+ }
}
+ private void addMetaInf( File outputFile, ArrayList<File> jarFiles ) throws IOException
+ {
+ File tmp = File.createTempFile( outputFile.getName(), ".add", outputFile.getParentFile() );
+
+ FileOutputStream fos = new FileOutputStream( tmp );
+ ZipOutputStream zos = new ZipOutputStream( fos );
+ Set<String> entries = new HashSet<String>();
+
+ updateWithMetaInf( zos, outputFile, entries, false );
+
+ for( File f : jarFiles ) {
+ updateWithMetaInf( zos, f, entries, true );
+ }
+
+ zos.close();
+
+ outputFile.delete();
+
+ if( !tmp.renameTo( outputFile ) ) {
+ throw new IOException( String.format( "Cannot rename %s to %s", tmp, outputFile.getName() ) );
+ }
+ }
+
+ private void updateWithMetaInf( ZipOutputStream zos, File jarFile, Set<String> entries, boolean metaInfOnly )
+ throws ZipException, IOException
+ {
+ ZipFile zin = new ZipFile( jarFile );
+
+ for( Enumeration<? extends ZipEntry> en = zin.entries(); en.hasMoreElements(); ) {
+ ZipEntry ze = en.nextElement();
+
+ if( ze.isDirectory() )
+ continue;
+
+ String zn = ze.getName();
+
+ if( metaInfOnly ) {
+ if( !zn.startsWith( "META-INF/" ) ) {
+ continue;
+ }
+
+ if( this.extractDuplicates && !entries.add( zn ) ) {
+ continue;
+ }
+
+ if( !metaInfMatches( zn ) ) {
+ continue;
+ }
+ }
+
+ zos.putNextEntry( new ZipEntry( zn ) );
+
+ InputStream is = zin.getInputStream( ze );
+
+ copyStreamWithoutClosing( is, zos );
+
+ is.close();
+ zos.closeEntry();
+ }
+
+ zin.close();
+ }
+
+ private boolean metaInfMatches( String path )
+ {
+ for( String inc : this.apkMetaIncludes ) {
+ if( SelectorUtils.matchPath( "META-INF/" + inc, path ) )
+ return true;
+ }
+
+ return false;
+ }
+
private Map<String, List<File>> m_jars = new HashMap<String, List<File>>();
private void computeDuplicateFiles(File jar) throws ZipException, IOException {
@@ -368,7 +500,7 @@ public boolean accept(File dir, String name) {
}
private File removeDuplicatesFromJar(File in, List<String> duplicates) {
- File target = new File(project.getBasedir(), "target");
+ String target = project.getBuild().getOutputDirectory();
File tmp = new File(target, "unpacked-embedded-jars");
tmp.mkdirs();
File out = new File(tmp, in.getName());
@@ -837,4 +969,8 @@ protected AndroidSigner getAndroidSigner() {
return new AndroidSigner(sign.getDebug());
}
}
+
+ private String[] getDefaultMetaIncludes() {
+ return new String[0];
+ }
}
89 src/test/java/com/jayway/maven/plugins/android/phase09package/ApkMojoTest.java
View
@@ -0,0 +1,89 @@
+
+package com.jayway.maven.plugins.android.phase09package;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import com.jayway.maven.plugins.android.AbstractAndroidMojoTestCase;
+import com.jayway.maven.plugins.android.config.ConfigHandler;
+
+@RunWith( Parameterized.class )
+public class ApkMojoTest
+extends AbstractAndroidMojoTestCase<ApkMojo>
+{
+
+ @Parameters
+ static public List<Object[]> suite()
+ {
+ final List<Object[]> suite = new ArrayList<Object[]>();
+
+ suite.add( new Object[] { "apk-config-project1", new String[0] } );
+ suite.add( new Object[] { "apk-config-project2", new String[] { "persistence.xml" } } );
+ suite.add( new Object[] { "apk-config-project2", new String[] { "services/**", "persistence.xml" } } );
+
+ return suite;
+ }
+
+ private final String projectName;
+
+ private final String[] expected;
+
+ public ApkMojoTest( String projectName, String[] expected )
+ {
+ this.projectName = projectName;
+ this.expected = expected;
+ }
+
+ @Override
+ public String getPluginGoalName()
+ {
+ return "apk";
+ }
+
+ @Override
+ @Before
+ public void setUp()
+ throws Exception
+ {
+ super.setUp();
+ }
+
+ @Override
+ @After
+ public void tearDown()
+ throws Exception
+ {
+ super.tearDown();
+ }
+
+ @Test
+ public void testConfigHelper()
+ throws Exception
+ {
+ final ApkMojo mojo = createMojo( this.projectName );
+
+ final ConfigHandler cfh = new ConfigHandler( mojo );
+
+ cfh.parseConfiguration();
+
+ final String[] includes = getFieldValue( mojo, "apkMetaIncludes" );
+
+ Assert.assertNotNull( includes );
+ Assert.assertArrayEquals( this.expected, includes );
+ }
+
+ protected <T> T getFieldValue( Object object, String fieldName )
+ throws IllegalAccessException
+ {
+ return (T) super.getVariableValueFromObject( object, fieldName );
+ }
+
+}
17 src/test/resources/apk-config-project1/plugin-config.xml
View
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.jayway.maven.plugins.android.tests</groupId>
+ <artifactId>apk-config-project1</artifactId>
+ <version>15.4.3.1011</version>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>android-maven-plugin</artifactId>
+ <configuration>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
22 src/test/resources/apk-config-project2/plugin-config.xml
View
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.jayway.maven.plugins.android.tests</groupId>
+ <artifactId>apk-config-project2</artifactId>
+ <version>15.4.3.1011</version>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>android-maven-plugin</artifactId>
+ <configuration>
+ <apk>
+ <metaIncludes>
+ <include>persistence.xml</include>
+ </metaIncludes>
+ </apk>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
23 src/test/resources/apk-config-project3/plugin-config.xml
View
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <groupId>com.jayway.maven.plugins.android.tests</groupId>
+ <artifactId>apk-config-project2</artifactId>
+ <version>15.4.3.1011</version>
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>android-maven-plugin</artifactId>
+ <configuration>
+ <apk>
+ <metaIncludes>
+ <include>services/**</include>
+ <include>persistence.xml</include>
+ </metaIncludes>
+ </apk>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
Something went wrong with that request. Please try again.