All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.androidannotations.internal.helper.AndroidManifestFinder Maven / Gradle / Ivy

The newest version!
/**
 * Copyright (C) 2010-2016 eBusiness Information, Excilys Group
 * Copyright (C) 2016-2020 the AndroidAnnotations project
 *
 * 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 org.androidannotations.internal.helper;

import static org.androidannotations.helper.CaseHelper.upperCaseFirst;
import static org.androidannotations.helper.ModelConstants.classSuffix;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.lang.model.util.Elements;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;

import org.androidannotations.AndroidAnnotationsEnvironment;
import org.androidannotations.Option;
import org.androidannotations.helper.AndroidManifest;
import org.androidannotations.internal.exception.AndroidManifestNotFoundException;
import org.androidannotations.logger.Logger;
import org.androidannotations.logger.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class AndroidManifestFinder {

	public static final Option OPTION_MANIFEST = new Option("androidManifestFile", null);

	public static final Option OPTION_LIBRARY = new Option("library", "false");
	public static final Option OPTION_INSTANT_FEATURE = new Option("instantAppFeature", "false");

	private static final Logger LOGGER = LoggerFactory.getLogger(AndroidManifestFinder.class);

	private final AndroidAnnotationsEnvironment environment;

	public AndroidManifestFinder(AndroidAnnotationsEnvironment environment) {
		this.environment = environment;
	}

	public AndroidManifest extractAndroidManifest() throws AndroidManifestNotFoundException {
		try {
			File androidManifestFile = findManifestFile();
			String projectDirectory = androidManifestFile.getParent();

			boolean libraryOption = environment.getOptionBooleanValue(OPTION_LIBRARY);

			if (libraryOption) {
				return parse(androidManifestFile, true);
			}

			File projectProperties = new File(projectDirectory, "project.properties");

			boolean libraryProject = false;
			if (projectProperties.exists()) {
				Properties properties = new Properties();
				try {
					properties.load(new FileInputStream(projectProperties));
					if (properties.containsKey("android.library")) {
						String androidLibraryProperty = properties.getProperty("android.library");
						libraryProject = "true".equals(androidLibraryProperty);

						LOGGER.debug("Found android.library={} property in project.properties", libraryProject);
					}
				} catch (IOException ignored) {
					// we assume the project is not a library
				}
			}

			return parse(androidManifestFile, libraryProject);
		} catch (FileNotFoundException exception) {
			throw new AndroidManifestNotFoundException("Unable to find AndroidManifest.xml", exception);
		}
	}

	private File findManifestFile() throws FileNotFoundException {
		String androidManifestFile = environment.getOptionValue(OPTION_MANIFEST);
		if (androidManifestFile != null) {
			return findManifestInSpecifiedPath(androidManifestFile);
		} else {
			return findManifestInKnownPaths();
		}
	}

	private File findManifestInSpecifiedPath(String androidManifestPath) throws FileNotFoundException {
		File androidManifestFile = new File(androidManifestPath);
		if (!androidManifestFile.exists()) {
			LOGGER.error("Could not find the AndroidManifest.xml file in specified path : {}", androidManifestPath);
			throw new FileNotFoundException();
		} else {
			LOGGER.debug("AndroidManifest.xml file found with specified path: {}", androidManifestFile.toString());
		}
		return androidManifestFile;
	}

	private File findManifestInKnownPaths() throws FileNotFoundException {
		FileHelper.FileHolder holder = FileHelper.findRootProjectHolder(environment.getProcessingEnvironment());
		return findManifestInKnownPathsStartingFromGenFolder(holder.sourcesGenerationFolder.getAbsolutePath());
	}

	File findManifestInKnownPathsStartingFromGenFolder(String sourcesGenerationFolder) {
		Iterable strategies = Arrays.asList(new GradleAndroidManifestFinderStrategy(environment, sourcesGenerationFolder),
				new LegacyGradleAndroidManifestFinderStrategy(environment, sourcesGenerationFolder), new MavenAndroidManifestFinderStrategy(sourcesGenerationFolder),
				new EclipseAndroidManifestFinderStrategy(sourcesGenerationFolder));

		AndroidManifestFinderStrategy applyingStrategy = null;

		for (AndroidManifestFinderStrategy strategy : strategies) {
			if (strategy.applies()) {
				applyingStrategy = strategy;
				break;
			}
		}

		File androidManifestFile = null;

		if (applyingStrategy != null) {
			androidManifestFile = applyingStrategy.findAndroidManifestFile();
		}

		if (androidManifestFile != null) {
			LOGGER.debug("{} AndroidManifest.xml file found using generation folder {}: {}", applyingStrategy.name, sourcesGenerationFolder, androidManifestFile.toString());
		} else {
			LOGGER.error("Could not find the AndroidManifest.xml file, using  generation folder [{}])", sourcesGenerationFolder);
		}

		return androidManifestFile;
	}

	private static abstract class AndroidManifestFinderStrategy {
		final String name;

		final Matcher matcher;

		AndroidManifestFinderStrategy(String name, Pattern sourceFolderPattern, String sourceFolder) {
			this.name = name;
			this.matcher = sourceFolderPattern.matcher(sourceFolder);
		}

		File findAndroidManifestFile() {
			for (String location : possibleLocations()) {
				File manifestFile = new File(matcher.group(1), location + "/AndroidManifest.xml");
				if (manifestFile.exists()) {
					return manifestFile;
				}
			}
			return null;
		}

		boolean applies() {
			return matcher.matches();
		}

		abstract Iterable possibleLocations();
	}

	private static class GradleAndroidManifestFinderStrategy extends AbstractGradleAndroidManifestFinderStrategy {

		private static final Pattern GRADLE_GEN_FOLDER = Pattern.compile("^(.*?)build[\\\\/]generated[\\\\/]ap_generated_sources[\\\\/](.*)[\\\\/]out(.*)$");

		GradleAndroidManifestFinderStrategy(AndroidAnnotationsEnvironment environment, String sourceFolder) {
			super(GRADLE_GEN_FOLDER, environment, sourceFolder);
		}

		@Override
		protected String getGradleVariant() {
			return matcher.group(2);
		}
	}

	private static class LegacyGradleAndroidManifestFinderStrategy extends AbstractGradleAndroidManifestFinderStrategy {

		private static final Pattern GRADLE_GEN_FOLDER = Pattern.compile("^(.*?)build[\\\\/]generated[\\\\/]source[\\\\/](k?apt)(.*)$");

		LegacyGradleAndroidManifestFinderStrategy(AndroidAnnotationsEnvironment environment, String sourceFolder) {
			super(GRADLE_GEN_FOLDER, environment, sourceFolder);
		}

		@Override
		protected String getGradleVariant() {
			return matcher.group(3).substring(1);
		}
	}

	private static abstract class AbstractGradleAndroidManifestFinderStrategy extends AndroidManifestFinderStrategy {

		static final Pattern OUTPUT_JSON_PATTERN = Pattern.compile(".*,\"path\":\"(.*?)\",.*");

		private static final List SUPPORTED_ABI_SPLITS = Arrays.asList("arm64-v8a", "armeabi", "armeabi-v7a", "mips", "mips64", "x86", "x86_64");
		private static final List SUPPORTED_DENSITY_SPLITS = Arrays.asList("hdpi", "ldpi", "mdpi", "xhdpi", "xxhdpi", "xxxhdpi");

		private static final String BUILD_TOOLS_V32_MANIFEST_PATH = "build/intermediates/merged_manifests";

		private final AndroidAnnotationsEnvironment environment;

		AbstractGradleAndroidManifestFinderStrategy(Pattern pattern, AndroidAnnotationsEnvironment environment, String sourceFolder) {
			super("Gradle", pattern, sourceFolder);
			this.environment = environment;
		}

		protected String getPath() {
			return matcher.group(1);
		}

		protected abstract String getGradleVariant();

		@Override
		Iterable possibleLocations() {
			String path = getPath();
			String gradleVariant = getGradleVariant();

			List possibleLocations = new ArrayList<>();
			findPossibleLocationsV32(path, gradleVariant, possibleLocations);
			for (String directory : Arrays.asList("build/intermediates/manifests/full", "build/intermediates/bundles", "build/intermediates/manifests/aapt", "build/intermediates/library_manifest")) {
				findPossibleLocations(path, directory, gradleVariant, possibleLocations);
			}

			return updateLocations(path, possibleLocations);
		}

		private List updateLocations(String path, List possibleLocations) {
			List knownLocations = new ArrayList<>();
			for (String location : possibleLocations) {
				String expectedLocation = path + "/" + location;
				File file = new File(expectedLocation + "/output.json");
				if (file.exists()) {
					Matcher jsonMatcher = OUTPUT_JSON_PATTERN.matcher(readJsonFromFile(file));
					if (jsonMatcher.matches()) {
						String relativeManifestPath = jsonMatcher.group(1);
						File manifestFile = new File(expectedLocation + "/" + relativeManifestPath);
						String manifestDirectory = manifestFile.getParentFile().getAbsolutePath();
						knownLocations.add(manifestDirectory.substring(path.length()));
					}
				}
			}

			if (knownLocations.isEmpty()) {
				knownLocations.addAll(possibleLocations);
			}

			return knownLocations;
		}

		private String readJsonFromFile(File file) {
			try (BufferedReader fileReader = new BufferedReader(new FileReader(file))) {
				return fileReader.readLine();
			} catch (IOException e) {
				LOGGER.error(e, "unable to read json file: {}", file);
				return "";
			}
		}

		private void findPossibleLocationsV32(String basePath, String variantPart, List possibleLocations) {
			String[] directories = new File(basePath + BUILD_TOOLS_V32_MANIFEST_PATH).list();

			if (directories == null) {
				return;
			}

			if (variantPart.startsWith("/") || variantPart.startsWith("\\")) {
				variantPart = variantPart.substring(1);
			}

			boolean isFeature = environment.getOptionBooleanValue(OPTION_INSTANT_FEATURE) && (variantPart.startsWith("feature/") || variantPart.startsWith("feature\\"));
			if (isFeature) {
				variantPart = variantPart.substring(8);
			}

			String[] variantParts = variantPart.split("[/\\\\]");
			if (variantParts.length > 1) {
				StringBuilder sb = new StringBuilder(variantParts[0]);
				for (int i = 1; i < variantParts.length; i++) {
					String part = variantParts[i];
					sb.append(upperCaseFirst(part));
				}
				variantPart = sb.toString();
			}

			String possibleLocation = BUILD_TOOLS_V32_MANIFEST_PATH + "/" + variantPart;
			if (isFeature) {
				variantPart += "Feature";
				possibleLocation += "Feature";
			}

			findPossibleLocations(basePath, possibleLocations, possibleLocation);
			findPossibleLocations(basePath, possibleLocations, possibleLocation + "/process" + upperCaseFirst(variantPart) + "Manifest/merged");
		}

		private void findPossibleLocations(String basePath, List possibleLocations, String possibleLocationWithProcessManifest) {
			if (new File(basePath, possibleLocationWithProcessManifest).isDirectory()) {
				possibleLocations.add(possibleLocationWithProcessManifest);
				addPossibleSplitLocations(basePath, possibleLocationWithProcessManifest, possibleLocations);
			}
		}

		private void findPossibleLocations(String basePath, String targetPath, String variantPart, List possibleLocations) {
			String[] directories = new File(basePath + targetPath).list();

			if (directories == null) {
				return;
			}

			if (variantPart.startsWith("/") || variantPart.startsWith("\\")) {
				variantPart = variantPart.substring(1);
			}

			for (String directory : directories) {
				String possibleLocation = targetPath + "/" + directory;
				File variantDir = new File(basePath + possibleLocation);
				if (variantDir.isDirectory() && variantPart.toLowerCase().startsWith(directory.toLowerCase())) {
					String remainingPart = variantPart.substring(directory.length());
					if (remainingPart.length() == 0) {
						possibleLocations.add(possibleLocation);
						addPossibleSplitLocations(basePath, possibleLocation, possibleLocations);
					} else {
						findPossibleLocations(basePath, possibleLocation, remainingPart, possibleLocations);
					}
				}
			}
		}

		private void addPossibleSplitLocations(String basePath, String possibleLocation, List possibleLocations) {
			for (String abiSplit : SUPPORTED_ABI_SPLITS) {
				File splitDir = new File(basePath + possibleLocation + "/" + abiSplit);
				if (splitDir.isDirectory()) {
					possibleLocations.add(possibleLocation + "/" + abiSplit);
					for (String densitySplit : SUPPORTED_DENSITY_SPLITS) {
						File splitSubDir = new File(basePath + possibleLocation + "/" + abiSplit + "/" + densitySplit);
						if (splitSubDir.isDirectory()) {
							possibleLocations.add(possibleLocation + "/" + abiSplit + "/" + densitySplit);
						}
					}
				}
			}
			for (String densitySplit : SUPPORTED_DENSITY_SPLITS) {
				File splitDir = new File(basePath + possibleLocation + "/" + densitySplit);
				if (splitDir.isDirectory()) {
					possibleLocations.add(possibleLocation + "/" + densitySplit);
				}
			}
		}
	}

	private static class MavenAndroidManifestFinderStrategy extends AndroidManifestFinderStrategy {

		static final Pattern MAVEN_GEN_FOLDER = Pattern.compile("^(.*?)target[\\\\/]generated-sources.*$");

		MavenAndroidManifestFinderStrategy(String sourceFolder) {
			super("Maven", MAVEN_GEN_FOLDER, sourceFolder);
		}

		@Override
		Iterable possibleLocations() {
			return Arrays.asList("target", "src/main", "");
		}
	}

	private static class EclipseAndroidManifestFinderStrategy extends AndroidManifestFinderStrategy {

		static final Pattern ECLIPSE_GEN_FOLDER = Pattern.compile("^(.*?)\\.apt_generated.*$");

		EclipseAndroidManifestFinderStrategy(String sourceFolder) {
			super("Eclipse", ECLIPSE_GEN_FOLDER, sourceFolder);
		}

		@Override
		Iterable possibleLocations() {
			return Collections.singleton("");
		}
	}

	private AndroidManifest parse(File androidManifestFile, boolean libraryProject) throws AndroidManifestNotFoundException {
		DocumentBuilderFactory docBuilderFactory = DocumentBuilderFactory.newInstance();

		Document doc;
		try {
			DocumentBuilder docBuilder = docBuilderFactory.newDocumentBuilder();
			doc = docBuilder.parse(androidManifestFile);
		} catch (Exception e) {
			LOGGER.error("Could not parse the AndroidManifest.xml file at path {}", androidManifestFile, e);
			throw new AndroidManifestNotFoundException("Could not parse the AndroidManifest.xml file at path {}" + androidManifestFile, e);
		}

		Element documentElement = doc.getDocumentElement();
		documentElement.normalize();

		String applicationPackage = documentElement.getAttribute("package");

		int minSdkVersion = -1;
		int maxSdkVersion = -1;
		int targetSdkVersion = -1;
		NodeList sdkNodes = documentElement.getElementsByTagName("uses-sdk");
		if (sdkNodes.getLength() > 0) {
			Node sdkNode = sdkNodes.item(0);
			minSdkVersion = extractAttributeIntValue(sdkNode, "android:minSdkVersion", -1);
			maxSdkVersion = extractAttributeIntValue(sdkNode, "android:maxSdkVersion", -1);
			targetSdkVersion = extractAttributeIntValue(sdkNode, "android:targetSdkVersion", -1);
		}

		if (libraryProject) {
			return AndroidManifest.createLibraryManifest(applicationPackage, minSdkVersion, maxSdkVersion, targetSdkVersion);
		}

		NodeList applicationNodes = documentElement.getElementsByTagName("application");

		String applicationClassQualifiedName = null;
		boolean applicationDebuggableMode = false;

		if (applicationNodes.getLength() > 0) {
			Node applicationNode = applicationNodes.item(0);
			Node nameAttribute = applicationNode.getAttributes().getNamedItem("android:name");

			applicationClassQualifiedName = manifestNameToValidQualifiedName(applicationPackage, nameAttribute);

			if (applicationClassQualifiedName == null) {
				if (nameAttribute != null) {
					LOGGER.warn("The class application declared in the AndroidManifest.xml cannot be found in the compile path: [{}]", nameAttribute.getNodeValue());
				}
			}

			Node debuggableAttribute = applicationNode.getAttributes().getNamedItem("android:debuggable");
			if (debuggableAttribute != null) {
				applicationDebuggableMode = debuggableAttribute.getNodeValue().equalsIgnoreCase("true");
			}
		}

		NodeList activityNodes = documentElement.getElementsByTagName("activity");
		List activityQualifiedNames = extractComponentNames(applicationPackage, activityNodes);

		NodeList serviceNodes = documentElement.getElementsByTagName("service");
		List serviceQualifiedNames = extractComponentNames(applicationPackage, serviceNodes);

		NodeList receiverNodes = documentElement.getElementsByTagName("receiver");
		List receiverQualifiedNames = extractComponentNames(applicationPackage, receiverNodes);

		NodeList providerNodes = documentElement.getElementsByTagName("provider");
		List providerQualifiedNames = extractComponentNames(applicationPackage, providerNodes);

		List componentQualifiedNames = new ArrayList<>();
		componentQualifiedNames.addAll(activityQualifiedNames);
		componentQualifiedNames.addAll(serviceQualifiedNames);
		componentQualifiedNames.addAll(receiverQualifiedNames);
		componentQualifiedNames.addAll(providerQualifiedNames);

		NodeList metaDataNodes = documentElement.getElementsByTagName("meta-data");
		Map metaDataQualifiedNames = extractMetaDataQualifiedNames(metaDataNodes);

		NodeList usesPermissionNodes = documentElement.getElementsByTagName("uses-permission");
		List usesPermissionQualifiedNames = extractUsesPermissionNames(usesPermissionNodes);

		List permissionQualifiedNames = new ArrayList<>();
		permissionQualifiedNames.addAll(usesPermissionQualifiedNames);

		return AndroidManifest.createManifest(applicationPackage, applicationClassQualifiedName, componentQualifiedNames, metaDataQualifiedNames, permissionQualifiedNames, minSdkVersion,
				maxSdkVersion, targetSdkVersion, applicationDebuggableMode);
	}

	private int extractAttributeIntValue(Node node, String attribute, int defaultValue) {
		try {
			NamedNodeMap attributes = node.getAttributes();
			if (attributes.getLength() > 0) {
				Node attributeNode = attributes.getNamedItem(attribute);
				if (attributeNode != null) {
					return Integer.parseInt(attributeNode.getNodeValue());
				}
			}
		} catch (NumberFormatException ignored) {
			// we assume the manifest is well-formed
		}
		return defaultValue;
	}

	private List extractComponentNames(String applicationPackage, NodeList componentNodes) {
		List componentQualifiedNames = new ArrayList<>();

		for (int i = 0; i < componentNodes.getLength(); i++) {
			Node activityNode = componentNodes.item(i);
			Node nameAttribute = activityNode.getAttributes().getNamedItem("android:name");

			String qualifiedName = manifestNameToValidQualifiedName(applicationPackage, nameAttribute);

			if (qualifiedName != null) {
				componentQualifiedNames.add(qualifiedName);
			} else {
				if (nameAttribute != null) {
					LOGGER.warn("A class activity declared in the AndroidManifest.xml cannot be found in the compile path: [{}]", nameAttribute.getNodeValue());
				} else {
					LOGGER.warn("The {} activity node in the AndroidManifest.xml has no android:name attribute", i);
				}
			}
		}
		return componentQualifiedNames;
	}

	private Map extractMetaDataQualifiedNames(NodeList metaDataNodes) {
		Map metaDataQualifiedNames = new HashMap();

		for (int i = 0; i < metaDataNodes.getLength(); i++) {
			Node node = metaDataNodes.item(i);
			Node nameAttribute = node.getAttributes().getNamedItem("android:name");
			Node valueAttribute = node.getAttributes().getNamedItem("android:value");
			Node resourceAttribute = node.getAttributes().getNamedItem("android:resource");

			if (nameAttribute == null || (valueAttribute == null && resourceAttribute == null)) {
				if (nameAttribute != null) {
					LOGGER.warn("A malformed  has been found in the manifest with name {}", nameAttribute.getNodeValue());
				} else {
					LOGGER.warn("A malformed  has been found in the manifest");
				}
			} else {
				String name = nameAttribute.getNodeValue();
				String value = valueAttribute != null ? valueAttribute.getNodeValue() : null;
				String resource = resourceAttribute != null ? resourceAttribute.getNodeValue() : null;
				metaDataQualifiedNames.put(name, new AndroidManifest.MetaDataInfo(name, value, resource));
			}
		}

		return metaDataQualifiedNames;
	}

	private String manifestNameToValidQualifiedName(String applicationPackage, Node nameAttribute) {
		if (nameAttribute != null) {
			String activityName = nameAttribute.getNodeValue();
			if (activityName.startsWith(applicationPackage)) {
				return returnClassIfExistsOrNull(activityName);
			} else {
				if (activityName.startsWith(".")) {
					return returnClassIfExistsOrNull(applicationPackage + activityName);
				} else {
					if (classOrModelClassExists(activityName)) {
						return activityName;
					} else {
						return returnClassIfExistsOrNull(applicationPackage + "." + activityName);
					}
				}
			}
		} else {
			return null;
		}
	}

	private boolean classOrModelClassExists(String className) {
		Elements elementUtils = environment.getProcessingEnvironment().getElementUtils();

		if (className.endsWith(classSuffix())) {
			className = className.substring(0, className.length() - classSuffix().length());
		}
		return elementUtils.getTypeElement(className) != null;
	}

	private String returnClassIfExistsOrNull(String className) {
		if (classOrModelClassExists(className)) {
			return className;
		} else {
			return null;
		}
	}

	private List extractUsesPermissionNames(NodeList usesPermissionNodes) {
		List usesPermissionQualifiedNames = new ArrayList<>();

		for (int i = 0; i < usesPermissionNodes.getLength(); i++) {
			Node usesPermissionNode = usesPermissionNodes.item(i);
			Node nameAttribute = usesPermissionNode.getAttributes().getNamedItem("android:name");

			if (nameAttribute == null) {
				return null;
			}

			usesPermissionQualifiedNames.add(nameAttribute.getNodeValue());
		}
		return usesPermissionQualifiedNames;
	}

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy