diff --git a/src/Xamarin.Android.Tools.AndroidSdk/AndroidAppManifest.cs b/src/Xamarin.Android.Tools.AndroidSdk/AndroidAppManifest.cs
new file mode 100644
index 0000000..88ee2cb
--- /dev/null
+++ b/src/Xamarin.Android.Tools.AndroidSdk/AndroidAppManifest.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Linq;
+using System.Xml;
+using System.Collections.Generic;
+using System.Xml.Linq;
+using System.Text.RegularExpressions;
+using System.Text;
+using System.IO;
+
+namespace Xamarin.Android.Tools
+{
+ public class AndroidAppManifest
+ {
+ AndroidVersions versions;
+ XDocument doc;
+ XElement manifest, application, usesSdk;
+
+ static readonly XNamespace aNS = "http://schemas.android.com/apk/res/android";
+ static readonly XName aName = aNS + "name";
+
+ AndroidAppManifest (AndroidVersions versions, XDocument doc)
+ {
+ if (versions == null)
+ throw new ArgumentNullException (nameof (versions));
+ if (doc == null)
+ throw new ArgumentNullException (nameof (doc));
+ this.versions = versions;
+ this.doc = doc;
+ manifest = doc.Root;
+ if (manifest.Name != "manifest")
+ throw new ArgumentException ("App manifest does not have 'manifest' root element", nameof (doc));
+
+ application = manifest.Element ("application");
+ if (application == null)
+ manifest.Add (application = new XElement ("application"));
+
+ usesSdk = manifest.Element ("uses-sdk");
+ if (usesSdk == null)
+ manifest.Add (usesSdk = new XElement ("uses-sdk"));
+ }
+
+ public static string CanonicalizePackageName (string packageNameOrAssemblyName)
+ {
+ if (packageNameOrAssemblyName == null)
+ throw new ArgumentNullException ("packageNameOrAssemblyName");
+ if (string.IsNullOrEmpty (packageNameOrAssemblyName = packageNameOrAssemblyName.Trim ()))
+ throw new ArgumentException ("Must specify a package name or assembly name", "packageNameOrAssemblyName");
+
+ string[] packageParts = packageNameOrAssemblyName.Split (new[]{'.'}, StringSplitOptions.RemoveEmptyEntries);
+ for (int i = 0; i < packageParts.Length; ++i) {
+ packageParts [i] = Regex.Replace (packageParts [i], "[^A-Za-z0-9_]", "_");
+ if (char.IsDigit (packageParts [i], 0) || packageParts [i][0] == '_')
+ packageParts [i] = "x" + packageParts [i];
+ }
+ return packageParts.Length == 1
+ ? packageParts [0] + "." + packageParts [0]
+ : string.Join (".", packageParts);
+ }
+
+ public static AndroidAppManifest Create (string packageName, string appLabel, AndroidVersions versions)
+ {
+ return new AndroidAppManifest (versions, XDocument.Parse (
+ @"
+
+
+
+
+")) {
+ PackageName = packageName,
+ ApplicationLabel = appLabel,
+ };
+ }
+
+ public static AndroidAppManifest Load (string filename, AndroidVersions versions)
+ {
+ return Load (XDocument.Load (filename), versions);
+ }
+
+ public static AndroidAppManifest Load (XDocument doc, AndroidVersions versions)
+ {
+ return new AndroidAppManifest (versions, doc);
+ }
+
+ public void Write (XmlWriter writer)
+ {
+ doc.Save (writer);
+ }
+
+ public void WriteToFile (string fileName)
+ {
+ var xmlSettings = new XmlWriterSettings () {
+ Encoding = Encoding.UTF8,
+ CloseOutput = false,
+ Indent = true,
+ IndentChars = "\t",
+ NewLineChars = "\n",
+ };
+
+ var tempFile = FileUtil.GetTempFilenameForWrite (fileName);
+ bool success = false;
+ try {
+ using (var writer = XmlTextWriter.Create (tempFile, xmlSettings)) {
+ Write (writer);
+ }
+ FileUtil.SystemRename (tempFile, fileName);
+ success = true;
+ } finally {
+ if (!success) {
+ try {
+ File.Delete (tempFile);
+ } catch {
+ //the original exception is more important than this one
+ }
+ }
+ }
+ }
+
+ static string NullIfEmpty (string value)
+ {
+ return string.IsNullOrEmpty (value) ? null : value;
+ }
+
+ public string PackageName {
+ get { return (string) manifest.Attribute ("package"); }
+ set { manifest.SetAttributeValue ("package", NullIfEmpty (value)); }
+ }
+
+ public string ApplicationLabel {
+ get { return (string) application.Attribute (aNS + "label"); }
+ set { application.SetAttributeValue (aNS + "label", NullIfEmpty (value)); }
+ }
+
+ public string ApplicationIcon {
+ get { return (string) application.Attribute (aNS + "icon"); }
+ set { application.SetAttributeValue (aNS + "icon", NullIfEmpty (value)); }
+ }
+
+ public string ApplicationTheme {
+ get { return (string) application.Attribute (aNS + "theme"); }
+ set { application.SetAttributeValue (aNS + "theme", NullIfEmpty (value)); }
+ }
+
+ public string VersionName {
+ get { return (string) manifest.Attribute (aNS + "versionName"); }
+ set { manifest.SetAttributeValue (aNS + "versionName", NullIfEmpty (value)); }
+ }
+
+ public string VersionCode {
+ get { return (string) manifest.Attribute (aNS + "versionCode"); }
+ set { manifest.SetAttributeValue (aNS + "versionCode", NullIfEmpty (value)); }
+ }
+
+ public string InstallLocation {
+ get { return (string) manifest.Attribute (aNS + "installLocation"); }
+ set { manifest.SetAttributeValue (aNS + "installLocation", NullIfEmpty (value)); }
+ }
+
+ public int? MinSdkVersion {
+ get { return ParseSdkVersion (usesSdk.Attribute (aNS + "minSdkVersion")); }
+ set { usesSdk.SetAttributeValue (aNS + "minSdkVersion", value == null ? null : value.ToString ()); }
+ }
+
+ public int? TargetSdkVersion {
+ get { return ParseSdkVersion (usesSdk.Attribute (aNS + "targetSdkVersion")); }
+ set { usesSdk.SetAttributeValue (aNS + "targetSdkVersion", value == null ? null : value.ToString ()); }
+ }
+
+ int? ParseSdkVersion (XAttribute attribute)
+ {
+ var version = (string)attribute;
+ if (string.IsNullOrEmpty (version))
+ return null;
+ int vn;
+ if (!int.TryParse (version, out vn)) {
+ int? apiLevel = versions.GetApiLevelFromId (version);
+ if (apiLevel.HasValue)
+ return apiLevel.Value;
+ return versions.MaxStableVersion.ApiLevel;
+ }
+ return vn;
+ }
+
+ public IEnumerable AndroidPermissions {
+ get {
+ foreach (var el in manifest.Elements ("uses-permission")) {
+ var name = (string) el.Attribute (aName);
+ if (name == null)
+ continue;
+ var lastDot = name.LastIndexOf ('.');
+ if (lastDot >= 0)
+ yield return name.Substring (lastDot + 1);
+ }
+ }
+ }
+
+ public IEnumerable AndroidPermissionsQualified {
+ get {
+ foreach (var el in manifest.Elements ("uses-permission")) {
+ var name = (string) el.Attribute (aName);
+ if (name != null)
+ yield return name;
+ }
+ }
+ }
+
+ public bool? Debuggable {
+ get { return (bool?) application.Attribute (aNS + "debuggable"); }
+ set { application.SetAttributeValue (aNS + "debuggable", value); }
+ }
+
+ public void SetAndroidPermissions (IEnumerable permissions)
+ {
+ var newPerms = new HashSet (permissions.Select (FullyQualifyPermission));
+ var current = new HashSet (AndroidPermissionsQualified);
+ AddAndroidPermissions (newPerms.Except (current));
+ RemoveAndroidPermissions (current.Except (newPerms));
+ }
+
+ void AddAndroidPermissions (IEnumerable permissions)
+ {
+ var newElements = permissions.Select (p => new XElement ("uses-permission", new XAttribute (aName, p)));
+
+ var lastPerm = manifest.Elements ("uses-permission").LastOrDefault ();
+ if (lastPerm != null) {
+ foreach (var el in newElements) {
+ lastPerm.AddAfterSelf (el);
+ lastPerm = el;
+ }
+ } else {
+ var parentNode = (XNode) manifest.Element ("application") ?? manifest.LastNode;
+ foreach (var el in newElements)
+ parentNode.AddBeforeSelf (el);
+ }
+ }
+
+ string FullyQualifyPermission (string permission)
+ {
+ //if already qualified, don't mess with it
+ if (permission.IndexOf ('.') > -1)
+ return permission;
+
+ switch (permission) {
+ case "READ_HISTORY_BOOKMARKS":
+ case "WRITE_HISTORY_BOOKMARKS":
+ return string.Format ("com.android.browser.permission.{0}", permission);
+ default:
+ return string.Format ("android.permission.{0}", permission);
+ }
+ }
+
+ void RemoveAndroidPermissions (IEnumerable permissions)
+ {
+ var perms = new HashSet (permissions);
+ var list = manifest.Elements ("uses-permission")
+ .Where (el => perms.Contains ((string)el.Attribute (aName))).ToList ();
+ foreach (var el in list)
+ el.Remove ();
+ }
+
+ [Obsolete ("Use GetLaunchableFastdevActivityName or GetLaunchableUserActivityName")]
+ public string GetLaunchableActivityName ()
+ {
+ return GetLaunchableFastDevActivityName ();
+ }
+
+ /// Gets an activity that can be used to initialize the override directory for fastdev.
+ [Obsolete ("This should not be needed anymore; Activity execution is not part of installation.")]
+ public string GetLaunchableFastDevActivityName ()
+ {
+ string first = null;
+ foreach (var a in GetLaunchableActivities ()) {
+ var name = (string) a.Attribute (aName);
+ //prefer the fastdev launcher, it's quicker
+ if (name == "mono.android.__FastDevLauncher") {
+ return name;
+ }
+ //else just use the first other launchable activity
+ if (first == null) {
+ first = name;
+ }
+ }
+
+ return string.IsNullOrEmpty (first)? null : first;
+ }
+
+ // We add a fake launchable activity for FastDev, but we don't want
+ // to launch that one when the user does Run or Debug
+ public string GetLaunchableUserActivityName ()
+ {
+ return GetLaunchableActivities ()
+ .Select (a => (string) a.Attribute (aName))
+ .FirstOrDefault (name => !string.IsNullOrEmpty (name) && name != "mono.android.__FastDevLauncher");
+ }
+
+ IEnumerable GetLaunchableActivities ()
+ {
+ foreach (var activity in application.Elements ("activity")) {
+ var filter = activity.Element ("intent-filter");
+ if (filter != null) {
+ foreach (var category in filter.Elements ("category"))
+ if (category != null && (string)category.Attribute (aName) == "android.intent.category.LAUNCHER")
+ yield return activity;
+ }
+ }
+ }
+
+ public IEnumerable GetAllActivityNames ()
+ {
+ foreach (var activity in application.Elements ("activity")) {
+ var activityName = (string) activity.Attribute (aName);
+ if (activityName != "mono.android.__FastDevLauncher")
+ yield return activityName;
+ }
+ }
+
+ public IEnumerable GetLaunchableActivityNames ()
+ {
+ return GetLaunchableActivities ()
+ .Select (a => (string) a.Attribute (aName))
+ .Where (name => !string.IsNullOrEmpty (name) && name != "mono.android.__FastDevLauncher");
+ }
+ }
+}
+
diff --git a/src/Xamarin.Android.Tools.AndroidSdk/FileUtil.cs b/src/Xamarin.Android.Tools.AndroidSdk/FileUtil.cs
new file mode 100644
index 0000000..6fe8487
--- /dev/null
+++ b/src/Xamarin.Android.Tools.AndroidSdk/FileUtil.cs
@@ -0,0 +1,58 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+
+namespace Xamarin.Android.Tools
+{
+ class FileUtil
+ {
+ public static string GetTempFilenameForWrite (string fileName)
+ {
+ return Path.GetDirectoryName (fileName) + Path.DirectorySeparatorChar + ".#" + Path.GetFileName (fileName);
+ }
+
+ //From MonoDevelop.Core.FileService
+ public static void SystemRename (string sourceFile, string destFile)
+ {
+ //FIXME: use the atomic System.IO.File.Replace on NTFS
+ if (OS.IsWindows) {
+ string wtmp = null;
+ if (File.Exists (destFile)) {
+ do {
+ wtmp = Path.Combine (Path.GetTempPath (), Guid.NewGuid ().ToString ());
+ } while (File.Exists (wtmp));
+
+ File.Move (destFile, wtmp);
+ }
+ try {
+ File.Move (sourceFile, destFile);
+ }
+ catch {
+ try {
+ if (wtmp != null)
+ File.Move (wtmp, destFile);
+ }
+ catch {
+ wtmp = null;
+ }
+ throw;
+ }
+ finally {
+ if (wtmp != null) {
+ try {
+ File.Delete (wtmp);
+ }
+ catch { }
+ }
+ }
+ }
+ else {
+ rename (sourceFile, destFile);
+ }
+ }
+
+ [DllImport ("libc", SetLastError=true)]
+ static extern int rename (string old, string @new);
+ }
+}
+
diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Tests/AndroidAppManifestTests.cs b/src/Xamarin.Android.Tools.AndroidSdk/Tests/AndroidAppManifestTests.cs
new file mode 100644
index 0000000..cefd8e3
--- /dev/null
+++ b/src/Xamarin.Android.Tools.AndroidSdk/Tests/AndroidAppManifestTests.cs
@@ -0,0 +1,166 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Linq;
+using System.Text;
+using System.Xml;
+using System.Xml.Linq;
+
+using NUnit.Framework;
+
+namespace Xamarin.Android.Tools.Tests
+{
+ [TestFixture]
+ public class AndroidAppManifestTests
+ {
+ [Test]
+ public void Load ()
+ {
+ var versions = new AndroidVersions (new AndroidVersion [0]);
+ Assert.Throws (() => AndroidAppManifest.Load ((string) null, versions));
+ Assert.Throws (() => AndroidAppManifest.Load ("filename", null));
+ Assert.Throws (() => AndroidAppManifest.Load ((XDocument) null, versions));
+ Assert.Throws (() => AndroidAppManifest.Load (GetTestAppManifest (), versions));
+
+ Assert.Throws (() => AndroidAppManifest.Load (XDocument.Parse (""), versions));
+ }
+
+ [Test]
+ public void ParsePermissions ()
+ {
+ var versions = new AndroidVersions (new AndroidVersion [0]);
+ var manifest = AndroidAppManifest.Load (GetTestAppManifest (), versions);
+ var permissions = manifest.AndroidPermissions.ToArray ();
+ Assert.AreEqual (3, permissions.Length, "#1");
+ Assert.IsTrue (permissions.Contains ("INTERNET"), "#2");
+ Assert.IsTrue (permissions.Contains ("READ_CONTACTS"), "#3");
+ Assert.IsTrue (permissions.Contains ("WRITE_CONTACTS"), "#4");
+ }
+
+ static XDocument GetTestAppManifest ()
+ {
+ using (var xml = typeof (AndroidAppManifestTests).Assembly.GetManifestResourceStream ("manifest-simplewidget.xml")) {
+ return XDocument.Load (xml);
+ }
+ }
+
+ [Test]
+ public void SetNewPermissions ()
+ {
+ var versions = new AndroidVersions (new AndroidVersion [0]);
+ var manifest = AndroidAppManifest.Load (GetTestAppManifest (), versions);
+ manifest.SetAndroidPermissions (new [] { "FOO" });
+
+ var sb = new StringBuilder ();
+ using (var writer = XmlWriter.Create (sb)) {
+ manifest.Write (writer);
+ }
+
+ manifest = AndroidAppManifest.Load (XDocument.Parse (sb.ToString ()), versions);
+ Assert.AreEqual (1, manifest.AndroidPermissions.Count (), "#1");
+ Assert.AreEqual ("FOO", manifest.AndroidPermissions.ElementAt (0));
+ }
+
+ [Test]
+ public void CanonicalizePackageName ()
+ {
+ Assert.Throws(() => AndroidAppManifest.CanonicalizePackageName (null));
+ Assert.Throws(() => AndroidAppManifest.CanonicalizePackageName (""));
+ Assert.Throws(() => AndroidAppManifest.CanonicalizePackageName (" "));
+
+ Assert.AreEqual ("A.A",
+ AndroidAppManifest.CanonicalizePackageName ("A"));
+ Assert.AreEqual ("Foo.Bar",
+ AndroidAppManifest.CanonicalizePackageName ("Foo.Bar"));
+ Assert.AreEqual ("foo_bar.foo_bar",
+ AndroidAppManifest.CanonicalizePackageName ("foo-bar"));
+ Assert.AreEqual ("x1.x1",
+ AndroidAppManifest.CanonicalizePackageName ("1"));
+ Assert.AreEqual ("x_1.x_2",
+ AndroidAppManifest.CanonicalizePackageName ("_1._2"));
+ Assert.AreEqual ("mfa1.x0.x2_2",
+ AndroidAppManifest.CanonicalizePackageName ("mfa1.0.2_2"));
+ Assert.AreEqual ("My.Cool_Assembly",
+ AndroidAppManifest.CanonicalizePackageName ("My.Cool Assembly"));
+ Assert.AreEqual ("x7Cats.x7Cats",
+ AndroidAppManifest.CanonicalizePackageName ("7Cats"));
+ }
+
+ [Test]
+ public void CanParseNonNumericSdkVersion ()
+ {
+ var versions = new AndroidVersions (new AndroidVersion [0]);
+ var doc = XDocument.Parse (@"
+
+
+
+
+ ");
+ var manifest = AndroidAppManifest.Load (doc, versions);
+
+ var mininum = manifest.MinSdkVersion;
+ var target = manifest.TargetSdkVersion;
+
+ Assert.IsTrue (mininum.HasValue);
+ Assert.IsTrue (target.HasValue);
+ Assert.AreEqual (21, mininum.Value);
+ Assert.AreEqual (21, target.Value);
+ }
+
+ [Test]
+ public void EnsureMinAndTargetSdkVersionsAreReadIndependently ()
+ {
+ // Regression test for https://bugzilla.xamarin.com/show_bug.cgi?id=21296
+ var versions = new AndroidVersions (new AndroidVersion [0]);
+ var doc = XDocument.Parse (@"
+
+
+
+
+ ");
+ var manifest = AndroidAppManifest.Load (doc, versions);
+
+ var mininum = manifest.MinSdkVersion;
+ var target = manifest.TargetSdkVersion;
+
+ Assert.IsTrue (mininum.HasValue);
+ Assert.IsTrue (target.HasValue);
+ Assert.AreEqual (8, mininum.Value);
+ Assert.AreEqual (12, target.Value);
+ }
+
+ [Test]
+ public void EnsureUsesPermissionElementOrder ()
+ {
+ var versions = new AndroidVersions (new AndroidVersion [0]);
+ var manifest = AndroidAppManifest.Create ("com.xamarin.test", "Xamarin Test", versions);
+ manifest.SetAndroidPermissions (new string[] { "FOO" });
+ var sb = new StringBuilder ();
+ using (var writer = XmlWriter.Create (sb)) {
+ manifest.Write (writer);
+ }
+
+ var doc = XDocument.Parse (sb.ToString ());
+ var app = doc.Element ("manifest").Element ("application");
+ Assert.IsNotNull (app, "Application element should exist");
+ Assert.IsFalse (app.ElementsAfterSelf ().Any (x => x.Name == "uses-permission"));
+ Assert.IsTrue (app.ElementsBeforeSelf ().Any (x => x.Name == "uses-permission"));
+ }
+
+ [Test]
+ public void CanGetAppTheme ()
+ {
+ var versions = new AndroidVersions (new AndroidVersion [0]);
+ var doc = XDocument.Parse (@"
+
+
+
+
+ ");
+ var manifest = AndroidAppManifest.Load (doc, versions);
+
+ Assert.AreEqual ("@android:style/Theme.Material.Light", manifest.ApplicationTheme);
+ }
+ }
+}
diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Tests/Resources/manifest-simplewidget.xml b/src/Xamarin.Android.Tools.AndroidSdk/Tests/Resources/manifest-simplewidget.xml
new file mode 100644
index 0000000..966639d
--- /dev/null
+++ b/src/Xamarin.Android.Tools.AndroidSdk/Tests/Resources/manifest-simplewidget.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj b/src/Xamarin.Android.Tools.AndroidSdk/Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj
index 9e65e90..3c56d14 100644
--- a/src/Xamarin.Android.Tools.AndroidSdk/Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj
+++ b/src/Xamarin.Android.Tools.AndroidSdk/Tests/Xamarin.Android.Tools.AndroidSdk-Tests.csproj
@@ -45,6 +45,7 @@
+
@@ -58,5 +59,10 @@
+
+
+ manifest-simplewidget.xml
+
+
\ No newline at end of file
diff --git a/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj b/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj
index d951470..dacb3b1 100644
--- a/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj
+++ b/src/Xamarin.Android.Tools.AndroidSdk/Xamarin.Android.Tools.AndroidSdk.csproj
@@ -45,6 +45,8 @@
+
+