package com.ensoftcorp.open.android.essentials.permissions;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedList;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import org.eclipse.core.resources.IProject;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.CoreException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import com.ensoftcorp.open.android.essentials.permissions.mappings.PermissionMapping;
/**
* This is a container object for encapsulating data and logic for parsing an Android Manifest file
* @author Ben Holland
*/
public class AndroidManifest {
/**
* Android Manifest file name
*/
public final static String ANDROID_MANIFEST_FILENAME = "AndroidManifest.xml";
/**
* Returns a File object pointing to the AndroidManifest.xml file for the given Eclipse Android workspace project
* @param project
* @return
* @throws Exception
*/
public static File getManifestFile(String project) throws Exception {
if (ResourcesPlugin.getWorkspace().getRoot().getProject(project).exists()) {
if (ResourcesPlugin.getWorkspace().getRoot().getProject(project).getFile(ANDROID_MANIFEST_FILENAME).exists()) {
return ResourcesPlugin.getWorkspace().getRoot().getProject(project).getFile(ANDROID_MANIFEST_FILENAME).getLocation().toFile();
}
}
throw new IllegalArgumentException("Manifest file not found");
}
/**
* Returns File objects pointing to AndroidManifest.xml files of projects the given Eclipse Android workspace project depends upon
* @param project
* @return
* @throws CoreException
*/
public static Collection<File> getDependentManifestFiles(String project) throws CoreException {
Collection<File> files = new LinkedList<File>();
for (IProject dependency : ResourcesPlugin.getWorkspace().getRoot().getProject(project).getReferencedProjects()) {
if (dependency.getFile(ANDROID_MANIFEST_FILENAME).exists()) {
files.add(dependency.getFile(ANDROID_MANIFEST_FILENAME).getLocation().toFile());
}
}
return files;
}
/**
* Returns a collection of Manifest objects for each AndroidManifest.xml file given
* @param files
* @return
* @throws IOException
* @throws IllegalArgumentException
*/
public static Collection<AndroidManifest> parseManifestFiles(Collection<File> files) throws IllegalArgumentException, IOException {
Collection<AndroidManifest> manifests = new LinkedList<AndroidManifest>();
for (File file : files) {
manifests.add(new AndroidManifest(file));
}
return manifests;
}
private File projectDirectory;
private File manifestLocation;
private Document androidManifest;
private Collection<Permission> manifestUsesPermissions = new LinkedList<Permission>();
private HashSet<String> manifestUnidentifedPermissions = new HashSet<String>();
private ArrayList<File> appIcons = new ArrayList<File>(); // sorted by best to worst resolutions
// Google: If you do not declare this attribute, the system assumes a default value of "1",
// which indicates that your application is compatible with all versions of Android.
private int minSDKVersion = 1;
// Google: Declaring this attribute is not recommended. First, there is no need to set the
// attribute as means of blocking deployment of your application onto new versions of the
// Android platform as they are released. By design, new versions of the platform are fully
// backward-compatible....Future versions of Android (beyond Android 2.0.1) will no longer
// check or enforce the maxSdkVersion attribute during installation or re-validation.
private int maxSDKVersion = PermissionMapping.HIGHEST_AVAILABLE_MAPPING;
// Google: If not set, the default value equals that given to minSdkVersion.
private int targetSDKVersion = minSDKVersion;
/**
* Constructs an AndroidManifest object from the given AndroidManifest.xml file
* @param androidManifestFile
* @throws IOException
* @throws IllegalArgumentException
*/
public AndroidManifest(File androidManifestFile) throws IOException, IllegalArgumentException {
projectDirectory = androidManifestFile.getParentFile();
manifestLocation = androidManifestFile;
// read the manifest file into memory
if (androidManifestFile.exists()) {
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
androidManifest = dBuilder.parse(androidManifestFile);
androidManifest.getDocumentElement().normalize();
} catch (Exception e){
throw new IOException("Project manifest file is corrupted.", e);
}
} else {
throw new IllegalArgumentException("Project manifest file is missing.");
}
// parse out the uses permissions
parseUsesPermissions();
// parse the min, max, and target sdk versions
parseUsesSDK();
// parse out the app icon
locateAppIconFiles();
}
/**
* Returns a collection of requested Android Permissions (includes documented, undocumented types)
* @return
*/
public Collection<Permission> getUsesPermissions() {
return manifestUsesPermissions;
}
/**
* Returns unidentified requested permissions. These are either custom application permissions or
* permissions that have not be included in the referenced permission mapping permissions list.
* @return
*/
public HashSet<String> getUnidentifedUsesPermissions(){
return manifestUnidentifedPermissions;
}
/**
* Returns an ordered list of File objects pointing to the App icons found in the workspace
* List is ordered by highest to lowest app icon resolutions
* Collection may be empty if not app icons were located
* @return
*/
public ArrayList<File> getAppIcons() {
return appIcons;
}
/**
* Returns the highest resolution app icon located in the workspace or null if no app icons were located
* @return
*/
public File getAppIcon() {
if(!appIcons.isEmpty()){
return appIcons.get(0); // return the highest quality app icon that was discovered
}
return null;
}
/**
* Returns the location of the AndroidManifest.xml file represented by this object instance
* @return
*/
public File getManifestLocation() {
return manifestLocation;
}
/**
* Returns the min supported SDK version
* @return
*/
public int getMinSDKVersion() {
return minSDKVersion;
}
/**
* Returns the max supported SDK version (or highest available mapping known in the PermissionMapping if not set)
* @return
*/
public int getMaxSDKVersion() {
return maxSDKVersion;
}
/**
* Returns the target supported SDK version
* @return
*/
public int getTargetSDKVersion() {
return targetSDKVersion;
}
/**
* Helper method for parsing requested permissions
*/
private void parseUsesPermissions() {
NodeList usesPermissions = androidManifest.getElementsByTagName("uses-permission");
if (usesPermissions.getLength() != 0) {
try {
for (int i = 0; i < usesPermissions.getLength(); i++) {
Node node = usesPermissions.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
String qualifiedPermissionName = element.getAttribute("android:name");
Permission permission = Permission.getPermissionByQualifiedName(qualifiedPermissionName);
if(permission != null){
manifestUsesPermissions.add(permission);
} else {
manifestUnidentifedPermissions.add(qualifiedPermissionName);
}
}
}
} catch (Exception e) {
// manifest is malformed
}
}
}
/**
* Helper method for parsing min, max, and target api versions
*/
private void parseUsesSDK(){
NodeList usesSdk = androidManifest.getElementsByTagName("uses-sdk");
if (usesSdk.getLength() != 0) {
try {
for (int i = 0; i < usesSdk.getLength(); i++) {
Node node = usesSdk.item(i);
if (node.getNodeType() == Node.ELEMENT_NODE) {
Element element = (Element) node;
// check for specified min sdk version
if(element.hasAttribute("android:minSdkVersion") || element.hasAttribute("minSdkVersion")){
String minSDK = element.getAttribute("android:minSdkVersion");
if(minSDK == null || minSDK.equals("")){
minSDK = element.getAttribute("minSdkVersion");
}
try {
minSDKVersion = Integer.parseInt(minSDK);
targetSDKVersion = minSDKVersion;
} catch (Exception e){}
}
// check for specified max sdk version
if(element.hasAttribute("android:maxSdkVersion") || element.hasAttribute("maxSdkVersion")){
String maxSDK = element.getAttribute("android:maxSdkVersion");
if(maxSDK == null || maxSDK.equals("")){
maxSDK = element.getAttribute("maxSdkVersion");
}
try {
maxSDKVersion = Integer.parseInt(maxSDK);
} catch (Exception e){}
}
// check for specified target sdk version
if(element.hasAttribute("android:targetSdkVersion") || element.hasAttribute("targetSdkVersion")){
String targetSDK = element.getAttribute("android:targetSdkVersion");
if(targetSDK == null || targetSDK.equals("")){
targetSDK = element.getAttribute("targetSdkVersion");
}
try {
targetSDKVersion = Integer.parseInt(targetSDK);
} catch (Exception e){}
}
}
}
} catch (Exception e) {
// manifest is malformed
}
}
}
/**
* Utility method for finding a File handle the app icon.
* Gathers icons in order of highest available resolution image of the app icon first.
* @return
*/
private void locateAppIconFiles() {
try {
// there should only be one application element
Element application = (Element) androidManifest.getElementsByTagName("application").item(0);
String attribute = application.getAttribute("android:icon");
// icon is always a png file that is sort of relative-ish
String iconName = new File(attribute.replace("@", "")).getName() + ".png";
// the @ means the directory could hold many different resolution
// sizes in -hdpi, -ldpi, -mdi, -xhdpi, etc. resolutions
// so we do a little black magic...there's probably a better way to do this...
String iconDirectory = new File(attribute.replace("@", "")).getPath();
iconDirectory = iconDirectory.substring(0, iconDirectory.lastIndexOf(File.separatorChar + iconName.replace(".png", "")));
// project resources are in the /res directory
File resourcesDirectory = new File(projectDirectory.getAbsolutePath() + File.separatorChar + "res");
// ok...so it can be in a few places depending on resolutions...lets just
// try them in order of best to worst...
String[] resolutions = { "-xhdpi", "-hdpi", "-ldpi", "-mdpi", "" };
for (String res : resolutions) {
File icon = new File(resourcesDirectory.getAbsolutePath() + File.separatorChar + iconDirectory + res + File.separatorChar + iconName);
if (icon.exists()) {
appIcons.add(icon);
}
}
} catch (Exception e) {
// manifest is malformed, or the icon is in a really weird spot...so...no icons for you!
}
}
}