/* * Copyright 2019 Google LLC * * 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. */ #if UNITY_EDITOR && UNITY_IOS using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using UnityEngine; using UnityEditor; using UnityEditor.Callbacks; using UnityEditor.iOS.Xcode; namespace Firebase.Editor { // This class is responisble for handling modifications to XCode projects post generation. internal class XcodeProjectModifier { /// /// Class instance of the raw Xcode project file /// /// /// The object's type is PBXProject. We don't expose this in the interface so this can be /// loaded without referencing the UnityEditor.iOS.Xcode module. /// internal object Project { get; private set; } /// /// Unity target guid in the XCode project /// internal string TargetGUID { get; private set; } /// /// Info.plist in the project directory /// /// /// The object's type is PlistElementDict. We don't expose this in the interface so this can be /// loaded without referencing the UnityEditor.iOS.Xcode module. /// internal object ProjectInfo { get { return ((PlistDocument)projectInfoDoc).root; } } /// /// Capabilities plist in the project directory /// /// /// The object's type is PlistElementDict. We don't expose this in the interface so this can be /// loaded without referencing the UnityEditor.iOS.Xcode module. /// internal object Capabilities { get { return ((PlistDocument)capabilitiesDoc).root; } } private object projectInfoDoc; private object capabilitiesDoc; private static readonly string projectInfoPListFile = "Info.plist"; private static readonly string capabilitiesPListFile = "Unity-iPhone/dev.entitlements"; private static readonly CategoryLogger logger = new CategoryLogger("XcodeProjectModifier"); private readonly string xcodeProjDir; /// /// Constructs a XcodeProjectModifier that handles loading a Xcode project and realted plists, /// allowing modifications on the objects and saving them back to disk. /// /// Xcode project directory internal XcodeProjectModifier(string xcodeProjDir) { logger.LogDebug("Loading Xcode project '{0}'.", xcodeProjDir); this.xcodeProjDir = xcodeProjDir; var projectPath = PBXProject.GetPBXProjectPath(xcodeProjDir); var project = new PBXProject(); Project = project; project.ReadFromString(File.ReadAllText(projectPath)); // TargetGuidByName & GetUnityTargetName is deprecated after 2019.3; use reflection to determine if we can call it // or need to use GetUnityMainTargetGuid. MethodInfo getUnityMainTargetGuid = project.GetType().GetMethod("GetUnityMainTargetGuid"); if (getUnityMainTargetGuid != null) { TargetGUID = (string) getUnityMainTargetGuid.Invoke(project, new object[] {}); } else { MethodInfo getUnityTargetName = project.GetType().GetMethod("GetUnityTargetName"); if (getUnityTargetName != null) { string targetName = (string)getUnityTargetName.Invoke(project, new object[] {}); TargetGUID = project.TargetGuidByName(targetName); } else { logger.LogError("Impossible Unity version, failed to get target guid."); } } projectInfoDoc = ReadPList(projectInfoPListFile); capabilitiesDoc = ReadPList(capabilitiesPListFile); } /// /// Save changes to Xcode project objects to disk. /// internal void Save() { logger.LogDebug("Saving Xcode project '{0}'.", xcodeProjDir); var projectPath = PBXProject.GetPBXProjectPath(xcodeProjDir); File.WriteAllText(projectPath, ((PBXProject)Project).WriteToString()); SavePList(projectInfoDoc, projectInfoPListFile); SavePList(capabilitiesDoc, capabilitiesPListFile); } // Map of Editor UI config types to set of callbacks private static Dictionary>> delegatesByType = new Dictionary>>(); /// /// Register a delegate to be called when the Xcode project is being post processed. This should /// be called by a static class constructor with the class having a [InitializeOnLoad] attribute. /// /// Type of Editor UI configuration class /// /// Callback to invoke at time of Xcode project post process. First argument is instance of /// XcodeProjectModifier and second argument is instance of T with its fields loaded from /// saved configuration. /// internal static void RegisterDelegate(Action callback) where T : class { Action internalCallback = delegate(XcodeProjectModifier a, System.Object o) { callback(a, o as T); }; List> listOfCallbacks = null; if (delegatesByType.TryGetValue(typeof(T), out listOfCallbacks) == false) { listOfCallbacks = new List>(); delegatesByType.Add(typeof(T), listOfCallbacks); } listOfCallbacks.Add(internalCallback); } /// /// Register a custom URL scheme in the Xcode project's Info.plist. Custom URL schemes are /// analagous to Android's intent filters and allow the app to be opened by other apps. /// /// /// The CFBundleURLName to associate with this scheme. /// /// /// The scheme to register. /// internal void AddCustomUrlScheme(string name, string scheme) { PlistElementDict root = (PlistElementDict) ProjectInfo; PlistElement elem; PlistElementArray types; if (root.values.TryGetValue("CFBundleURLTypes", out elem)) { types = (PlistElementArray) elem; } else { types = root.CreateArray("CFBundleURLTypes"); } if (ContainsUrlScheme(types, scheme)) { return; } PlistElementDict typeDict = types.AddDict(); typeDict.SetString("CFBundleURLName", name); PlistElementArray schemes = typeDict.CreateArray("CFBundleURLSchemes"); schemes.AddString(scheme); } // Helper method to determine if a given URL has been already registered in // the CFBundleURLTypes dict private static bool ContainsUrlScheme(object inputTypes, string scheme) { PlistElementArray types = (PlistElementArray)inputTypes; foreach (PlistElement typeElem in types.values) { PlistElementDict typeDict = typeElem.AsDict(); PlistElementArray schemes = typeDict["CFBundleURLSchemes"].AsArray(); foreach (PlistElement schemeElem in schemes.values) { if (string.Equals(schemeElem.AsString(), scheme)) { return true; } } } return false; } // This method is called by unity after it finishes project generation. It then loads the XCode // project and pLists files, creates class instances with methods having the XCodePostGen // Attribute, loads any Editor UI configuration object fields for those classes and then invokes // the methods. Once that is complete it saves the modifications back over the original files. [PostProcessBuild] internal static void PostProcessBuild(BuildTarget buildTarget, string pathToBuiltProject) { if (buildTarget != BuildTarget.iOS && buildTarget != BuildTarget.tvOS) { logger.LogDebug("Skipping PostProcessBuild as target {0} is not for iOS+", buildTarget); return; } if (delegatesByType.Count() == 0) { logger.LogDebug("Skipping PostProcessBuild as no delegates are registered"); return; } // Load Xcode project and plists var info = new XcodeProjectModifier(pathToBuiltProject); foreach (var t in delegatesByType) { // Create instance var classInstance = Activator.CreateInstance(t.Key); // Load any config fields in instance for easy access Utility.LoadConfig(classInstance); foreach (var callback in t.Value) { callback(info, classInstance); } } // Save changes to Xcode project and plists info.Save(); } // Utility function to read plist file from a path. private object ReadPList(string relFilePath) { var plistPath = Path.Combine(xcodeProjDir, relFilePath); var doc = new PlistDocument(); if (File.Exists(plistPath)) { doc.ReadFromString(File.ReadAllText(plistPath)); } return doc; } // Utility function to save plist file to a path private void SavePList(object doc, string relFilePath) { var plistPath = Path.Combine(xcodeProjDir, relFilePath); File.WriteAllText(plistPath, ((PlistDocument)doc).WriteToString()); } } } #endif