//
// Copyright (C) 2014 Google Inc. All Rights Reserved.
//
// 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.
//
// Keep this even on unsupported configurations.
namespace GooglePlayGames.Editor
{
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Xml;
using UnityEditor;
using UnityEngine;
///
/// Utility class to perform various tasks in the editor.
///
public static class GPGSUtil
{
/// Property key for project settings.
public const string SERVICEIDKEY = "App.NearbdServiceId";
/// Property key for project settings.
public const string APPIDKEY = "proj.AppId";
/// Property key for project settings.
public const string CLASSDIRECTORYKEY = "proj.classDir";
/// Property key for project settings.
public const string CLASSNAMEKEY = "proj.ConstantsClassName";
/// Property key for project settings.
public const string WEBCLIENTIDKEY = "and.ClientId";
/// Property key for project settings.
public const string ANDROIDRESOURCEKEY = "and.ResourceData";
/// Property key for project settings.
public const string ANDROIDSETUPDONEKEY = "android.SetupDone";
/// Property key for project settings.
public const string ANDROIDBUNDLEIDKEY = "and.BundleId";
/// Property key for plugin version.
public const string PLUGINVERSIONKEY = "proj.pluginVersion";
/// Property key for nearby settings done.
public const string NEARBYSETUPDONEKEY = "android.NearbySetupDone";
/// Property key for project settings.
public const string LASTUPGRADEKEY = "lastUpgrade";
/// Constant for token replacement
private const string SERVICEIDPLACEHOLDER = "__NEARBY_SERVICE_ID__";
private const string SERVICEID_ELEMENT_PLACEHOLDER = "__NEARBY_SERVICE_ELEMENT__";
private const string NEARBY_PERMISSIONS_PLACEHOLDER = "__NEARBY_PERMISSIONS__";
/// Constant for token replacement
private const string APPIDPLACEHOLDER = "__APP_ID__";
/// Constant for token replacement
private const string CLASSNAMEPLACEHOLDER = "__Class__";
/// Constant for token replacement
private const string WEBCLIENTIDPLACEHOLDER = "__WEB_CLIENTID__";
/// Constant for token replacement
private const string PLUGINVERSIONPLACEHOLDER = "__PLUGIN_VERSION__";
/// Constant for require google plus token replacement
private const string REQUIREGOOGLEPLUSPLACEHOLDER = "__REQUIRE_GOOGLE_PLUS__";
/// Property key for project settings.
private const string TOKENPERMISSIONKEY = "proj.tokenPermissions";
/// Constant for token replacement
private const string NAMESPACESTARTPLACEHOLDER = "__NameSpaceStart__";
/// Constant for token replacement
private const string NAMESPACEENDPLACEHOLDER = "__NameSpaceEnd__";
/// Constant for token replacement
private const string CONSTANTSPLACEHOLDER = "__Constant_Properties__";
///
/// The game info file path. This is a generated file.
///
private const string GameInfoPath = "Assets/GooglePlayGames/GameInfo.cs";
///
/// The manifest path.
///
/// The Games SDK requires additional metadata in the AndroidManifest.xml
/// file.
private const string ManifestPath =
"Assets/GooglePlayGames/Plugins/Android/GooglePlayGamesManifest.plugin/AndroidManifest.xml";
///
/// The map of replacements for filling in code templates. The
/// key is the string that appears in the template as a placeholder,
/// the value is the key into the GPGSProjectSettings.
///
private static Dictionary replacements =
new Dictionary()
{
// Put this element placeholder first, since it has embedded placeholder
{SERVICEID_ELEMENT_PLACEHOLDER, SERVICEID_ELEMENT_PLACEHOLDER},
{ SERVICEIDPLACEHOLDER, SERVICEIDKEY },
{ APPIDPLACEHOLDER, APPIDKEY },
{ CLASSNAMEPLACEHOLDER, CLASSNAMEKEY },
{ WEBCLIENTIDPLACEHOLDER, WEBCLIENTIDKEY },
{ PLUGINVERSIONPLACEHOLDER, PLUGINVERSIONKEY},
// Causes the placeholder to be replaced with overridden value at runtime.
{ NEARBY_PERMISSIONS_PLACEHOLDER, NEARBY_PERMISSIONS_PLACEHOLDER}
};
///
/// Replaces / in file path to be the os specific separator.
///
/// The path.
/// Path with correct separators.
public static string SlashesToPlatformSeparator(string path)
{
return path.Replace("/", System.IO.Path.DirectorySeparatorChar.ToString());
}
///
/// Reads the file.
///
/// The file contents. The slashes are corrected.
/// File path.
public static string ReadFile(string filePath)
{
filePath = SlashesToPlatformSeparator(filePath);
if (!File.Exists(filePath))
{
Alert("Plugin error: file not found: " + filePath);
return null;
}
StreamReader sr = new StreamReader(filePath);
string body = sr.ReadToEnd();
sr.Close();
return body;
}
///
/// Reads the editor template.
///
/// The editor template contents.
/// Name of the template in the editor directory.
public static string ReadEditorTemplate(string name)
{
return ReadFile(SlashesToPlatformSeparator("Assets/GooglePlayGames/Editor/" + name + ".txt"));
}
///
/// Writes the file.
///
/// File path - the slashes will be corrected.
/// Body of the file to write.
public static void WriteFile(string file, string body)
{
file = SlashesToPlatformSeparator(file);
using (var wr = new StreamWriter(file, false))
{
wr.Write(body);
}
}
///
/// Validates the string to be a valid nearby service id.
///
/// true, if like valid service identifier was looksed, false otherwise.
/// string to test.
public static bool LooksLikeValidServiceId(string s)
{
if (s.Length < 3)
{
return false;
}
foreach (char c in s)
{
if (!char.IsLetterOrDigit(c) && c != '.')
{
return false;
}
}
return true;
}
///
/// Looks the like valid app identifier.
///
/// true, if valid app identifier, false otherwise.
/// the string to test.
public static bool LooksLikeValidAppId(string s)
{
if (s.Length < 5)
{
return false;
}
foreach (char c in s)
{
if (c < '0' || c > '9')
{
return false;
}
}
return true;
}
///
/// Looks the like valid client identifier.
///
/// true, if valid client identifier, false otherwise.
/// the string to test.
public static bool LooksLikeValidClientId(string s)
{
return s.EndsWith(".googleusercontent.com");
}
///
/// Looks the like a valid bundle identifier.
///
/// true, if valid bundle identifier, false otherwise.
/// the string to test.
public static bool LooksLikeValidBundleId(string s)
{
return s.Length > 3;
}
///
/// Looks like a valid package.
///
/// true, if valid package name, false otherwise.
/// the string to test.
public static bool LooksLikeValidPackageName(string s)
{
if (string.IsNullOrEmpty(s))
{
throw new Exception("cannot be empty");
}
string[] parts = s.Split(new char[] { '.' });
foreach (string p in parts)
{
char[] bytes = p.ToCharArray();
for (int i = 0; i < bytes.Length; i++)
{
if (i == 0 && !char.IsLetter(bytes[i]))
{
throw new Exception("each part must start with a letter");
}
else if (char.IsWhiteSpace(bytes[i]))
{
throw new Exception("cannot contain spaces");
}
else if (!char.IsLetterOrDigit(bytes[i]) && bytes[i] != '_')
{
throw new Exception("must be alphanumeric or _");
}
}
}
return parts.Length >= 1;
}
///
/// Determines if is setup done.
///
/// true if is setup done; otherwise, false.
public static bool IsSetupDone()
{
bool doneSetup = true;
#if UNITY_ANDROID
doneSetup = GPGSProjectSettings.Instance.GetBool(ANDROIDSETUPDONEKEY, false);
// check gameinfo
if (File.Exists(GameInfoPath))
{
string contents = ReadFile(GameInfoPath);
if (contents.Contains(APPIDPLACEHOLDER))
{
Debug.Log("GameInfo not initialized with AppId. " +
"Run Window > Google Play Games > Setup > Android Setup...");
return false;
}
}
else
{
Debug.Log("GameInfo.cs does not exist. Run Window > Google Play Games > Setup > Android Setup...");
return false;
}
#endif
return doneSetup;
}
///
/// Makes legal identifier from string.
/// Returns a legal C# identifier from the given string. The transformations are:
/// - spaces => underscore _
/// - punctuation => empty string
/// - leading numbers are prefixed with underscore.
///
/// the id
/// Key to convert to an identifier.
public static string MakeIdentifier(string key)
{
string s;
string retval = string.Empty;
if (string.IsNullOrEmpty(key))
{
return "_";
}
s = key.Trim().Replace(' ', '_');
foreach (char c in s)
{
if (char.IsLetterOrDigit(c) || c == '_')
{
retval += c;
}
}
return retval;
}
///
/// Displays an error dialog.
///
/// the message
public static void Alert(string s)
{
Alert(GPGSStrings.Error, s);
}
///
/// Displays a dialog with the given title and message.
///
/// the title.
/// the message.
public static void Alert(string title, string message)
{
EditorUtility.DisplayDialog(title, message, GPGSStrings.Ok);
}
///
/// Gets the android sdk path.
///
/// The android sdk path.
public static string GetAndroidSdkPath()
{
string sdkPath = EditorPrefs.GetString("AndroidSdkRoot");
if (sdkPath != null && (sdkPath.EndsWith("/") || sdkPath.EndsWith("\\")))
{
sdkPath = sdkPath.Substring(0, sdkPath.Length - 1);
}
return sdkPath;
}
///
/// Determines if the android sdk exists.
///
/// true if android sdk exists; otherwise, false.
public static bool HasAndroidSdk()
{
string sdkPath = GetAndroidSdkPath();
return sdkPath != null && sdkPath.Trim() != string.Empty && System.IO.Directory.Exists(sdkPath);
}
///
/// Gets the unity major version.
///
/// The unity major version.
public static int GetUnityMajorVersion()
{
#if UNITY_5
string majorVersion = Application.unityVersion.Split('.')[0];
int ver;
if (!int.TryParse(majorVersion, out ver))
{
ver = 0;
}
return ver;
#elif UNITY_4_6
return 4;
#else
return 0;
#endif
}
///
/// Checks for the android manifest file exsistance.
///
/// true, if the file exists false otherwise.
public static bool AndroidManifestExists()
{
string destFilename = GPGSUtil.SlashesToPlatformSeparator(ManifestPath);
return File.Exists(destFilename);
}
///
/// Generates the android manifest.
///
public static void GenerateAndroidManifest()
{
string destFilename = GPGSUtil.SlashesToPlatformSeparator(ManifestPath);
// Generate AndroidManifest.xml
string manifestBody = GPGSUtil.ReadEditorTemplate("template-AndroidManifest");
Dictionary overrideValues =
new Dictionary();
if (!string.IsNullOrEmpty (GPGSProjectSettings.Instance.Get (SERVICEIDKEY)))
{
overrideValues [NEARBY_PERMISSIONS_PLACEHOLDER] =
" \n" +
" \n" +
" \n" +
" \n" +
" \n" +
" \n";
overrideValues [SERVICEID_ELEMENT_PLACEHOLDER] =
" \n" +
" \n";
}
else
{
overrideValues [NEARBY_PERMISSIONS_PLACEHOLDER] = "";
overrideValues [SERVICEID_ELEMENT_PLACEHOLDER] = "";
}
foreach (KeyValuePair ent in replacements)
{
string value =
GPGSProjectSettings.Instance.Get(ent.Value, overrideValues);
manifestBody = manifestBody.Replace(ent.Key, value);
}
GPGSUtil.WriteFile(destFilename, manifestBody);
GPGSUtil.UpdateGameInfo();
}
///
/// Writes the resource identifiers file. This file contains the
/// resource ids copied (downloaded?) from the play game app console.
///
/// Class directory.
/// Class name.
/// Resource keys.
public static void WriteResourceIds(string classDirectory, string className, Hashtable resourceKeys)
{
string constantsValues = string.Empty;
string[] parts = className.Split('.');
string dirName = classDirectory;
if (string.IsNullOrEmpty(dirName))
{
dirName = "Assets";
}
string nameSpace = string.Empty;
for (int i = 0; i < parts.Length - 1; i++)
{
dirName += "/" + parts[i];
if (nameSpace != string.Empty)
{
nameSpace += ".";
}
nameSpace += parts[i];
}
EnsureDirExists(dirName);
foreach (DictionaryEntry ent in resourceKeys)
{
string key = MakeIdentifier((string)ent.Key);
constantsValues += " public const string " +
key + " = \"" + ent.Value + "\"; // \n";
}
string fileBody = GPGSUtil.ReadEditorTemplate("template-Constants");
if (nameSpace != string.Empty)
{
fileBody = fileBody.Replace(
NAMESPACESTARTPLACEHOLDER,
"namespace " + nameSpace + "\n{");
}
else
{
fileBody = fileBody.Replace(NAMESPACESTARTPLACEHOLDER, string.Empty);
}
fileBody = fileBody.Replace(CLASSNAMEPLACEHOLDER, parts[parts.Length - 1]);
fileBody = fileBody.Replace(CONSTANTSPLACEHOLDER, constantsValues);
if (nameSpace != string.Empty)
{
fileBody = fileBody.Replace(
NAMESPACEENDPLACEHOLDER,
"}");
}
else
{
fileBody = fileBody.Replace(NAMESPACEENDPLACEHOLDER, string.Empty);
}
WriteFile(Path.Combine(dirName, parts[parts.Length - 1] + ".cs"), fileBody);
}
///
/// Updates the game info file. This is a generated file containing the
/// app and client ids.
///
public static void UpdateGameInfo()
{
string fileBody = GPGSUtil.ReadEditorTemplate("template-GameInfo");
foreach (KeyValuePair ent in replacements)
{
string value =
GPGSProjectSettings.Instance.Get(ent.Value);
fileBody = fileBody.Replace(ent.Key, value);
}
GPGSUtil.WriteFile(GameInfoPath, fileBody);
}
///
/// Ensures the dir exists.
///
/// Directory to check.
public static void EnsureDirExists(string dir)
{
dir = SlashesToPlatformSeparator(dir);
if (!Directory.Exists(dir))
{
Directory.CreateDirectory(dir);
}
}
///
/// Deletes the dir if exists.
///
/// Directory to delete.
public static void DeleteDirIfExists(string dir)
{
dir = SlashesToPlatformSeparator(dir);
if (Directory.Exists(dir))
{
Directory.Delete(dir, true);
}
}
///
/// Gets the Google Play Services library version. This is only
/// needed for Unity versions less than 5.
///
/// The GPS version.
/// Lib proj path.
private static int GetGPSVersion(string libProjPath)
{
string versionFile = libProjPath + "/res/values/version.xml";
XmlTextReader reader = new XmlTextReader(new StreamReader(versionFile));
bool inResource = false;
int version = -1;
while (reader.Read())
{
if (reader.Name == "resources")
{
inResource = true;
}
if (inResource && reader.Name == "integer")
{
if ("google_play_services_version".Equals(
reader.GetAttribute("name")))
{
reader.Read();
Debug.Log("Read version string: " + reader.Value);
version = Convert.ToInt32(reader.Value);
}
}
}
reader.Close();
return version;
}
}
}