/*
* 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