using System; using System.IO; namespace ImaginationOverflow.UniversalDeepLinking.Editor.Xcode { // This class is here to help you add capabilities to your Xcode project. // Because capabilities modify the PBXProject, the entitlements file and/or the Info.plist and not consistently, // it can be tedious. // Therefore this class open the PBXProject that is always modify by capabilities and open Entitlement and info.plist only when needed. // For optimisation reasons, we write the file only in the close method. // If you don't call it the file will not be written. public class ProjectCapabilityManager { private readonly string m_BuildPath; private readonly string m_TargetGuid; private readonly string m_PBXProjectPath; private readonly string m_EntitlementFilePath; private PlistDocument m_Entitlements; private PlistDocument m_InfoPlist; protected internal PBXProject project; // Create the manager with the required parameter to open files and set the properties in the write place. public ProjectCapabilityManager(string pbxProjectPath, string entitlementFilePath, string targetName) { m_BuildPath = Directory.GetParent(Path.GetDirectoryName(pbxProjectPath)).FullName; m_EntitlementFilePath = entitlementFilePath; m_PBXProjectPath = pbxProjectPath; project = new PBXProject(); project.ReadFromString(File.ReadAllText(m_PBXProjectPath)); m_TargetGuid = project.TargetGuidByName(targetName); } // Write the actual file to the disk. // If you don't call this method nothing will change. public void WriteToFile() { File.WriteAllText(m_PBXProjectPath, project.WriteToString()); if (m_Entitlements != null) m_Entitlements.WriteToFile(PBXPath.Combine(m_BuildPath, m_EntitlementFilePath)); if (m_InfoPlist != null) m_InfoPlist.WriteToFile(PBXPath.Combine(m_BuildPath, "Info.plist")); } // Add the iCloud capability with the desired options. public void AddiCloud(bool keyValueStorage, bool iCloudDocument, string[] customContainers) { var ent = GetOrCreateEntitlementDoc(); var val = (ent.root[ICloudEntitlements.ContainerIdValue] = new PlistElementArray()) as PlistElementArray; if (iCloudDocument) { val.values.Add(new PlistElementString(ICloudEntitlements.ContainerIdValue)); var ser = (ent.root[ICloudEntitlements.ServicesKey] = new PlistElementArray()) as PlistElementArray; ser.values.Add(new PlistElementString(ICloudEntitlements.ServicesKitValue)); ser.values.Add(new PlistElementString(ICloudEntitlements.ServicesDocValue)); var ubiquity = (ent.root[ICloudEntitlements.UbiquityContainerIdKey] = new PlistElementArray()) as PlistElementArray; ubiquity.values.Add(new PlistElementString(ICloudEntitlements.UbiquityContainerIdValue)); for (var i = 0; i < customContainers.Length; i++) { ser.values.Add(new PlistElementString(customContainers[i])); } } if (keyValueStorage) { ent.root[ICloudEntitlements.KeyValueStoreKey] = new PlistElementString(ICloudEntitlements.KeyValueStoreValue); } project.AddCapability(m_TargetGuid, PBXCapabilityType.iCloud, m_EntitlementFilePath, iCloudDocument); } // Add Push (or remote) Notifications capability to your project public void AddPushNotifications(bool development) { GetOrCreateEntitlementDoc().root[PushNotificationEntitlements.Key] = new PlistElementString(development ? PushNotificationEntitlements.DevelopmentValue : PushNotificationEntitlements.ProductionValue); project.AddCapability(m_TargetGuid, PBXCapabilityType.PushNotifications, m_EntitlementFilePath); } // Add GameCenter capability to the project. public void AddGameCenter() { var arr = (GetOrCreateInfoDoc().root[GameCenterInfo.Key] ?? (GetOrCreateInfoDoc().root[GameCenterInfo.Key] = new PlistElementArray())) as PlistElementArray; arr.values.Add(new PlistElementString(GameCenterInfo.Value)); project.AddCapability(m_TargetGuid, PBXCapabilityType.GameCenter); } // Add Wallet capability to the project. public void AddWallet(string[] passSubset) { var arr = (GetOrCreateEntitlementDoc().root[WalletEntitlements.Key] = new PlistElementArray()) as PlistElementArray; if ((passSubset == null || passSubset.Length == 0) && arr != null) { arr.values.Add(new PlistElementString(WalletEntitlements.BaseValue + WalletEntitlements.BaseValue)); } else { for (var i = 0; i < passSubset.Length; i++) { if (arr != null) arr.values.Add(new PlistElementString(WalletEntitlements.BaseValue + passSubset[i])); } } project.AddCapability(m_TargetGuid, PBXCapabilityType.Wallet, m_EntitlementFilePath); } // Add Siri capability to the project. public void AddSiri() { GetOrCreateEntitlementDoc().root[SiriEntitlements.Key] = new PlistElementBoolean(true); project.AddCapability(m_TargetGuid, PBXCapabilityType.Siri, m_EntitlementFilePath); } // Add Apple Pay capability to the project. public void AddApplePay(string[] merchants) { var arr = (GetOrCreateEntitlementDoc().root[ApplePayEntitlements.Key] = new PlistElementArray()) as PlistElementArray; for (var i = 0; i < merchants.Length; i++) { arr.values.Add(new PlistElementString(merchants[i])); } project.AddCapability(m_TargetGuid, PBXCapabilityType.ApplePay, m_EntitlementFilePath); } // Add In App Purchase capability to the project. public void AddInAppPurchase() { project.AddCapability(m_TargetGuid, PBXCapabilityType.InAppPurchase); } // Add Maps capability to the project. public void AddMaps(MapsOptions options) { var bundleArr = (GetOrCreateInfoDoc().root[MapsInfo.BundleKey] ?? (GetOrCreateInfoDoc().root[MapsInfo.BundleKey] = new PlistElementArray())) as PlistElementArray; bundleArr.values.Add(new PlistElementDict()); PlistElementDict bundleDic = GetOrCreateUniqueDictElementInArray(bundleArr); bundleDic[MapsInfo.BundleNameKey] = new PlistElementString(MapsInfo.BundleNameValue); var bundleTypeArr = (bundleDic[MapsInfo.BundleTypeKey] ?? (bundleDic[MapsInfo.BundleTypeKey] = new PlistElementArray())) as PlistElementArray; GetOrCreateStringElementInArray(bundleTypeArr, MapsInfo.BundleTypeValue); var optionArr = (GetOrCreateInfoDoc().root[MapsInfo.ModeKey] ?? (GetOrCreateInfoDoc().root[MapsInfo.ModeKey] = new PlistElementArray())) as PlistElementArray; if ((options & MapsOptions.Airplane) == MapsOptions.Airplane) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModePlaneValue); } if ((options & MapsOptions.Bike) == MapsOptions.Bike) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeBikeValue); } if ((options & MapsOptions.Bus) == MapsOptions.Bus) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeBusValue); } if ((options & MapsOptions.Car) == MapsOptions.Car) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeCarValue); } if ((options & MapsOptions.Ferry) == MapsOptions.Ferry) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeFerryValue); } if ((options & MapsOptions.Other) == MapsOptions.Other) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeOtherValue); } if ((options & MapsOptions.Pedestrian) == MapsOptions.Pedestrian) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModePedestrianValue); } if ((options & MapsOptions.RideSharing) == MapsOptions.RideSharing) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeRideShareValue); } if ((options & MapsOptions.StreetCar) == MapsOptions.StreetCar) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeStreetCarValue); } if ((options & MapsOptions.Subway) == MapsOptions.Subway) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeSubwayValue); } if ((options & MapsOptions.Taxi) == MapsOptions.Taxi) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeTaxiValue); } if ((options & MapsOptions.Train) == MapsOptions.Train) { GetOrCreateStringElementInArray(optionArr, MapsInfo.ModeTrainValue); } project.AddCapability(m_TargetGuid, PBXCapabilityType.Maps); } // Add Personal VPN capability to the project. public void AddPersonalVPN() { var arr = (GetOrCreateEntitlementDoc().root[VPNEntitlements.Key] = new PlistElementArray()) as PlistElementArray; arr.values.Add(new PlistElementString(VPNEntitlements.Value)); project.AddCapability(m_TargetGuid, PBXCapabilityType.PersonalVPN, m_EntitlementFilePath); } // Add Background capability to the project with the options wanted. public void AddBackgroundModes(BackgroundModesOptions options) { var optionArr = (GetOrCreateInfoDoc().root[BackgroundInfo.Key] ?? (GetOrCreateInfoDoc().root[BackgroundInfo.Key] = new PlistElementArray())) as PlistElementArray; if ((options & BackgroundModesOptions.ActsAsABluetoothLEAccessory) == BackgroundModesOptions.ActsAsABluetoothLEAccessory) { GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeActsBluetoothValue); } if ((options & BackgroundModesOptions.AudioAirplayPiP) == BackgroundModesOptions.AudioAirplayPiP) { GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeAudioValue); } if ((options & BackgroundModesOptions.BackgroundFetch) == BackgroundModesOptions.BackgroundFetch) { GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeFetchValue); } if ((options & BackgroundModesOptions.ExternalAccessoryCommunication) == BackgroundModesOptions.ExternalAccessoryCommunication) { GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeExtAccessoryValue); } if ((options & BackgroundModesOptions.LocationUpdates) == BackgroundModesOptions.LocationUpdates) { GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeLocationValue); } if ((options & BackgroundModesOptions.NewsstandDownloads) == BackgroundModesOptions.NewsstandDownloads) { GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeNewsstandValue); } if ((options & BackgroundModesOptions.RemoteNotifications) == BackgroundModesOptions.RemoteNotifications) { GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModePushValue); } if ((options & BackgroundModesOptions.VoiceOverIP) == BackgroundModesOptions.VoiceOverIP) { GetOrCreateStringElementInArray(optionArr, BackgroundInfo.ModeVOIPValue); } project.AddCapability(m_TargetGuid, PBXCapabilityType.BackgroundModes); } // Add Keychain Sharing capability to the project with a list of groups. public void AddKeychainSharing(string[] accessGroups) { var arr = (GetOrCreateEntitlementDoc().root[KeyChainEntitlements.Key] = new PlistElementArray()) as PlistElementArray; if (accessGroups != null) { for (var i = 0; i < accessGroups.Length; i++) { arr.values.Add(new PlistElementString(accessGroups[i])); } } else { arr.values.Add(new PlistElementString(KeyChainEntitlements.DefaultValue)); } project.AddCapability(m_TargetGuid, PBXCapabilityType.KeychainSharing, m_EntitlementFilePath); } // Add Inter App Audio capability to the project. public void AddInterAppAudio() { GetOrCreateEntitlementDoc().root[AudioEntitlements.Key] = new PlistElementBoolean(true); project.AddCapability(m_TargetGuid, PBXCapabilityType.InterAppAudio, m_EntitlementFilePath); } // Add Associated Domains capability to the project. public void AddAssociatedDomains(string[] domains) { var arr = (GetOrCreateEntitlementDoc().root[AssociatedDomainsEntitlements.Key] = new PlistElementArray()) as PlistElementArray; for (var i = 0; i < domains.Length; i++) { arr.values.Add(new PlistElementString(domains[i])); } project.AddCapability(m_TargetGuid, PBXCapabilityType.AssociatedDomains, m_EntitlementFilePath); } // Add App Groups capability to the project. public void AddAppGroups(string[] groups) { var arr = (GetOrCreateEntitlementDoc().root[AppGroupsEntitlements.Key] = new PlistElementArray()) as PlistElementArray; for (var i = 0; i < groups.Length; i++) { arr.values.Add(new PlistElementString(groups[i])); } project.AddCapability(m_TargetGuid, PBXCapabilityType.AppGroups, m_EntitlementFilePath); } // Add HomeKit capability to the project. public void AddHomeKit() { GetOrCreateEntitlementDoc().root[HomeKitEntitlements.Key] = new PlistElementBoolean(true); project.AddCapability(m_TargetGuid, PBXCapabilityType.HomeKit, m_EntitlementFilePath); } // Add Data Protection capability to the project. public void AddDataProtection() { GetOrCreateEntitlementDoc().root[DataProtectionEntitlements.Key] = new PlistElementString(DataProtectionEntitlements.Value); project.AddCapability(m_TargetGuid, PBXCapabilityType.DataProtection, m_EntitlementFilePath); } // Add HealthKit capability to the project. public void AddHealthKit() { var capabilityArr = (GetOrCreateInfoDoc().root[HealthInfo.Key] ?? (GetOrCreateInfoDoc().root[HealthInfo.Key] = new PlistElementArray())) as PlistElementArray; GetOrCreateStringElementInArray(capabilityArr, HealthInfo.Value); GetOrCreateEntitlementDoc().root[HealthKitEntitlements.Key] = new PlistElementBoolean(true); project.AddCapability(m_TargetGuid, PBXCapabilityType.HealthKit, m_EntitlementFilePath); } // Add Wireless Accessory Configuration capability to the project. public void AddWirelessAccessoryConfiguration() { GetOrCreateEntitlementDoc().root[WirelessAccessoryConfigurationEntitlements.Key] = new PlistElementBoolean(true); project.AddCapability(m_TargetGuid, PBXCapabilityType.WirelessAccessoryConfiguration, m_EntitlementFilePath); } private PlistDocument GetOrCreateEntitlementDoc() { if (m_Entitlements == null) { m_Entitlements = new PlistDocument(); string[] entitlementsFiles = Directory.GetFiles(m_BuildPath, m_EntitlementFilePath); if (entitlementsFiles.Length > 0) { m_Entitlements.ReadFromFile(entitlementsFiles[0]); } else { m_Entitlements.Create(); } } return m_Entitlements; } private PlistDocument GetOrCreateInfoDoc() { if (m_InfoPlist == null) { m_InfoPlist = new PlistDocument(); string[] infoFiles = Directory.GetFiles(m_BuildPath + "/", "Info.plist"); if (infoFiles.Length > 0) { m_InfoPlist.ReadFromFile(infoFiles[0]); } else { m_InfoPlist.Create(); } } return m_InfoPlist; } private PlistElementString GetOrCreateStringElementInArray(PlistElementArray root, string value) { PlistElementString r = null; var c = root.values.Count; var exist = false; for (var i = 0; i < c; i++) { if (root.values[i] is PlistElementString && (root.values[i] as PlistElementString).value == value) { r = root.values[i] as PlistElementString; exist = true; } } if (!exist) { r = new PlistElementString(value); root.values.Add(r); } return r; } private PlistElementDict GetOrCreateUniqueDictElementInArray(PlistElementArray root) { PlistElementDict r; if (root.values.Count == 0) { r = root.values[0] as PlistElementDict; } else { r = new PlistElementDict(); root.values.Add(r); } return r; } } // The list of options available for Background Mode. [Flags] [Serializable] public enum BackgroundModesOptions { None = 0, AudioAirplayPiP = 1<<0, LocationUpdates = 1<<1, VoiceOverIP = 1<<2, NewsstandDownloads = 1<<3, ExternalAccessoryCommunication = 1<<4, UsesBluetoothLEAccessory = 1<<5, ActsAsABluetoothLEAccessory = 1<<6, BackgroundFetch = 1<<7, RemoteNotifications = 1<<8 } // The list of options available for Maps. [Serializable] [Flags] public enum MapsOptions { None = 0, Airplane = 1<<0, Bike = 1<<1, Bus = 1<<2, Car = 1<<3, Ferry = 1<<4, Pedestrian = 1<<5, RideSharing = 1<<6, StreetCar = 1<<7, Subway = 1<<8, Taxi = 1<<9, Train = 1<<10, Other = 1<<11 } /* Follows the large quantity of string used as key and value all over the place in the info.plist or entitlements file. */ internal class GameCenterInfo { internal static readonly string Key = "UIRequiredDeviceCapabilities"; internal static readonly string Value = "gamekit"; } internal class MapsInfo { internal static readonly string BundleKey = "CFBundleDocumentTypes"; internal static readonly string BundleNameKey = "CFBundleTypeName"; internal static readonly string BundleNameValue = "MKDirectionsRequest"; internal static readonly string BundleTypeKey = "LSItemContentTypes"; internal static readonly string BundleTypeValue = "com.apple.maps.directionsrequest"; internal static readonly string ModeKey = "MKDirectionsApplicationSupportedModes"; internal static readonly string ModePlaneValue = "MKDirectionsModePlane"; internal static readonly string ModeBikeValue = "MKDirectionsModeBike"; internal static readonly string ModeBusValue = "MKDirectionsModeBus"; internal static readonly string ModeCarValue = "MKDirectionsModeCar"; internal static readonly string ModeFerryValue = "MKDirectionsModeFerry"; internal static readonly string ModeOtherValue = "MKDirectionsModeOther"; internal static readonly string ModePedestrianValue = "MKDirectionsModePedestrian"; internal static readonly string ModeRideShareValue = "MKDirectionsModeRideShare"; internal static readonly string ModeStreetCarValue = "MKDirectionsModeStreetCar"; internal static readonly string ModeSubwayValue = "MKDirectionsModeSubway"; internal static readonly string ModeTaxiValue = "MKDirectionsModeTaxi"; internal static readonly string ModeTrainValue = "MKDirectionsModeTrain"; } internal class BackgroundInfo { internal static readonly string Key = "UIBackgroundModes"; internal static readonly string ModeAudioValue = "audio"; internal static readonly string ModeBluetoothValue = "bluetooth-central"; internal static readonly string ModeActsBluetoothValue = "bluetooth-peripheral"; internal static readonly string ModeExtAccessoryValue = "external-accessory"; internal static readonly string ModeFetchValue = "fetch"; internal static readonly string ModeLocationValue = "location"; internal static readonly string ModeNewsstandValue = "newsstand-content"; internal static readonly string ModePushValue = "remote-notification"; internal static readonly string ModeVOIPValue = "voip"; } internal class HealthInfo { internal static readonly string Key = "UIRequiredDeviceCapabilities"; internal static readonly string Value = "healthkit"; } internal class ICloudEntitlements { internal static readonly string ContainerIdKey = "com.apple.developer.icloud-container-identifiers"; internal static readonly string UbiquityContainerIdKey = "com.apple.developer.ubiquity-container-identifiers"; internal static readonly string ContainerIdValue = "iCloud.$(CFBundleIdentifier)"; internal static readonly string UbiquityContainerIdValue = "iCloud.$(CFBundleIdentifier)"; internal static readonly string ServicesKey = "com.apple.developer.icloud-services"; internal static readonly string ServicesDocValue = "CloudDocuments"; internal static readonly string ServicesKitValue = "CloudKit"; internal static readonly string KeyValueStoreKey = "com.apple.developer.ubiquity-kvstore-identifier"; internal static readonly string KeyValueStoreValue = "$(TeamIdentifierPrefix)$(CFBundleIdentifier)"; } internal class PushNotificationEntitlements { internal static readonly string Key = "aps-environment"; internal static readonly string DevelopmentValue = "development"; internal static readonly string ProductionValue = "production"; } internal class WalletEntitlements { internal static readonly string Key = "com.apple.developer.pass-type-identifiers"; internal static readonly string BaseValue = "$(TeamIdentifierPrefix)"; internal static readonly string DefaultValue = "*"; } internal class SiriEntitlements { internal static readonly string Key = "com.apple.developer.siri"; } internal class ApplePayEntitlements { internal static readonly string Key = "com.apple.developer.in-app-payments"; } internal class VPNEntitlements { internal static readonly string Key = "com.apple.developer.networking.vpn.api"; internal static readonly string Value = "allow-vpn"; } internal class KeyChainEntitlements { internal static readonly string Key = "keychain-access-groups"; internal static readonly string DefaultValue = "$(AppIdentifierPrefix)$(CFBundleIdentifier)"; } internal class AudioEntitlements { internal static readonly string Key = "inter-app-audio"; } internal class AssociatedDomainsEntitlements { // value is an array of string of domains internal static readonly string Key = "com.apple.developer.associated-domains"; } internal class AppGroupsEntitlements { // value is an array of string of groups internal static readonly string Key = "com.apple.security.application-groups"; } internal class HomeKitEntitlements { // value is bool true. internal static readonly string Key = "com.apple.developer.homekit"; } internal class DataProtectionEntitlements { internal static readonly string Key = "com.apple.developer.default-data-protection"; internal static readonly string Value = "NSFileProtectionComplete"; } internal class HealthKitEntitlements { // value is bool true. internal static readonly string Key = "com.apple.developer.healthkit"; } internal class WirelessAccessoryConfigurationEntitlements { // value is bool true. internal static readonly string Key = "com.apple.external-accessory.wireless-configuration"; } }