// Copyright 1998-2017 Epic Games, Inc. All Rights Reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Diagnostics;
using System.IO;
using Microsoft.Win32;
using System.Xml.Linq;
namespace UnrealBuildTool
{
class UEDeployAndroid : UEBuildDeploy, IAndroidDeploy
{
///
/// Internal usage for GetApiLevel
///
private List PossibleApiLevels = null;
private FileReference ProjectFile;
public UEDeployAndroid(FileReference InProjectFile)
{
ProjectFile = InProjectFile;
}
private UnrealPluginLanguage UPL = null;
public void SetAndroidPluginData(List Architectures, List inPluginExtraData)
{
List NDKArches = new List();
foreach (var Arch in Architectures)
{
NDKArches.Add(GetNDKArch(Arch));
}
UPL = new UnrealPluginLanguage(ProjectFile, inPluginExtraData, NDKArches, "http://schemas.android.com/apk/res/android", "xmlns:android=\"http://schemas.android.com/apk/res/android\"", UnrealTargetPlatform.Android);
// APL.SetTrace();
}
///
/// Simple function to pipe output asynchronously
///
private void ParseApiLevel(object Sender, DataReceivedEventArgs Event)
{
// DataReceivedEventHandler is fired with a null string when the output stream is closed. We don't want to
// print anything for that event.
if (!String.IsNullOrEmpty(Event.Data))
{
string Line = Event.Data;
if (Line.StartsWith("id:"))
{
// the line should look like: id: 1 or "android-19"
string[] Tokens = Line.Split("\"".ToCharArray());
if (Tokens.Length >= 2)
{
PossibleApiLevels.Add(Tokens[1]);
}
}
}
}
private ConfigHierarchy GetConfigCacheIni(ConfigHierarchyType Type)
{
return ConfigCache.ReadHierarchy(Type, DirectoryReference.FromFile(ProjectFile), UnrealTargetPlatform.Android);
}
private string CachedSDKLevel = null;
private string GetSdkApiLevel(AndroidToolChain ToolChain)
{
if (CachedSDKLevel == null)
{
// ask the .ini system for what version to use
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
string SDKLevel;
Ini.GetString("/Script/AndroidPlatformEditor.AndroidSDKSettings", "SDKAPILevel", out SDKLevel);
// if we want to use whatever version the ndk uses, then use that
if (SDKLevel == "matchndk")
{
SDKLevel = ToolChain.GetNdkApiLevel();
}
// run a command and capture output
if (SDKLevel == "latest")
{
// we expect there to be one, so use the first one
string AndroidCommandPath = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/tools/android" + (Utils.IsRunningOnMono ? "" : ".bat"));
var ExeInfo = new ProcessStartInfo(AndroidCommandPath, "list targets");
ExeInfo.UseShellExecute = false;
ExeInfo.RedirectStandardOutput = true;
using (var GameProcess = Process.Start(ExeInfo))
{
PossibleApiLevels = new List();
GameProcess.BeginOutputReadLine();
GameProcess.OutputDataReceived += ParseApiLevel;
GameProcess.WaitForExit();
}
if (PossibleApiLevels != null && PossibleApiLevels.Count > 0)
{
SDKLevel = ToolChain.GetLargestApiLevel(PossibleApiLevels.ToArray());
}
else
{
throw new BuildException("Can't make an APK without an API installed (see \"android.bat list targets\")");
}
}
Console.WriteLine("Building Java with SDK API level '{0}'", SDKLevel);
CachedSDKLevel = SDKLevel;
}
return CachedSDKLevel;
}
private bool IsVulkanSDKAvailable()
{
bool bHaveVulkan = false;
// First look for VulkanSDK (two possible env variables)
string VulkanSDKPath = Environment.GetEnvironmentVariable("VULKAN_SDK");
if (String.IsNullOrEmpty(VulkanSDKPath))
{
VulkanSDKPath = Environment.GetEnvironmentVariable("VK_SDK_PATH");
}
// Note: header is the same for all architectures so just use arch-arm
string NDKPath = Environment.GetEnvironmentVariable("NDKROOT");
string NDKVulkanIncludePath = NDKPath + "/platforms/android-24/arch-arm/usr/include/vulkan";
// Use NDK Vulkan header if discovered, or VulkanSDK if available
if (File.Exists(NDKVulkanIncludePath + "/vulkan.h"))
{
bHaveVulkan = true;
}
else
if (!String.IsNullOrEmpty(VulkanSDKPath))
{
bHaveVulkan = true;
}
return bHaveVulkan;
}
public static string GetOBBVersionNumber(int PackageVersion)
{
string VersionString = PackageVersion.ToString("0");
return VersionString;
}
public bool PackageDataInsideApk(bool bDisallowPackagingDataInApk)
{
return PackageDataInsideApk(bDisallowPackagingDataInApk, null);
}
public bool PackageDataInsideApk(bool bDisallowPackagingDataInApk, ConfigHierarchy Ini)
{
if (bDisallowPackagingDataInApk)
{
return false;
}
// make a new one if one wasn't passed in
if (Ini == null)
{
Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
}
// we check this a lot, so make it easy
bool bPackageDataInsideApk;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bPackageDataInsideApk", out bPackageDataInsideApk);
return bPackageDataInsideApk;
}
public bool UseExternalFilesDir(bool bDisallowExternalFilesDir, ConfigHierarchy Ini = null)
{
if (bDisallowExternalFilesDir)
{
return false;
}
// make a new one if one wasn't passed in
if (Ini == null)
{
Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
}
// we check this a lot, so make it easy
bool bUseExternalFilesDir;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bUseExternalFilesDir", out bUseExternalFilesDir);
return bUseExternalFilesDir;
}
public bool IsPackagingForDaydream(ConfigHierarchy Ini = null)
{
// make a new one if one wasn't passed in
if (Ini == null)
{
Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
}
string GoogleVRMode = "";
if(Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "GoogleVRMode", out GoogleVRMode))
{
return GoogleVRMode == "Daydream" || GoogleVRMode == "DaydreamAndCardboard";
}
else
{
return false;
}
}
public bool DisableVerifyOBBOnStartUp(ConfigHierarchy Ini = null)
{
// make a new one if one wasn't passed in
if (Ini == null)
{
Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
}
// we check this a lot, so make it easy
bool bDisableVerifyOBBOnStartUp;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bDisableVerifyOBBOnStartUp", out bDisableVerifyOBBOnStartUp);
return bDisableVerifyOBBOnStartUp;
}
private static string GetAntPath()
{
// look up an ANT_HOME env var
string AntHome = Environment.GetEnvironmentVariable("ANT_HOME");
if (!string.IsNullOrEmpty(AntHome) && Directory.Exists(AntHome))
{
string AntPath = AntHome + "/bin/ant" + (Utils.IsRunningOnMono ? "" : ".bat");
// use it if found
if (File.Exists(AntPath))
{
return AntPath;
}
}
// otherwise, look in the eclipse install for the ant plugin (matches the unzipped Android ADT from Google)
string PluginDir = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/../eclipse/plugins");
if (Directory.Exists(PluginDir))
{
string[] Plugins = Directory.GetDirectories(PluginDir, "org.apache.ant*");
// use the first one with ant.bat
if (Plugins.Length > 0)
{
foreach (string PluginName in Plugins)
{
string AntPath = PluginName + "/bin/ant" + (Utils.IsRunningOnMono ? "" : ".bat");
// use it if found
if (File.Exists(AntPath))
{
return AntPath;
}
}
}
}
throw new BuildException("Unable to find ant.bat (via %ANT_HOME% or %ANDROID_HOME%/../eclipse/plugins/org.apache.ant*");
}
private static void CopyFileDirectory(string SourceDir, string DestDir, Dictionary Replacements)
{
if (!Directory.Exists(SourceDir))
{
return;
}
string[] Files = Directory.GetFiles(SourceDir, "*.*", SearchOption.AllDirectories);
foreach (string Filename in Files)
{
// skip template files
if (Path.GetExtension(Filename) == ".template")
{
continue;
}
// make the dst filename with the same structure as it was in SourceDir
string DestFilename = Path.Combine(DestDir, Utils.MakePathRelativeTo(Filename, SourceDir));
if (File.Exists(DestFilename))
{
File.Delete(DestFilename);
}
// make the subdirectory if needed
string DestSubdir = Path.GetDirectoryName(DestFilename);
if (!Directory.Exists(DestSubdir))
{
Directory.CreateDirectory(DestSubdir);
}
// some files are handled specially
string Ext = Path.GetExtension(Filename);
if (Ext == ".xml")
{
string Contents = File.ReadAllText(Filename);
// replace some variables
foreach (var Pair in Replacements)
{
Contents = Contents.Replace(Pair.Key, Pair.Value);
}
// write out file
File.WriteAllText(DestFilename, Contents);
}
else
{
File.Copy(Filename, DestFilename);
// remove any read only flags
FileInfo DestFileInfo = new FileInfo(DestFilename);
DestFileInfo.Attributes = DestFileInfo.Attributes & ~FileAttributes.ReadOnly;
}
}
}
private static void DeleteDirectory(string InPath, string SubDirectoryToKeep = "")
{
// skip the dir we want to
if (String.Compare(Path.GetFileName(InPath), SubDirectoryToKeep, true) == 0)
{
return;
}
// delete all files in here
string[] Files;
try
{
Files = Directory.GetFiles(InPath);
}
catch (Exception)
{
// directory doesn't exist so all is good
return;
}
foreach (string Filename in Files)
{
try
{
// remove any read only flags
FileInfo FileInfo = new FileInfo(Filename);
FileInfo.Attributes = FileInfo.Attributes & ~FileAttributes.ReadOnly;
FileInfo.Delete();
}
catch (Exception)
{
Log.TraceInformation("Failed to delete all files in directory {0}. Continuing on...", InPath);
}
}
string[] Dirs = Directory.GetDirectories(InPath, "*.*", SearchOption.TopDirectoryOnly);
foreach (string Dir in Dirs)
{
DeleteDirectory(Dir, SubDirectoryToKeep);
// try to delete the directory, but allow it to fail (due to SubDirectoryToKeep still existing)
try
{
Directory.Delete(Dir);
}
catch (Exception)
{
// do nothing
}
}
}
public string GetUE4BuildFilePath(String EngineDirectory)
{
return Path.GetFullPath(Path.Combine(EngineDirectory, "Build/Android/Java"));
}
public string GetUE4JavaSrcPath()
{
return Path.Combine("src", "com", "epicgames", "ue4");
}
public string GetUE4JavaFilePath(String EngineDirectory)
{
return Path.GetFullPath(Path.Combine(GetUE4BuildFilePath(EngineDirectory), GetUE4JavaSrcPath()));
}
public string GetUE4JavaBuildSettingsFileName(String EngineDirectory)
{
return Path.Combine(GetUE4JavaFilePath(EngineDirectory), "JavaBuildSettings.java");
}
public string GetUE4JavaDownloadShimFileName(string Directory)
{
return Path.Combine(Directory, "DownloadShim.java");
}
public string GetUE4TemplateJavaSourceDir(string Directory)
{
return Path.Combine(GetUE4BuildFilePath(Directory), "JavaTemplates");
}
public string GetUE4TemplateJavaDestination(string Directory, string FileName)
{
return Path.Combine(Directory, FileName);
}
public string GetUE4JavaOBBDataFileName(string Directory)
{
return Path.Combine(Directory, "OBBData.java");
}
public class TemplateFile
{
public string SourceFile;
public string DestinationFile;
}
private void MakeDirectoryIfRequired(string DestFilename)
{
string DestSubdir = Path.GetDirectoryName(DestFilename);
if (!Directory.Exists(DestSubdir))
{
Directory.CreateDirectory(DestSubdir);
}
}
public int GetStoreVersion()
{
var Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
int StoreVersion;
Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "StoreVersion", out StoreVersion);
return StoreVersion;
}
public void WriteJavaOBBDataFile(string FileName, string PackageName, List ObbSources)
{
Log.TraceInformation("\n==== Writing to OBB data file {0} ====", FileName);
int StoreVersion = GetStoreVersion();
string[] obbDataFile = File.Exists(FileName) ? File.ReadAllLines(FileName) : null;
StringBuilder obbData = new StringBuilder("package " + PackageName + ";\n\n");
obbData.Append("public class OBBData\n{\n");
obbData.Append("public static class XAPKFile {\npublic final boolean mIsMain;\npublic final String mFileVersion;\n");
obbData.Append("public final long mFileSize;\nXAPKFile(boolean isMain, String fileVersion, long fileSize) {\nmIsMain = isMain;\nmFileVersion = fileVersion;\nmFileSize = fileSize;\n");
obbData.Append("}\n}\n\n");
// write the data here
obbData.Append("public static final XAPKFile[] xAPKS = {\n");
// For each obb file... but we only have one... for now anyway.
bool first = ObbSources.Count > 1;
foreach (string ObbSource in ObbSources)
{
obbData.Append("new XAPKFile(\ntrue, // true signifies a main file\n");
obbData.AppendFormat("\"{0}\", // the version of the APK that the file was uploaded against\n", GetOBBVersionNumber(StoreVersion));
obbData.AppendFormat("{0}L // the length of the file in bytes\n", File.Exists(ObbSource) ? new FileInfo(ObbSource).Length : 0);
obbData.AppendFormat("){0}\n", first ? "," : "");
first = false;
}
obbData.Append("};\n"); // close off data
//
obbData.Append("};\n"); // close class definition off
if (obbDataFile == null || !obbDataFile.SequenceEqual((obbData.ToString()).Split('\n')))
{
MakeDirectoryIfRequired(FileName);
using (StreamWriter outputFile = new StreamWriter(FileName, false))
{
var obbSrc = obbData.ToString().Split('\n');
foreach (var line in obbSrc)
{
outputFile.WriteLine(line);
}
}
}
else
{
Log.TraceInformation("\n==== OBB data file up to date so not writing. ====");
}
}
public void WriteJavaDownloadSupportFiles(string ShimFileName, IEnumerable TemplateFiles, Dictionary replacements)
{
// Deal with the Shim first as that is a known target and is easy to deal with
// If it exists then read it
string[] DestFileContent = File.Exists(ShimFileName) ? File.ReadAllLines(ShimFileName) : null;
StringBuilder ShimFileContent = new StringBuilder("package com.epicgames.ue4;\n\n");
ShimFileContent.AppendFormat("import {0}.OBBDownloaderService;\n", replacements["$$PackageName$$"]);
ShimFileContent.AppendFormat("import {0}.DownloaderActivity;\n", replacements["$$PackageName$$"]);
// Workaround to do OBB file checking without using DownloadActivity to avoid transit to another activity in Daydream
bool bPackageForDaydream = IsPackagingForDaydream();
if (bPackageForDaydream)
{
ShimFileContent.Append("import android.app.Activity;\n");
ShimFileContent.Append("import com.google.android.vending.expansion.downloader.Helpers;\n");
ShimFileContent.AppendFormat("import {0}.OBBData;\n", replacements["$$PackageName$$"]);
}
ShimFileContent.Append("\n\npublic class DownloadShim\n{\n");
ShimFileContent.Append("\tpublic static OBBDownloaderService DownloaderService;\n");
ShimFileContent.Append("\tpublic static DownloaderActivity DownloadActivity;\n");
ShimFileContent.Append("\tpublic static Class GetDownloaderType() { return DownloaderActivity.class; }\n");
// Workaround to do OBB file checking without using DownloadActivity to avoid a SPM bug
if (bPackageForDaydream)
{
ShimFileContent.Append("\tpublic static boolean expansionFilesDelivered(Activity activity) {\n");
ShimFileContent.Append("\t\tfor (OBBData.XAPKFile xf : OBBData.xAPKS) {\n");
ShimFileContent.Append("\t\t\tString fileName = Helpers.getExpansionAPKFileName(activity, xf.mIsMain, xf.mFileVersion);\n");
ShimFileContent.Append("\t\t\tGameActivity.Log.debug(\"Checking for file : \" + fileName);\n");
ShimFileContent.Append("\t\t\tString fileForNewFile = Helpers.generateSaveFileName(activity, fileName);\n");
ShimFileContent.Append("\t\t\tString fileForDevFile = Helpers.generateSaveFileNameDevelopment(activity, fileName);\n");
ShimFileContent.Append("\t\t\tGameActivity.Log.debug(\"which is really being resolved to : \" + fileForNewFile + \"\\n Or : \" + fileForDevFile);\n");
ShimFileContent.Append("\t\t\tif (!Helpers.doesFileExist(activity, fileName, xf.mFileSize, false) &&\n");
ShimFileContent.Append("\t\t\t\t!Helpers.doesFileExistDev(activity, fileName, xf.mFileSize, false))\n");
ShimFileContent.Append("\t\t\t\treturn false;\n");
ShimFileContent.Append("\t\t\t}\n");
ShimFileContent.Append("\t\treturn true;\n");
ShimFileContent.Append("\t}\n");
}
ShimFileContent.Append("}\n");
Log.TraceInformation("\n==== Writing to shim file {0} ====", ShimFileName);
// If they aren't the same then dump out the settings
if (DestFileContent == null || !DestFileContent.SequenceEqual((ShimFileContent.ToString()).Split('\n')))
{
MakeDirectoryIfRequired(ShimFileName);
using (StreamWriter outputFile = new StreamWriter(ShimFileName, false))
{
var shimSrc = ShimFileContent.ToString().Split('\n');
foreach (var line in shimSrc)
{
outputFile.WriteLine(line);
}
}
}
else
{
Log.TraceInformation("\n==== Shim data file up to date so not writing. ====");
}
// Now we move on to the template files
foreach (var template in TemplateFiles)
{
string[] templateSrc = File.ReadAllLines(template.SourceFile);
string[] templateDest = File.Exists(template.DestinationFile) ? File.ReadAllLines(template.DestinationFile) : null;
for (int i = 0; i < templateSrc.Length; ++i)
{
string srcLine = templateSrc[i];
bool changed = false;
foreach (var kvp in replacements)
{
if (srcLine.Contains(kvp.Key))
{
srcLine = srcLine.Replace(kvp.Key, kvp.Value);
changed = true;
}
}
if (changed)
{
templateSrc[i] = srcLine;
}
}
Log.TraceInformation("\n==== Writing to template target file {0} ====", template.DestinationFile);
if (templateDest == null || templateSrc.Length != templateDest.Length || !templateSrc.SequenceEqual(templateDest))
{
MakeDirectoryIfRequired(template.DestinationFile);
using (StreamWriter outputFile = new StreamWriter(template.DestinationFile, false))
{
foreach (var line in templateSrc)
{
outputFile.WriteLine(line);
}
}
}
else
{
Log.TraceInformation("\n==== Template target file up to date so not writing. ====");
}
}
}
private static string GetNDKArch(string UE4Arch)
{
switch (UE4Arch)
{
case "-armv7": return "armeabi-v7a";
case "-arm64": return "arm64-v8a";
case "-x64": return "x86_64";
case "-x86": return "x86";
default: throw new BuildException("Unknown UE4 architecture {0}", UE4Arch);
}
}
public static string GetUE4Arch(string NDKArch)
{
switch (NDKArch)
{
case "armeabi-v7a": return "-armv7";
case "arm64-v8a": return "-arm64";
case "x86": return "-x86";
case "arm64": return "-arm64";
case "x86_64":
case "x64": return "-x64";
// default: throw new BuildException("Unknown NDK architecture '{0}'", NDKArch);
// future-proof by returning armv7 for unknown
default: return "-armv7";
}
}
private static void StripDebugSymbols(string SourceFileName, string TargetFileName, string UE4Arch)
{
// Copy the file and remove read-only if necessary
File.Copy(SourceFileName, TargetFileName, true);
FileAttributes Attribs = File.GetAttributes(TargetFileName);
if (Attribs.HasFlag(FileAttributes.ReadOnly))
{
File.SetAttributes(TargetFileName, Attribs & ~FileAttributes.ReadOnly);
}
ProcessStartInfo StartInfo = new ProcessStartInfo();
StartInfo.FileName = AndroidToolChain.GetStripExecutablePath(UE4Arch);
StartInfo.Arguments = "--strip-debug \"" + TargetFileName + "\"";
StartInfo.UseShellExecute = false;
StartInfo.CreateNoWindow = true;
Utils.RunLocalProcessAndLogOutput(StartInfo);
}
private static void CopySTL(AndroidToolChain ToolChain, string UE4BuildPath, string UE4Arch, string NDKArch, bool bForDistribution)
{
string GccVersion = "4.6";
if (Directory.Exists(Environment.ExpandEnvironmentVariables("%NDKROOT%/sources/cxx-stl/gnu-libstdc++/4.9")))
{
GccVersion = "4.9";
}
else if (Directory.Exists(Environment.ExpandEnvironmentVariables("%NDKROOT%/sources/cxx-stl/gnu-libstdc++/4.8")))
{
GccVersion = "4.8";
}
// copy it in!
string SourceSTLSOName = Environment.ExpandEnvironmentVariables("%NDKROOT%/sources/cxx-stl/gnu-libstdc++/") + GccVersion + "/libs/" + NDKArch + "/libgnustl_shared.so";
string FinalSTLSOName = UE4BuildPath + "/libs/" + NDKArch + "/libgnustl_shared.so";
// check to see if libgnustl_shared.so is newer than last time we copied (or needs stripping for distribution)
bool bFileExists = File.Exists(FinalSTLSOName);
TimeSpan Diff = File.GetLastWriteTimeUtc(FinalSTLSOName) - File.GetLastWriteTimeUtc(SourceSTLSOName);
if (bForDistribution || !bFileExists || Diff.TotalSeconds < -1 || Diff.TotalSeconds > 1)
{
if (bFileExists)
{
File.Delete(FinalSTLSOName);
}
Directory.CreateDirectory(Path.GetDirectoryName(FinalSTLSOName));
if (bForDistribution)
{
// Strip debug symbols for distribution builds
StripDebugSymbols(SourceSTLSOName, FinalSTLSOName, UE4Arch);
}
else
{
File.Copy(SourceSTLSOName, FinalSTLSOName, true);
}
}
}
private void CopyGfxDebugger(string UE4BuildPath, string UE4Arch, string NDKArch)
{
string AndroidGraphicsDebugger;
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "AndroidGraphicsDebugger", out AndroidGraphicsDebugger);
switch (AndroidGraphicsDebugger.ToLower())
{
case "mali":
{
string MaliGraphicsDebuggerPath;
AndroidPlatformSDK.GetPath(Ini, "/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "MaliGraphicsDebuggerPath", out MaliGraphicsDebuggerPath);
if (Directory.Exists(MaliGraphicsDebuggerPath))
{
Directory.CreateDirectory(Path.Combine(UE4BuildPath, "libs", NDKArch));
string MaliLibSrcPath = Path.Combine(MaliGraphicsDebuggerPath, @"target\android-non-root\arm", NDKArch, "libMGD.so");
if (!File.Exists(MaliLibSrcPath))
{
// in v4.3.0 library location was changed
MaliLibSrcPath = Path.Combine(MaliGraphicsDebuggerPath, @"target\android\arm\unrooted", NDKArch, "libMGD.so");
}
string MaliLibDstPath = Path.Combine(UE4BuildPath, "libs", NDKArch, "libMGD.so");
Console.WriteLine("Copying {0} to {1}", MaliLibSrcPath, MaliLibDstPath);
File.Copy(MaliLibSrcPath, MaliLibDstPath, true);
}
}
break;
// @TODO: Add NVIDIA Gfx Debugger
/*
case "nvidia":
{
Directory.CreateDirectory(UE4BuildPath + "/libs/" + NDKArch);
File.Copy("F:/NVPACK/android-kk-egl-t124-a32/Stripped_libNvPmApi.Core.so", UE4BuildPath + "/libs/" + NDKArch + "/libNvPmApi.Core.so", true);
File.Copy("F:/NVPACK/android-kk-egl-t124-a32/Stripped_libNvidia_gfx_debugger.so", UE4BuildPath + "/libs/" + NDKArch + "/libNvidia_gfx_debugger.so", true);
}
*/
default:
break;
}
}
private static int RunCommandLineProgramAndReturnResult(string WorkingDirectory, string Command, string Params, string OverrideDesc = null, bool bUseShellExecute = false)
{
if (OverrideDesc == null)
{
Log.TraceInformation("\nRunning: " + Command + " " + Params);
}
else if (OverrideDesc != "")
{
Log.TraceInformation(OverrideDesc);
Log.TraceVerbose("\nRunning: " + Command + " " + Params);
}
ProcessStartInfo StartInfo = new ProcessStartInfo();
StartInfo.WorkingDirectory = WorkingDirectory;
StartInfo.FileName = Command;
StartInfo.Arguments = Params;
StartInfo.UseShellExecute = bUseShellExecute;
StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
Process Proc = new Process();
Proc.StartInfo = StartInfo;
Proc.Start();
Proc.WaitForExit();
return Proc.ExitCode;
}
private static void RunCommandLineProgramWithException(string WorkingDirectory, string Command, string Params, string OverrideDesc = null, bool bUseShellExecute = false)
{
if (OverrideDesc == null)
{
Log.TraceInformation("\nRunning: " + Command + " " + Params);
}
else if (OverrideDesc != "")
{
Log.TraceInformation(OverrideDesc);
Log.TraceVerbose("\nRunning: " + Command + " " + Params);
}
ProcessStartInfo StartInfo = new ProcessStartInfo();
StartInfo.WorkingDirectory = WorkingDirectory;
StartInfo.FileName = Command;
StartInfo.Arguments = Params;
StartInfo.UseShellExecute = bUseShellExecute;
StartInfo.WindowStyle = ProcessWindowStyle.Minimized;
Process Proc = new Process();
Proc.StartInfo = StartInfo;
Proc.Start();
Proc.WaitForExit();
// android bat failure
if (Proc.ExitCode != 0)
{
throw new BuildException("{0} failed with args {1}", Command, Params);
}
}
private bool CheckApplicationName(string UE4BuildPath, string ProjectName, out string ApplicationDisplayName)
{
string StringsXMLPath = Path.Combine(UE4BuildPath, "res/values/strings.xml");
ApplicationDisplayName = null;
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "ApplicationDisplayName", out ApplicationDisplayName);
// use project name if display name is left blank
if (String.IsNullOrWhiteSpace(ApplicationDisplayName))
{
ApplicationDisplayName = ProjectName;
}
// replace escaped characters (note: changes pattern before &, then patches back to allow escaped character codes in the string)
ApplicationDisplayName = ApplicationDisplayName.Replace("", "$@#$").Replace("&", "&").Replace("'", "\\'").Replace("\"", "\\\"").Replace("<", "<").Replace(">", ">").Replace("$@#$", "");
// if it doesn't exist, need to repackage
if (!File.Exists(StringsXMLPath))
{
return true;
}
// read it and see if needs to be updated
string Contents = File.ReadAllText(StringsXMLPath);
// find the key
string AppNameTag = "";
int KeyIndex = Contents.IndexOf(AppNameTag);
// if doesn't exist, need to repackage
if (KeyIndex < 0)
{
return true;
}
// get the current value
KeyIndex += AppNameTag.Length;
int TagEnd = Contents.IndexOf("", KeyIndex);
if (TagEnd < 0)
{
return true;
}
string CurrentApplicationName = Contents.Substring(KeyIndex, TagEnd - KeyIndex);
// no need to do anything if matches
if (CurrentApplicationName == ApplicationDisplayName)
{
// name matches, no need to force a repackage
return false;
}
// need to repackage
return true;
}
private void UpdateProjectProperties(AndroidToolChain ToolChain, string UE4BuildPath, string ProjectName)
{
Log.TraceInformation("\n===={0}====UPDATING BUILD CONFIGURATION FILES====================================================", DateTime.Now.ToString());
// get all of the libs (from engine + project)
string JavaLibsDir = Path.Combine(UE4BuildPath, "JavaLibs");
string[] LibDirs = Directory.GetDirectories(JavaLibsDir);
// get existing project.properties lines (if any)
string ProjectPropertiesFile = Path.Combine(UE4BuildPath, "project.properties");
string[] PropertiesLines = new string[] { };
if (File.Exists(ProjectPropertiesFile))
{
PropertiesLines = File.ReadAllLines(ProjectPropertiesFile);
}
// figure out how many libraries were already listed (if there were more than this listed, then we need to start the file over, because we need to unreference a library)
int NumOutstandingAlreadyReferencedLibs = 0;
foreach (string Line in PropertiesLines)
{
if (Line.StartsWith("android.library.reference."))
{
NumOutstandingAlreadyReferencedLibs++;
}
}
// now go through each one and verify they are listed in project properties, and if not, add them
List LibsToBeAdded = new List();
foreach (string LibDir in LibDirs)
{
// put it in terms of the subdirectory that would be in the project.properties
string RelativePath = "JavaLibs/" + Path.GetFileName(LibDir);
// now look for this in the existing file
bool bWasReferencedAlready = false;
foreach (string Line in PropertiesLines)
{
if (Line.StartsWith("android.library.reference.") && Line.EndsWith(RelativePath))
{
// this lib was already referenced, don't need to readd
bWasReferencedAlready = true;
break;
}
}
if (bWasReferencedAlready)
{
// if it was, no further action needed, and count it off
NumOutstandingAlreadyReferencedLibs--;
}
else
{
// otherwise, we need to add it to the project properties
LibsToBeAdded.Add(RelativePath);
}
}
// now at this point, if there are any outstanding already referenced libs, we have too many, so we have to start over
if (NumOutstandingAlreadyReferencedLibs > 0)
{
// @todo android: If a user had a project.properties in the game, NEVER do this
Log.TraceInformation("There were too many libs already referenced in project.properties, tossing it");
File.Delete(ProjectPropertiesFile);
LibsToBeAdded.Clear();
foreach (string LibDir in LibDirs)
{
// put it in terms of the subdirectory that would be in the project.properties
LibsToBeAdded.Add("JavaLibs/" + Path.GetFileName(LibDir));
}
}
// now update the project for each library
string AndroidCommandPath = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/tools/android" + (Utils.IsRunningOnMono ? "" : ".bat"));
string UpdateCommandLine = "--silent update project --subprojects --name " + ProjectName + " --path . --target " + GetSdkApiLevel(ToolChain);
foreach (string Lib in LibsToBeAdded)
{
string LocalUpdateCommandLine = UpdateCommandLine + " --library " + Lib;
// make sure each library has a build.xml - --subprojects doesn't create build.xml files, but it will create project.properties
// and later code needs each lib to have a build.xml
RunCommandLineProgramWithException(UE4BuildPath, AndroidCommandPath, "--silent update lib-project --path " + Lib + " --target " + GetSdkApiLevel(ToolChain), "");
RunCommandLineProgramWithException(UE4BuildPath, AndroidCommandPath, LocalUpdateCommandLine, "Updating project.properties, local.properties, and build.xml for " + Path.GetFileName(Lib) + "...");
}
}
private string GetAllBuildSettings(AndroidToolChain ToolChain, string BuildPath, bool bForDistribution, bool bMakeSeparateApks, bool bPackageDataInsideApk, bool bDisableVerifyOBBOnStartUp, bool bUseExternalFilesDir)
{
// make the settings string - this will be char by char compared against last time
StringBuilder CurrentSettings = new StringBuilder();
CurrentSettings.AppendLine(string.Format("NDKROOT={0}", Environment.GetEnvironmentVariable("NDKROOT")));
CurrentSettings.AppendLine(string.Format("ANDROID_HOME={0}", Environment.GetEnvironmentVariable("ANDROID_HOME")));
CurrentSettings.AppendLine(string.Format("ANT_HOME={0}", Environment.GetEnvironmentVariable("ANT_HOME")));
CurrentSettings.AppendLine(string.Format("JAVA_HOME={0}", Environment.GetEnvironmentVariable("JAVA_HOME")));
CurrentSettings.AppendLine(string.Format("SDKVersion={0}", GetSdkApiLevel(ToolChain)));
CurrentSettings.AppendLine(string.Format("bForDistribution={0}", bForDistribution));
CurrentSettings.AppendLine(string.Format("bMakeSeparateApks={0}", bMakeSeparateApks));
CurrentSettings.AppendLine(string.Format("bPackageDataInsideApk={0}", bPackageDataInsideApk));
CurrentSettings.AppendLine(string.Format("bDisableVerifyOBBOnStartUp={0}", bDisableVerifyOBBOnStartUp));
CurrentSettings.AppendLine(string.Format("bUseExternalFilesDir={0}", bUseExternalFilesDir));
// all AndroidRuntimeSettings ini settings in here
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
ConfigHierarchySection Section = Ini.FindSection("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings");
if (Section != null)
{
foreach (string Key in Section.KeyNames)
{
IEnumerable Values;
Section.TryGetValues(Key, out Values);
foreach (string Value in Values)
{
CurrentSettings.AppendLine(string.Format("{0}={1}", Key, Value));
}
}
}
Section = Ini.FindSection("/Script/AndroidPlatformEditor.AndroidSDKSettings");
if (Section != null)
{
foreach (string Key in Section.KeyNames)
{
IEnumerable Values;
Section.TryGetValues(Key, out Values);
foreach (string Value in Values)
{
CurrentSettings.AppendLine(string.Format("{0}={1}", Key, Value));
}
}
}
var Arches = ToolChain.GetAllArchitectures();
foreach (string Arch in Arches)
{
CurrentSettings.AppendFormat("Arch={0}{1}", Arch, Environment.NewLine);
}
var GPUArchitectures = ToolChain.GetAllGPUArchitectures();
foreach (string GPUArch in GPUArchitectures)
{
CurrentSettings.AppendFormat("GPUArch={0}{1}", GPUArch, Environment.NewLine);
}
return CurrentSettings.ToString();
}
private bool CheckDependencies(AndroidToolChain ToolChain, string ProjectName, string ProjectDirectory, string UE4BuildFilesPath, string GameBuildFilesPath, string EngineDirectory, List SettingsFiles,
string CookFlavor, string OutputPath, string UE4BuildPath, bool bMakeSeparateApks, bool bPackageDataInsideApk)
{
var Arches = ToolChain.GetAllArchitectures();
var GPUArchitectures = ToolChain.GetAllGPUArchitectures();
// check all input files (.so, java files, .ini files, etc)
bool bAllInputsCurrent = true;
foreach (string Arch in Arches)
{
foreach (string GPUArch in GPUArchitectures)
{
string SourceSOName = AndroidToolChain.InlineArchName(OutputPath, Arch, GPUArch);
// if the source binary was UE4Game, replace it with the new project name, when re-packaging a binary only build
string ApkFilename = Path.GetFileNameWithoutExtension(OutputPath).Replace("UE4Game", ProjectName);
string DestApkName = Path.Combine(ProjectDirectory, "Binaries/Android/") + ApkFilename + ".apk";
// if we making multiple Apks, we need to put the architecture into the name
if (bMakeSeparateApks)
{
DestApkName = AndroidToolChain.InlineArchName(DestApkName, Arch, GPUArch);
}
// check to see if it's out of date before trying the slow make apk process (look at .so and all Engine and Project build files to be safe)
List InputFiles = new List();
InputFiles.Add(SourceSOName);
InputFiles.AddRange(Directory.EnumerateFiles(UE4BuildFilesPath, "*.*", SearchOption.AllDirectories));
if (Directory.Exists(GameBuildFilesPath))
{
InputFiles.AddRange(Directory.EnumerateFiles(GameBuildFilesPath, "*.*", SearchOption.AllDirectories));
}
// make sure changed java files will rebuild apk
InputFiles.AddRange(SettingsFiles);
// rebuild if .pak files exist for OBB in APK case
if (bPackageDataInsideApk)
{
string PAKFileLocation = ProjectDirectory + "/Saved/StagedBuilds/Android" + CookFlavor + "/" + ProjectName + "/Content/Paks";
if (Directory.Exists(PAKFileLocation))
{
var PakFiles = Directory.EnumerateFiles(PAKFileLocation, "*.pak", SearchOption.TopDirectoryOnly);
foreach (var Name in PakFiles)
{
InputFiles.Add(Name);
}
}
}
// look for any newer input file
DateTime ApkTime = File.GetLastWriteTimeUtc(DestApkName);
foreach (var InputFileName in InputFiles)
{
if (File.Exists(InputFileName))
{
// skip .log files
if (Path.GetExtension(InputFileName) == ".log")
{
continue;
}
DateTime InputFileTime = File.GetLastWriteTimeUtc(InputFileName);
if (InputFileTime.CompareTo(ApkTime) > 0)
{
bAllInputsCurrent = false;
Log.TraceInformation("{0} is out of date due to newer input file {1}", DestApkName, InputFileName);
break;
}
}
}
}
}
return bAllInputsCurrent;
}
private int ConvertDepthBufferIniValue(string IniValue)
{
switch (IniValue.ToLower())
{
case "bits16":
return 16;
case "bits24":
return 24;
case "bits32":
return 32;
default:
return 0;
}
}
private string ConvertOrientationIniValue(string IniValue)
{
switch (IniValue.ToLower())
{
case "portrait":
return "portrait";
case "reverseportrait":
return "reversePortrait";
case "sensorportrait":
return "sensorPortrait";
case "landscape":
return "landscape";
case "reverselandscape":
return "reverseLandscape";
case "sensorlandscape":
return "sensorLandscape";
case "sensor":
return "sensor";
case "fullsensor":
return "fullSensor";
default:
return "landscape";
}
}
private void DetermineScreenOrientationRequirements(out bool bNeedPortrait, out bool bNeedLandscape)
{
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
string Orientation;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "Orientation", out Orientation);
bNeedLandscape = false;
bNeedPortrait = false;
switch (Orientation.ToLower())
{
case "portrait":
bNeedPortrait = true;
break;
case "reverseportrait":
bNeedPortrait = true;
break;
case "sensorportrait":
bNeedPortrait = true;
break;
case "landscape":
bNeedLandscape = true;
break;
case "reverselandscape":
bNeedLandscape = true;
break;
case "sensorlandscape":
bNeedLandscape = true;
break;
case "sensor":
bNeedPortrait = true;
bNeedLandscape = true;
break;
case "fullsensor":
bNeedPortrait = true;
bNeedLandscape = true;
break;
default:
bNeedPortrait = true;
bNeedLandscape = true;
break;
}
}
private void PickDownloaderScreenOrientation(string UE4BuildPath, bool bNeedPortrait, bool bNeedLandscape)
{
// Remove unused downloader_progress.xml to prevent missing resource
if (!bNeedPortrait)
{
string LayoutPath = UE4BuildPath + "/res/layout-port/downloader_progress.xml";
if (File.Exists(LayoutPath))
{
File.Delete(LayoutPath);
}
}
if (!bNeedLandscape)
{
string LayoutPath = UE4BuildPath + "/res/layout-land/downloader_progress.xml";
if (File.Exists(LayoutPath))
{
File.Delete(LayoutPath);
}
}
// Loop through each of the resolutions (only /res/drawable/ is required, others are optional)
string[] Resolutions = new string[] { "/res/drawable/", "/res/drawable-ldpi/", "/res/drawable-mdpi/", "/res/drawable-hdpi/", "/res/drawable-xhdpi/" };
foreach (string ResolutionPath in Resolutions)
{
string PortraitFilename = UE4BuildPath + ResolutionPath + "downloadimagev.png";
if (bNeedPortrait)
{
if (!File.Exists(PortraitFilename) && (ResolutionPath == "/res/drawable/"))
{
Log.TraceWarning("Warning: Downloader screen source image {0} not available, downloader screen will not function properly!", PortraitFilename);
}
}
else
{
// Remove unused image
if (File.Exists(PortraitFilename))
{
File.Delete(PortraitFilename);
}
}
string LandscapeFilename = UE4BuildPath + ResolutionPath + "downloadimageh.png";
if (bNeedLandscape)
{
if (!File.Exists(LandscapeFilename) && (ResolutionPath == "/res/drawable/"))
{
Log.TraceWarning("Warning: Downloader screen source image {0} not available, downloader screen will not function properly!", LandscapeFilename);
}
}
else
{
// Remove unused image
if (File.Exists(LandscapeFilename))
{
File.Delete(LandscapeFilename);
}
}
}
}
private void PackageForDaydream(string UE4BuildPath)
{
bool bPackageForDaydream = IsPackagingForDaydream();
if (!bPackageForDaydream)
{
// If this isn't a Daydream App, we need to make sure to remove
// Daydream specific assets.
// Remove the Daydream app tile background.
string AppTileBackgroundPath = UE4BuildPath + "/res/drawable-nodpi/vr_icon_background.png";
if (File.Exists(AppTileBackgroundPath))
{
File.Delete(AppTileBackgroundPath);
}
// Remove the Daydream app tile icon.
string AppTileIconPath = UE4BuildPath + "/res/drawable-nodpi/vr_icon.png";
if (File.Exists(AppTileIconPath))
{
File.Delete(AppTileIconPath);
}
}
}
private void PickSplashScreenOrientation(string UE4BuildPath, bool bNeedPortrait, bool bNeedLandscape)
{
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
bool bShowLaunchImage = false;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bShowLaunchImage", out bShowLaunchImage);
bool bPackageForGearVR;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bPackageForGearVR", out bPackageForGearVR);
bool bPackageForDaydream = IsPackagingForDaydream();
//override the parameters if we are not showing a launch image or are packaging for GearVR and Daydream
if (bPackageForGearVR || bPackageForDaydream || !bShowLaunchImage)
{
bNeedPortrait = bNeedLandscape = false;
}
// Remove unused styles.xml to prevent missing resource
if (!bNeedPortrait)
{
string StylesPath = UE4BuildPath + "/res/values-port/styles.xml";
if (File.Exists(StylesPath))
{
File.Delete(StylesPath);
}
}
if (!bNeedLandscape)
{
string StylesPath = UE4BuildPath + "/res/values-land/styles.xml";
if (File.Exists(StylesPath))
{
File.Delete(StylesPath);
}
}
// Loop through each of the resolutions (only /res/drawable/ is required, others are optional)
string[] Resolutions = new string[] { "/res/drawable/", "/res/drawable-ldpi/", "/res/drawable-mdpi/", "/res/drawable-hdpi/", "/res/drawable-xhdpi/" };
foreach (string ResolutionPath in Resolutions)
{
string PortraitFilename = UE4BuildPath + ResolutionPath + "splashscreen_portrait.png";
if (bNeedPortrait)
{
if (!File.Exists(PortraitFilename) && (ResolutionPath == "/res/drawable/"))
{
Log.TraceWarning("Warning: Splash screen source image {0} not available, splash screen will not function properly!", PortraitFilename);
}
}
else
{
// Remove unused image
if (File.Exists(PortraitFilename))
{
File.Delete(PortraitFilename);
}
}
string LandscapeFilename = UE4BuildPath + ResolutionPath + "splashscreen_landscape.png";
if (bNeedLandscape)
{
if (!File.Exists(LandscapeFilename) && (ResolutionPath == "/res/drawable/"))
{
Log.TraceWarning("Warning: Splash screen source image {0} not available, splash screen will not function properly!", LandscapeFilename);
}
}
else
{
// Remove unused image
if (File.Exists(LandscapeFilename))
{
File.Delete(LandscapeFilename);
}
}
}
}
private string GetPackageName(string ProjectName)
{
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
string PackageName;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "PackageName", out PackageName);
// replace some variables
PackageName = PackageName.Replace("[PROJECT]", ProjectName);
PackageName = PackageName.Replace("-", "_");
return PackageName;
}
private string GetPublicKey()
{
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
string PlayLicenseKey = "";
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "GooglePlayLicenseKey", out PlayLicenseKey);
return PlayLicenseKey;
}
private string GenerateManifest(AndroidToolChain ToolChain, string ProjectName, string EngineDirectory, bool bIsForDistribution, bool bPackageDataInsideApk, string GameBuildFilesPath, bool bHasOBBFiles, bool bDisableVerifyOBBOnStartUp, string UE4Arch, string GPUArch, string CookFlavor, bool bUseExternalFilesDir, string Configuration)
{
// Read the engine version
string EngineMajorVersion = "4";
string EngineMinorVersion = "0";
string EnginePatchVersion = "0";
string EngineVersionFile = Path.Combine(EngineDirectory, "Source", "Runtime", "Launch", "Resources", "Version.h");
string[] EngineVersionLines = File.ReadAllLines(EngineVersionFile);
for (int i = 0; i < EngineVersionLines.Length; ++i)
{
if (EngineVersionLines[i].StartsWith("#define ENGINE_MAJOR_VERSION"))
{
EngineMajorVersion = EngineVersionLines[i].Split('\t')[1].Trim(' ');
}
else if (EngineVersionLines[i].StartsWith("#define ENGINE_MINOR_VERSION"))
{
EngineMinorVersion = EngineVersionLines[i].Split('\t')[1].Trim(' ');
}
else if (EngineVersionLines[i].StartsWith("#define ENGINE_PATCH_VERSION"))
{
EnginePatchVersion = EngineVersionLines[i].Split('\t')[1].Trim(' ');
}
}
string EngineVersion = EngineMajorVersion + "." + EngineMinorVersion + "." + EnginePatchVersion;
string Arch = GetNDKArch(UE4Arch);
int NDKLevelInt = ToolChain.GetNdkApiLevelInt();
// 64-bit targets must be android-21 or higher
if (NDKLevelInt < 21)
{
if (UE4Arch == "-arm64" || UE4Arch == "-x64")
{
NDKLevelInt = 21;
}
}
// ini file to get settings from
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
string PackageName = GetPackageName(ProjectName);
bool bEnableGooglePlaySupport;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bEnableGooglePlaySupport", out bEnableGooglePlaySupport);
bool bUseGetAccounts;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bUseGetAccounts", out bUseGetAccounts);
string DepthBufferPreference;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "DepthBufferPreference", out DepthBufferPreference);
int MinSDKVersion;
Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "MinSDKVersion", out MinSDKVersion);
int TargetSDKVersion = MinSDKVersion;
Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "TargetSDKVersion", out TargetSDKVersion);
int StoreVersion;
Ini.GetInt32("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "StoreVersion", out StoreVersion);
string VersionDisplayName;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "VersionDisplayName", out VersionDisplayName);
string Orientation;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "Orientation", out Orientation);
bool EnableFullScreen;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bFullScreen", out EnableFullScreen);
List ExtraManifestNodeTags;
Ini.GetArray("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "ExtraManifestNodeTags", out ExtraManifestNodeTags);
List ExtraApplicationNodeTags;
Ini.GetArray("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "ExtraApplicationNodeTags", out ExtraApplicationNodeTags);
List ExtraActivityNodeTags;
Ini.GetArray("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "ExtraActivityNodeTags", out ExtraActivityNodeTags);
string ExtraActivitySettings;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "ExtraActivitySettings", out ExtraActivitySettings);
string ExtraApplicationSettings;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "ExtraApplicationSettings", out ExtraApplicationSettings);
List ExtraPermissions;
Ini.GetArray("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "ExtraPermissions", out ExtraPermissions);
bool bPackageForGearVR;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bPackageForGearVR", out bPackageForGearVR);
bool bEnableIAP = false;
Ini.GetBool("OnlineSubsystemGooglePlay.Store", "bSupportsInAppPurchasing", out bEnableIAP);
bool bShowLaunchImage = false;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bShowLaunchImage", out bShowLaunchImage);
string AndroidGraphicsDebugger;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "AndroidGraphicsDebugger", out AndroidGraphicsDebugger);
string InstallLocation;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "InstallLocation", out InstallLocation);
switch(InstallLocation.ToLower())
{
case "preferexternal":
InstallLocation = "preferExternal";
break;
case "auto":
InstallLocation = "auto";
break;
default:
InstallLocation = "internalOnly";
break;
}
// fix up the MinSdkVersion
if (NDKLevelInt > 19)
{
if (MinSDKVersion < 21)
{
MinSDKVersion = 21;
Log.TraceInformation("Fixing minSdkVersion; NDK level above 19 requires minSdkVersion of 21 (arch={0})", UE4Arch.Substring(1));
}
}
// disable GearVR if not supported platform (in this case only armv7 for now)
if (UE4Arch != "-armv7")
{
if (bPackageForGearVR)
{
Log.TraceInformation("Disabling Package For GearVR for unsupported architecture {0}", UE4Arch);
bPackageForGearVR = false;
}
}
// disable splash screen for GearVR (for now)
if (bPackageForGearVR)
{
if (bShowLaunchImage)
{
Log.TraceInformation("Disabling Show Launch Image for GearVR enabled application");
bShowLaunchImage = false;
}
}
bool bPackageForDaydream = IsPackagingForDaydream();
// disable splash screen for daydream
if (bPackageForDaydream)
{
if (bShowLaunchImage)
{
Log.TraceInformation("Disabling Show Launch Image for Daydream enabled application");
bShowLaunchImage = false;
}
}
//figure out which texture compressions are supported
bool bETC1Enabled, bETC2Enabled, bDXTEnabled, bATCEnabled, bPVRTCEnabled, bASTCEnabled;
bETC1Enabled = bETC2Enabled = bDXTEnabled = bATCEnabled = bPVRTCEnabled = bASTCEnabled = false;
if (CookFlavor.Length < 1)
{
//All values supproted
bETC1Enabled = bETC2Enabled = bDXTEnabled = bATCEnabled = bPVRTCEnabled = bASTCEnabled = true;
}
else
{
switch(CookFlavor)
{
case "_Multi":
//need to check ini to determine which are supported
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bMultiTargetFormat_ETC1", out bETC1Enabled);
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bMultiTargetFormat_ETC2", out bETC2Enabled);
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bMultiTargetFormat_DXT", out bDXTEnabled);
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bMultiTargetFormat_ATC", out bATCEnabled);
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bMultiTargetFormat_PVRTC", out bPVRTCEnabled);
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bMultiTargetFormat_ASTC", out bASTCEnabled);
break;
case "_ETC1":
bETC1Enabled = true;
break;
case "_ETC2":
bETC2Enabled = true;
break;
case "_DXT":
bDXTEnabled = true;
break;
case "_ATC":
bATCEnabled = true;
break;
case "_PVRTC":
bPVRTCEnabled = true;
break;
case "_ASTC":
bASTCEnabled = true;
break;
default:
Log.TraceWarning("Invalid or unknown CookFlavor used in GenerateManifest: {0}", CookFlavor);
break;
}
}
bool bSupportingAllTextureFormats = bETC1Enabled && bETC2Enabled && bDXTEnabled && bATCEnabled && bPVRTCEnabled && bASTCEnabled;
// If it is only ETC2 we need to skip adding the texture format filtering and instead use ES 3.0 as minimum version (it requires ETC2)
bool bOnlyETC2Enabled = (bETC2Enabled && !(bETC1Enabled || bDXTEnabled || bATCEnabled || bPVRTCEnabled || bASTCEnabled));
StringBuilder Text = new StringBuilder();
Text.AppendLine("");
Text.AppendLine("", VersionDisplayName));
Text.AppendLine("");
Text.AppendLine("\t");
Text.AppendLine("\t");
Text.AppendLine("\t android:hasCode=\"true\">");
if (bShowLaunchImage)
{
// normal application settings
Text.AppendLine("\t\t", bIsForDistribution ? "false" : "true"));
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t\t\t");
Text.AppendLine(string.Format("\t\t\t\t"));
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t\t", bIsForDistribution ? "false" : "true"));
Text.AppendLine("\t\t\t");
if (!bShowLaunchImage)
{
Text.AppendLine("\t\t\t");
Text.AppendLine("\t\t\t\t");
Text.AppendLine(string.Format("\t\t\t\t"));
Text.AppendLine("\t\t\t");
}
if (!string.IsNullOrEmpty(ExtraActivitySettings))
{
ExtraActivitySettings = ExtraActivitySettings.Replace("\\n", "\n");
foreach (string Line in ExtraActivitySettings.Split("\r\n".ToCharArray()))
{
Text.AppendLine("\t\t\t" + Line);
}
}
string ActivityAdditionsFile = Path.Combine(GameBuildFilesPath, "ManifestActivityAdditions.txt");
if (File.Exists(ActivityAdditionsFile))
{
foreach (string Line in File.ReadAllLines(ActivityAdditionsFile))
{
Text.AppendLine("\t\t\t" + Line);
}
}
Text.AppendLine("\t\t");
// For OBB download support
if (bShowLaunchImage)
{
Text.AppendLine("\t\t");
}
else
{
Text.AppendLine("\t\t");
}
Text.AppendLine(string.Format("\t\t", EngineVersion));
Text.AppendLine(string.Format("\t\t", ConvertDepthBufferIniValue(DepthBufferPreference)));
Text.AppendLine(string.Format("\t\t", bPackageDataInsideApk ? "true" : "false"));
Text.AppendLine(string.Format("\t\t", (bIsForDistribution && !bDisableVerifyOBBOnStartUp) ? "true" : "false"));
Text.AppendLine(string.Format("\t\t", EnableFullScreen ? "true" : "false"));
Text.AppendLine(string.Format("\t\t", ProjectName));
Text.AppendLine(string.Format("\t\t", bHasOBBFiles ? "true" : "false"));
Text.AppendLine(string.Format("\t\t", Configuration));
Text.AppendLine(string.Format("\t\t", bUseExternalFilesDir ? "true" : "false"));
Text.AppendLine(string.Format("\t\t", bPackageForDaydream ? "true" : "false"));
Text.AppendLine("\t\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t\t");
if (!string.IsNullOrEmpty(ExtraApplicationSettings))
{
ExtraApplicationSettings = ExtraApplicationSettings.Replace("\\n", "\n");
foreach (string Line in ExtraApplicationSettings.Split("\r\n".ToCharArray()))
{
Text.AppendLine("\t\t" + Line);
}
}
string ApplicationAdditionsFile = Path.Combine(GameBuildFilesPath, "ManifestApplicationAdditions.txt");
if (File.Exists(ApplicationAdditionsFile))
{
foreach (string Line in File.ReadAllLines(ApplicationAdditionsFile))
{
Text.AppendLine("\t\t" + Line);
}
}
// Required for OBB download support
Text.AppendLine("\t\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t\t");
Text.AppendLine("\t");
Text.AppendLine("");
Text.AppendLine("\t");
// check for an override for the requirements section of the manifest
string RequirementsOverrideFile = Path.Combine(GameBuildFilesPath, "ManifestRequirementsOverride.txt");
if (File.Exists(RequirementsOverrideFile))
{
foreach (string Line in File.ReadAllLines(RequirementsOverrideFile))
{
Text.AppendLine("\t" + Line);
}
}
else
{
// need just the number part of the sdk
Text.AppendLine(string.Format("\t", MinSDKVersion, TargetSDKVersion));
Text.AppendLine("\t");
Text.AppendLine("\t");
Text.AppendLine("\t");
Text.AppendLine("\t");
Text.AppendLine("\t");
// Text.AppendLine("\t");
Text.AppendLine("\t");
Text.AppendLine("\t");
Text.AppendLine("\t");
if (bEnableGooglePlaySupport && bUseGetAccounts)
{
Text.AppendLine("\t");
}
Text.AppendLine("\t");
// Text.AppendLine("\t");
if (bEnableIAP)
{
Text.AppendLine("\t");
}
if (ExtraPermissions != null)
{
foreach (string Permission in ExtraPermissions)
{
string TrimmedPermission = Permission.Trim(' ');
if (TrimmedPermission != "")
{
string PermissionString = string.Format("\t", TrimmedPermission);
if (!Text.ToString().Contains(PermissionString))
{
Text.AppendLine(PermissionString);
}
}
}
}
string RequirementsAdditionsFile = Path.Combine(GameBuildFilesPath, "ManifestRequirementsAdditions.txt");
if (File.Exists(RequirementsAdditionsFile))
{
foreach (string Line in File.ReadAllLines(RequirementsAdditionsFile))
{
Text.AppendLine("\t" + Line);
}
}
if (AndroidGraphicsDebugger.ToLower() == "adreno")
{
string PermissionString = "\t";
if (!Text.ToString().Contains(PermissionString))
{
Text.AppendLine(PermissionString);
}
}
if (!bSupportingAllTextureFormats)
{
Text.AppendLine("\t");
if (bETC1Enabled)
{
Text.AppendLine("\t");
}
if (bETC2Enabled && !bOnlyETC2Enabled)
{
Text.AppendLine("\t");
Text.AppendLine("\t");
}
if (bATCEnabled)
{
Text.AppendLine("\t");
Text.AppendLine("\t");
}
if (bDXTEnabled)
{
Text.AppendLine("\t");
Text.AppendLine("\t");
Text.AppendLine("\t");
}
if (bPVRTCEnabled)
{
Text.AppendLine("\t");
}
if (bASTCEnabled)
{
Text.AppendLine("\t");
}
}
}
Text.AppendLine("");
// allow plugins to modify final manifest HERE
XDocument XDoc;
try
{
XDoc = XDocument.Parse(Text.ToString());
}
catch (Exception e)
{
throw new BuildException("AndroidManifest.xml is invalid {0}\n{1}", e, Text.ToString());
}
UPL.ProcessPluginNode(Arch, "androidManifestUpdates", "", ref XDoc);
return XDoc.ToString();
}
private string GenerateProguard(string Arch, string EngineSourcePath, string GameBuildFilesPath)
{
StringBuilder Text = new StringBuilder();
string ProguardFile = Path.Combine(EngineSourcePath, "proguard-project.txt");
if (File.Exists(ProguardFile))
{
foreach (string Line in File.ReadAllLines(ProguardFile))
{
Text.AppendLine(Line);
}
}
string ProguardAdditionsFile = Path.Combine(GameBuildFilesPath, "ProguardAdditions.txt");
if (File.Exists(ProguardAdditionsFile))
{
foreach (string Line in File.ReadAllLines(ProguardAdditionsFile))
{
Text.AppendLine(Line);
}
}
// add plugin additions
return UPL.ProcessPluginNode(Arch, "proguardAdditions", Text.ToString());
}
private void ValidateGooglePlay(string UE4BuildPath)
{
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
bool bEnableGooglePlaySupport;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bEnableGooglePlaySupport", out bEnableGooglePlaySupport);
if (!bEnableGooglePlaySupport)
{
// do not need to do anything; it is fine
return;
}
string IniAppId;
bool bInvalidIniAppId = false;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "GamesAppID", out IniAppId);
//validate the value found in the AndroidRuntimeSettings
Int64 Value;
if (IniAppId.Length == 0 || !Int64.TryParse(IniAppId, out Value))
{
bInvalidIniAppId = true;
}
bool bInvalid = false;
string ReplacementId = "";
String Filename = Path.Combine(UE4BuildPath, "res", "values", "GooglePlayAppID.xml");
if (File.Exists(Filename))
{
string[] FileContent = File.ReadAllLines(Filename);
int LineIndex = -1;
foreach (string Line in FileContent)
{
++LineIndex;
int StartIndex = Line.IndexOf("\"app_id\">");
if (StartIndex < 0)
continue;
StartIndex += 9;
int EndIndex = Line.IndexOf("");
if (EndIndex < 0)
continue;
string XmlAppId = Line.Substring(StartIndex, EndIndex - StartIndex);
//validate that the AppId matches the .ini value for the GooglePlay AppId, assuming it's valid
if (!bInvalidIniAppId && IniAppId.CompareTo(XmlAppId) != 0)
{
Log.TraceInformation("Replacing Google Play AppID in GooglePlayAppID.xml with AndroidRuntimeSettings .ini value");
bInvalid = true;
ReplacementId = IniAppId;
}
else if(XmlAppId.Length == 0 || !Int64.TryParse(XmlAppId, out Value))
{
Log.TraceWarning("\nWARNING: GooglePlay Games App ID is invalid! Replacing it with \"1\"");
//write file with something which will fail but not cause an exception if executed
bInvalid = true;
ReplacementId = "1";
}
if(bInvalid)
{
// remove any read only flags if invalid so it can be replaced
FileInfo DestFileInfo = new FileInfo(Filename);
DestFileInfo.Attributes = DestFileInfo.Attributes & ~FileAttributes.ReadOnly;
//preserve the rest of the file, just fix up this line
string NewLine = Line.Replace("\"app_id\">" + XmlAppId + "", "\"app_id\">" + ReplacementId + "");
FileContent[LineIndex] = NewLine;
File.WriteAllLines(Filename, FileContent);
}
break;
}
}
else
{
string NewAppId;
// if we don't have an appID to use from the config, write file with something which will fail but not cause an exception if executed
if (bInvalidIniAppId)
{
Log.TraceWarning("\nWARNING: Creating GooglePlayAppID.xml using a Google Play AppID of \"1\" because there was no valid AppID in AndroidRuntimeSettings!");
NewAppId = "1";
}
else
{
Log.TraceInformation("Creating GooglePlayAppID.xml with AndroidRuntimeSettings .ini value");
NewAppId = IniAppId;
}
File.WriteAllText(Filename, "\n\n\t" + NewAppId + "\n\n");
}
}
private bool FilesAreDifferent(string SourceFilename, string DestFilename)
{
// source must exist
FileInfo SourceInfo = new FileInfo(SourceFilename);
if (!SourceInfo.Exists)
{
throw new BuildException("Can't make an APK without file [{0}]", SourceFilename);
}
// different if destination doesn't exist
FileInfo DestInfo = new FileInfo(DestFilename);
if (!DestInfo.Exists)
{
return true;
}
// file lengths differ?
if (SourceInfo.Length != DestInfo.Length)
{
return true;
}
// validate timestamps
TimeSpan Diff = DestInfo.LastWriteTimeUtc - SourceInfo.LastWriteTimeUtc;
if (Diff.TotalSeconds < -1 || Diff.TotalSeconds > 1)
{
return true;
}
// could check actual bytes just to be sure, but good enough
return false;
}
private bool RequiresOBB(bool bDisallowPackageInAPK, string OBBLocation)
{
if (bDisallowPackageInAPK)
{
Log.TraceInformation("APK contains data.");
return false;
}
else if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("uebp_LOCAL_ROOT")))
{
Log.TraceInformation("On build machine.");
return true;
}
else
{
Log.TraceInformation("Looking for OBB.");
return File.Exists(OBBLocation);
}
}
private void MakeApk(AndroidToolChain ToolChain, string ProjectName, string ProjectDirectory, string OutputPath, string EngineDirectory, bool bForDistribution, string CookFlavor, bool bMakeSeparateApks, bool bIncrementalPackage, bool bDisallowPackagingDataInApk, bool bDisallowExternalFilesDir)
{
Log.TraceInformation("\n===={0}====PREPARING TO MAKE APK=================================================================", DateTime.Now.ToString());
// cache some tools paths
string NDKBuildPath = Environment.ExpandEnvironmentVariables("%NDKROOT%/ndk-build" + (Utils.IsRunningOnMono ? "" : ".cmd"));
// set up some directory info
string IntermediateAndroidPath = Path.Combine(ProjectDirectory, "Intermediate/Android/");
string UE4BuildPath = Path.Combine(IntermediateAndroidPath, "APK");
string UE4JavaFilePath = Path.Combine(ProjectDirectory, "Build", "Android", GetUE4JavaSrcPath());
string UE4BuildFilesPath = GetUE4BuildFilePath(EngineDirectory);
string GameBuildFilesPath = Path.Combine(ProjectDirectory, "Build/Android");
// Generate Java files
string PackageName = GetPackageName(ProjectName);
string TemplateDestinationBase = Path.Combine(ProjectDirectory, "Build", "Android", "src", PackageName.Replace('.', Path.DirectorySeparatorChar));
MakeDirectoryIfRequired(TemplateDestinationBase);
// We'll be writing the OBB data into the same location as the download service files
string UE4OBBDataFileName = GetUE4JavaOBBDataFileName(TemplateDestinationBase);
string UE4DownloadShimFileName = GetUE4JavaDownloadShimFileName(UE4JavaFilePath);
// Template generated files
string JavaTemplateSourceDir = GetUE4TemplateJavaSourceDir(EngineDirectory);
var templates = from template in Directory.EnumerateFiles(JavaTemplateSourceDir, "*.template")
let RealName = Path.GetFileNameWithoutExtension(template)
select new TemplateFile { SourceFile = template, DestinationFile = GetUE4TemplateJavaDestination(TemplateDestinationBase, RealName) };
// Generate the OBB and Shim files here
string ObbFileLocation = ProjectDirectory + "/Saved/StagedBuilds/Android" + CookFlavor + ".obb";
// This is kind of a small hack to get around a rewrite problem
// We need to make sure the file is there but if the OBB file doesn't exist then we don't want to replace it
if (File.Exists(ObbFileLocation) || !File.Exists(UE4OBBDataFileName))
{
WriteJavaOBBDataFile(UE4OBBDataFileName, PackageName, new List { ObbFileLocation });
}
// Make sure any existing proguard file in project is NOT used (back it up)
string ProjectBuildProguardFile = Path.Combine(GameBuildFilesPath, "proguard-project.txt");
if (File.Exists(ProjectBuildProguardFile))
{
string ProjectBackupProguardFile = Path.Combine(GameBuildFilesPath, "proguard-project.backup");
File.Move(ProjectBuildProguardFile, ProjectBackupProguardFile);
}
WriteJavaDownloadSupportFiles(UE4DownloadShimFileName, templates, new Dictionary{
{ "$$GameName$$", ProjectName },
{ "$$PublicKey$$", GetPublicKey() },
{ "$$PackageName$$",PackageName }
});
// Sometimes old files get left behind if things change, so we'll do a clean up pass
{
string CleanUpBaseDir = Path.Combine(ProjectDirectory, "Build", "Android", "src");
var files = Directory.EnumerateFiles(CleanUpBaseDir, "*.java", SearchOption.AllDirectories);
Log.TraceInformation("Cleaning up files based on template dir {0}", TemplateDestinationBase);
// Make a set of files that are okay to clean up
var cleanFiles = new HashSet();
cleanFiles.Add("OBBData.java");
foreach (var template in templates)
{
cleanFiles.Add(Path.GetFileName(template.DestinationFile));
}
foreach (var filename in files)
{
if (filename == UE4DownloadShimFileName) // we always need the shim, and it'll get rewritten if needed anyway
continue;
string filePath = Path.GetDirectoryName(filename); // grab the file's path
if (filePath != TemplateDestinationBase) // and check to make sure it isn't the same as the Template directory we calculated earlier
{
// Only delete the files in the cleanup set
if (!cleanFiles.Contains(Path.GetFileName(filename)))
continue;
Log.TraceInformation("Cleaning up file {0} with path {1}", filename, filePath);
File.Delete(filename);
// Check to see if this file also exists in our target destination, and if so nuke it too
string DestFilename = Path.Combine(UE4BuildPath, Utils.MakePathRelativeTo(filePath, UE4BuildFilesPath));
if (File.Exists(filename))
{
File.Delete(filename);
}
}
}
// Directory clean up code
var directories = Directory.EnumerateDirectories(CleanUpBaseDir, "*", SearchOption.AllDirectories).OrderByDescending(x => x);
foreach (var directory in directories)
{
if (Directory.Exists(directory) && Directory.GetFiles(directory, "*.*", SearchOption.AllDirectories).Count() == 0)
{
Log.TraceInformation("Cleaning Directory {0} as empty.", directory);
Directory.Delete(directory, true);
}
};
}
// cache if we want data in the Apk
bool bPackageDataInsideApk = PackageDataInsideApk(bDisallowPackagingDataInApk);
bool bDisableVerifyOBBOnStartUp = DisableVerifyOBBOnStartUp();
bool bUseExternalFilesDir = UseExternalFilesDir(bDisallowExternalFilesDir);
// check to see if any "meta information" is newer than last time we build
string CurrentBuildSettings = GetAllBuildSettings(ToolChain, UE4BuildPath, bForDistribution, bMakeSeparateApks, bPackageDataInsideApk, bDisableVerifyOBBOnStartUp, bUseExternalFilesDir);
string BuildSettingsCacheFile = Path.Combine(UE4BuildPath, "UEBuildSettings.txt");
// do we match previous build settings?
bool bBuildSettingsMatch = true;
// get application name and whether it changed, needing to force repackage
string ApplicationDisplayName;
if (CheckApplicationName(UE4BuildPath, ProjectName, out ApplicationDisplayName))
{
bBuildSettingsMatch = false;
Log.TraceInformation("Application display name is different than last build, forcing repackage.");
}
// if the manifest matches, look at other settings stored in a file
if (bBuildSettingsMatch)
{
if (File.Exists(BuildSettingsCacheFile))
{
string PreviousBuildSettings = File.ReadAllText(BuildSettingsCacheFile);
if (PreviousBuildSettings != CurrentBuildSettings)
{
bBuildSettingsMatch = false;
Log.TraceInformation("Previous .apk file(s) were made with different build settings, forcing repackage.");
}
}
}
// only check input dependencies if the build settings already match
if (bBuildSettingsMatch)
{
// check if so's are up to date against various inputs
var JavaFiles = new List{
UE4OBBDataFileName,
UE4DownloadShimFileName
};
// Add the generated files too
JavaFiles.AddRange(from t in templates select t.SourceFile);
JavaFiles.AddRange(from t in templates select t.DestinationFile);
bBuildSettingsMatch = CheckDependencies(ToolChain, ProjectName, ProjectDirectory, UE4BuildFilesPath, GameBuildFilesPath,
EngineDirectory, JavaFiles, CookFlavor, OutputPath, UE4BuildPath, bMakeSeparateApks, bPackageDataInsideApk);
}
var Arches = ToolChain.GetAllArchitectures();
var GPUArchitectures = ToolChain.GetAllGPUArchitectures();
// figure out the configuration from output filename
string Configuration = "Development";
string OutputConfig = Path.GetFileNameWithoutExtension(OutputPath);
if (OutputConfig.EndsWith("-Debug"))
{
Configuration = "Debug";
}
else if (OutputConfig.EndsWith("-Test"))
{
Configuration = "Test";
}
else if (OutputConfig.EndsWith("-DebugGame"))
{
Configuration = "Debug";
}
else if (OutputConfig.EndsWith("-Shipping"))
{
Configuration = "Shipping";
}
// Initialize UPL contexts for each architecture enabled
List NDKArches = new List();
foreach (var Arch in Arches)
{
string NDKArch = GetNDKArch(Arch);
if (!NDKArches.Contains(NDKArch))
{
NDKArches.Add(NDKArch);
}
}
UPL.Init(NDKArches, bForDistribution, EngineDirectory, UE4BuildPath, ProjectDirectory, Configuration);
IEnumerable> BuildList = null;
if (!bBuildSettingsMatch)
{
BuildList = from Arch in Arches
from GPUArch in GPUArchitectures
let manifest = GenerateManifest(ToolChain, ProjectName, EngineDirectory, bForDistribution, bPackageDataInsideApk, GameBuildFilesPath, RequiresOBB(bDisallowPackagingDataInApk, ObbFileLocation), bDisableVerifyOBBOnStartUp, Arch, GPUArch, CookFlavor, bUseExternalFilesDir, Configuration)
select Tuple.Create(Arch, GPUArch, manifest);
}
else
{
BuildList = from Arch in Arches
from GPUArch in GPUArchitectures
let manifestFile = Path.Combine(IntermediateAndroidPath, Arch + "_" + GPUArch + "_AndroidManifest.xml")
let manifest = GenerateManifest(ToolChain, ProjectName, EngineDirectory, bForDistribution, bPackageDataInsideApk, GameBuildFilesPath, RequiresOBB(bDisallowPackagingDataInApk, ObbFileLocation), bDisableVerifyOBBOnStartUp, Arch, GPUArch, CookFlavor, bUseExternalFilesDir, Configuration)
let OldManifest = File.Exists(manifestFile) ? File.ReadAllText(manifestFile) : ""
where manifest != OldManifest
select Tuple.Create(Arch, GPUArch, manifest);
}
// Now we have to spin over all the arch/gpu combinations to make sure they all match
int BuildListComboTotal = BuildList.Count();
if (BuildListComboTotal == 0)
{
Log.TraceInformation("Output .apk file(s) are up to date (dependencies and build settings are up to date)");
return;
}
// Once for all arches code:
// make up a dictionary of strings to replace in xml files (strings.xml)
Dictionary Replacements = new Dictionary();
Replacements.Add("${EXECUTABLE_NAME}", ApplicationDisplayName);
if (!bIncrementalPackage)
{
// Wipe the Intermediate/Build/APK directory first, except for dexedLibs, because Google Services takes FOREVER to predex, and it almost never changes
// so allow the ANT checking to win here - if this grows a bit with extra libs, it's fine, it _should_ only pull in dexedLibs it needs
Log.TraceInformation("Performing complete package - wiping {0}, except for predexedLibs", UE4BuildPath);
DeleteDirectory(UE4BuildPath, "dexedLibs");
}
// If we are packaging for Amazon then we need to copy the file to the correct location
Log.TraceInformation("bPackageDataInsideApk = {0}", bPackageDataInsideApk);
if (bPackageDataInsideApk)
{
Console.WriteLine("Obb location {0}", ObbFileLocation);
string ObbFileDestination = UE4BuildPath + "/assets";
Console.WriteLine("Obb destination location {0}", ObbFileDestination);
if (File.Exists(ObbFileLocation))
{
Directory.CreateDirectory(UE4BuildPath);
Directory.CreateDirectory(ObbFileDestination);
Console.WriteLine("Obb file exists...");
var DestFileName = Path.Combine(ObbFileDestination, "main.obb.png"); // Need a rename to turn off compression
var SrcFileName = ObbFileLocation;
if (!File.Exists(DestFileName) || File.GetLastWriteTimeUtc(DestFileName) < File.GetLastWriteTimeUtc(SrcFileName))
{
Console.WriteLine("Copying {0} to {1}", SrcFileName, DestFileName);
File.Copy(SrcFileName, DestFileName);
}
}
}
else // try to remove the file it we aren't packaging inside the APK
{
string ObbFileDestination = UE4BuildPath + "/assets";
var DestFileName = Path.Combine(ObbFileDestination, "main.obb.png");
if (File.Exists(DestFileName))
{
File.Delete(DestFileName);
}
}
//Copy build files to the intermediate folder in this order (later overrides earlier):
// - Shared Engine
// - Shared Engine NoRedist (for Epic secret files)
// - Game
// - Game NoRedist (for Epic secret files)
CopyFileDirectory(UE4BuildFilesPath, UE4BuildPath, Replacements);
CopyFileDirectory(UE4BuildFilesPath + "/NotForLicensees", UE4BuildPath, Replacements);
CopyFileDirectory(UE4BuildFilesPath + "/NoRedist", UE4BuildPath, Replacements);
CopyFileDirectory(GameBuildFilesPath, UE4BuildPath, Replacements);
CopyFileDirectory(GameBuildFilesPath + "/NotForLicensees", UE4BuildPath, Replacements);
CopyFileDirectory(GameBuildFilesPath + "/NoRedist", UE4BuildPath, Replacements);
//Extract AAR and Jar files with dependencies
ExtractAARAndJARFiles(EngineDirectory, UE4BuildPath, NDKArches, PackageName);
//Now validate GooglePlay app_id if enabled
ValidateGooglePlay(UE4BuildPath);
//determine which orientation requirements this app has
bool bNeedLandscape = false;
bool bNeedPortrait = false;
DetermineScreenOrientationRequirements(out bNeedPortrait, out bNeedLandscape);
//Now keep the splash screen images matching orientation requested
PickSplashScreenOrientation(UE4BuildPath, bNeedPortrait, bNeedLandscape);
//Now package the app based on Daydream packaging settings
PackageForDaydream(UE4BuildPath);
//Similarly, keep only the downloader screen image matching the orientation requested
PickDownloaderScreenOrientation(UE4BuildPath, bNeedPortrait, bNeedLandscape);
// at this point, we can write out the cached build settings to compare for a next build
File.WriteAllText(BuildSettingsCacheFile, CurrentBuildSettings);
// at this point, we can write out the cached build settings to compare for a next build
File.WriteAllText(BuildSettingsCacheFile, CurrentBuildSettings);
///////////////
// in case the game had an AndroidManifest.xml file, we overwrite it now with the generated one
//File.WriteAllText(ManifestFile, NewManifest);
///////////////
Log.TraceInformation("\n===={0}====PREPARING NATIVE CODE=================================================================", DateTime.Now.ToString());
bool HasNDKPath = File.Exists(NDKBuildPath);
// get Ant verbosity level
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
string AntVerbosity;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "AntVerbosity", out AntVerbosity);
foreach (var build in BuildList)
{
string Arch = build.Item1;
string GPUArchitecture = build.Item2;
string Manifest = build.Item3;
string NDKArch = GetNDKArch(Arch);
string SourceSOName = AndroidToolChain.InlineArchName(OutputPath, Arch, GPUArchitecture);
// if the source binary was UE4Game, replace it with the new project name, when re-packaging a binary only build
string ApkFilename = Path.GetFileNameWithoutExtension(OutputPath).Replace("UE4Game", ProjectName);
string DestApkName = Path.Combine(ProjectDirectory, "Binaries/Android/") + ApkFilename + ".apk";
// As we are always making seperate APKs we need to put the architecture into the name
DestApkName = AndroidToolChain.InlineArchName(DestApkName, Arch, GPUArchitecture);
// Write the manifest to the correct locations (cache and real)
String ManifestFile = Path.Combine(IntermediateAndroidPath, Arch + "_" + GPUArchitecture + "_AndroidManifest.xml");
File.WriteAllText(ManifestFile, Manifest);
ManifestFile = Path.Combine(UE4BuildPath, "AndroidManifest.xml");
File.WriteAllText(ManifestFile, Manifest);
// copy prebuild plugin files
UPL.ProcessPluginNode(NDKArch, "prebuildCopies", "");
// update metadata files (like project.properties, build.xml) if we are missing a build.xml or if we just overwrote project.properties with a bad version in it (from game/engine dir)
UpdateProjectProperties(ToolChain, UE4BuildPath, ProjectName);
// modify the generated build.xml before the final include
UpdateBuildXML(Arch, NDKArch, EngineDirectory, UE4BuildPath);
// update GameActivity.java if out of date
UpdateGameActivity(Arch, NDKArch, EngineDirectory, UE4BuildPath);
// Copy the generated .so file from the binaries directory to the jni folder
if (!File.Exists(SourceSOName))
{
throw new BuildException("Can't make an APK without the compiled .so [{0}]", SourceSOName);
}
if (!Directory.Exists(UE4BuildPath + "/jni"))
{
throw new BuildException("Can't make an APK without the jni directory [{0}/jni]", UE4BuildFilesPath);
}
String FinalSOName;
if (HasNDKPath)
{
string LibDir = UE4BuildPath + "/jni/" + GetNDKArch(Arch);
FinalSOName = LibDir + "/libUE4.so";
// check to see if libUE4.so needs to be copied
if (BuildListComboTotal > 1 || FilesAreDifferent(SourceSOName, FinalSOName))
{
Log.TraceInformation("\nCopying new .so {0} file to jni folder...", SourceSOName);
Directory.CreateDirectory(LibDir);
// copy the binary to the standard .so location
File.Copy(SourceSOName, FinalSOName, true);
}
}
else
{
// if no NDK, we don't need any of the debugger stuff, so we just copy the .so to where it will end up
FinalSOName = UE4BuildPath + "/libs/" + GetNDKArch(Arch) + "/libUE4.so";
// check to see if libUE4.so needs to be copied
if (BuildListComboTotal > 1 || FilesAreDifferent(SourceSOName, FinalSOName))
{
Log.TraceInformation("\nCopying .so {0} file to jni folder...", SourceSOName);
Directory.CreateDirectory(Path.GetDirectoryName(FinalSOName));
File.Copy(SourceSOName, FinalSOName, true);
}
}
// remove any read only flags
FileInfo DestFileInfo = new FileInfo(FinalSOName);
DestFileInfo.Attributes = DestFileInfo.Attributes & ~FileAttributes.ReadOnly;
File.SetLastWriteTimeUtc(FinalSOName, File.GetLastWriteTimeUtc(SourceSOName));
// if we need to run ndk-build, do it now
if (HasNDKPath)
{
string LibSOName = UE4BuildPath + "/libs/" + GetNDKArch(Arch) + "/libUE4.so";
// always delete libs up to this point so fat binaries and incremental builds work together (otherwise we might end up with multiple
// so files in an apk that doesn't want them)
// note that we don't want to delete all libs, just the ones we copied
TimeSpan Diff = File.GetLastWriteTimeUtc(LibSOName) - File.GetLastWriteTimeUtc(FinalSOName);
if (!File.Exists(LibSOName) || Diff.TotalSeconds < -1 || Diff.TotalSeconds > 1)
{
foreach (string Lib in Directory.EnumerateFiles(UE4BuildPath + "/libs", "libUE4*.so", SearchOption.AllDirectories))
{
File.Delete(Lib);
}
string CommandLine = "APP_ABI=\"" + GetNDKArch(Arch) + " " + "\"";
if (!bForDistribution)
{
CommandLine += " NDK_DEBUG=1";
}
RunCommandLineProgramWithException(UE4BuildPath, NDKBuildPath, CommandLine, "Preparing native code for debugging...", true);
File.SetLastWriteTimeUtc(LibSOName, File.GetLastWriteTimeUtc(FinalSOName));
}
}
// after ndk-build is called, we can now copy in the stl .so (ndk-build deletes old files)
// copy libgnustl_shared.so to library (use 4.8 if possible, otherwise 4.6)
CopySTL(ToolChain, UE4BuildPath, Arch, NDKArch, bForDistribution);
CopyGfxDebugger(UE4BuildPath, Arch, NDKArch);
// copy postbuild plugin files
UPL.ProcessPluginNode(NDKArch, "resourceCopies", "");
Log.TraceInformation("\n===={0}====PERFORMING FINAL APK PACKAGE OPERATION================================================", DateTime.Now.ToString());
string AntBuildType = "debug";
string AntOutputSuffix = "-debug";
if (bForDistribution)
{
// Generate the Proguard file contents and write it
string ProguardContents = GenerateProguard(NDKArch, UE4BuildFilesPath, GameBuildFilesPath);
string ProguardFilename = Path.Combine(UE4BuildPath, "proguard-project.txt");
if (File.Exists(ProguardFilename))
{
File.Delete(ProguardFilename);
}
File.WriteAllText(ProguardFilename, ProguardContents);
// this will write out ant.properties with info needed to sign a distribution build
PrepareToSignApk(UE4BuildPath);
AntBuildType = "release";
AntOutputSuffix = "-release";
}
// Use ant to build the .apk file
string AntOptions = AntBuildType + " -Djava.source=1.7 -Djava.target=1.7";
string ShellExecutable = Utils.IsRunningOnMono ? "/bin/sh" : "cmd.exe";
string ShellParametersBegin = Utils.IsRunningOnMono ? "-c '" : "/c ";
string ShellParametersEnd = Utils.IsRunningOnMono ? "'" : "";
switch (AntVerbosity.ToLower())
{
default:
case "quiet":
if (RunCommandLineProgramAndReturnResult(UE4BuildPath, ShellExecutable, ShellParametersBegin + "\"" + GetAntPath() + "\" -quiet " + AntOptions + ShellParametersEnd, "Making .apk with Ant... (note: it's safe to ignore javac obsolete warnings)") != 0)
{
RunCommandLineProgramAndReturnResult(UE4BuildPath, ShellExecutable, ShellParametersBegin + "\"" + GetAntPath() + "\" " + AntOptions + ShellParametersEnd, "Making .apk with Ant again to show errors");
}
break;
case "normal":
RunCommandLineProgramAndReturnResult(UE4BuildPath, ShellExecutable, ShellParametersBegin + "\"" + GetAntPath() + "\" " + AntOptions + ShellParametersEnd, "Making .apk with Ant again to show errors");
break;
case "verbose":
RunCommandLineProgramAndReturnResult(UE4BuildPath, ShellExecutable, ShellParametersBegin + "\"" + GetAntPath() + "\" -verbose " + AntOptions + ShellParametersEnd, "Making .apk with Ant again to show errors");
break;
}
// make sure destination exists
Directory.CreateDirectory(Path.GetDirectoryName(DestApkName));
// now copy to the final location
File.Copy(UE4BuildPath + "/bin/" + ProjectName + AntOutputSuffix + ".apk", DestApkName, true);
bool bBuildWithHiddenSymbolVisibility = false;
Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bBuildWithHiddenSymbolVisibility", out bBuildWithHiddenSymbolVisibility);
if ( Configuration == "Shipping" && bBuildWithHiddenSymbolVisibility)
{
// Copy .so with symbols to
int StoreVersion = GetStoreVersion();
string SymbolSODirectory = Path.Combine(Path.GetDirectoryName(DestApkName), ProjectName + "_Symbols_v" + StoreVersion + "/" + ProjectName + Arch + GPUArchitecture);
string SymbolifiedSOPath = Path.Combine(SymbolSODirectory, Path.GetFileName(FinalSOName));
MakeDirectoryIfRequired(SymbolifiedSOPath);
File.Copy(FinalSOName, SymbolifiedSOPath, true);
}
}
}
private void PrepareToSignApk(string BuildPath)
{
// ini file to get settings from
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
string KeyAlias, KeyStore, KeyStorePassword, KeyPassword;
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "KeyAlias", out KeyAlias);
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "KeyStore", out KeyStore);
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "KeyStorePassword", out KeyStorePassword);
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "KeyPassword", out KeyPassword);
if (string.IsNullOrEmpty(KeyAlias) || string.IsNullOrEmpty(KeyStore) || string.IsNullOrEmpty(KeyStorePassword))
{
throw new BuildException("DistributionSigning settings are not all set. Check the DistributionSettings section in the Andriod tab of Project Settings");
}
string[] AntPropertiesLines = new string[4];
AntPropertiesLines[0] = "key.store=" + KeyStore;
AntPropertiesLines[1] = "key.alias=" + KeyAlias;
AntPropertiesLines[2] = "key.store.password=" + KeyStorePassword;
AntPropertiesLines[3] = "key.alias.password=" + ((string.IsNullOrEmpty(KeyPassword) || KeyPassword == "_sameaskeystore_") ? KeyStorePassword : KeyPassword);
// now write out the properties
File.WriteAllLines(Path.Combine(BuildPath, "ant.properties"), AntPropertiesLines);
}
private List CollectPluginDataPaths(TargetReceipt Receipt)
{
List PluginExtras = new List();
if (Receipt == null)
{
Log.TraceInformation("Receipt is NULL");
return PluginExtras;
}
// collect plugin extra data paths from target receipt
var Results = Receipt.AdditionalProperties.Where(x => x.Name == "AndroidPlugin");
foreach (var Property in Results)
{
// Keep only unique paths
string PluginPath = Property.Value;
if (PluginExtras.FirstOrDefault(x => x == PluginPath) == null)
{
PluginExtras.Add(PluginPath);
Log.TraceInformation("AndroidPlugin: {0}", PluginPath);
}
}
return PluginExtras;
}
public override bool PrepTargetForDeployment(UEBuildDeployTarget InTarget)
{
//Log.TraceInformation("$$$$$$$$$$$$$$ PrepTargetForDeployment $$$$$$$$$$$$$$$$$ {0}", InTarget.TargetName);
AndroidToolChain ToolChain = new AndroidToolChain(InTarget.ProjectFile, false, InTarget.AndroidArchitectures, InTarget.AndroidGPUArchitectures);
// we need to strip architecture from any of the output paths
string BaseSoName = ToolChain.RemoveArchName(InTarget.OutputPaths[0].FullName);
// get the receipt
UnrealTargetPlatform Platform = InTarget.Platform;
UnrealTargetConfiguration Configuration = InTarget.Configuration;
string ProjectBaseName = Path.GetFileName(BaseSoName).Replace("-" + Platform, "").Replace("-" + Configuration, "").Replace(".so", "");
string ReceiptFilename = TargetReceipt.GetDefaultPath(InTarget.ProjectDirectory.FullName, ProjectBaseName, Platform, Configuration, "");
Log.TraceInformation("Receipt Filename: {0}", ReceiptFilename);
SetAndroidPluginData(ToolChain.GetAllArchitectures(), CollectPluginDataPaths(TargetReceipt.Read(ReceiptFilename)));
// make an apk at the end of compiling, so that we can run without packaging (debugger, cook on the fly, etc)
string RelativeEnginePath = UnrealBuildTool.EngineDirectory.MakeRelativeTo(DirectoryReference.GetCurrentDirectory());
MakeApk(ToolChain, InTarget.TargetName, InTarget.ProjectDirectory.FullName, BaseSoName, RelativeEnginePath, bForDistribution: false, CookFlavor: "",
bMakeSeparateApks: ShouldMakeSeparateApks(), bIncrementalPackage: true, bDisallowPackagingDataInApk: false, bDisallowExternalFilesDir: true);
// if we made any non-standard .apk files, the generated debugger settings may be wrong
if (ShouldMakeSeparateApks() && (InTarget.OutputPaths.Count > 1 || !InTarget.OutputPaths[0].FullName.Contains("-armv7-es2")))
{
Console.WriteLine("================================================================================================================================");
Console.WriteLine("Non-default apk(s) have been made: If you are debugging, you will need to manually select one to run in the debugger properties!");
Console.WriteLine("================================================================================================================================");
}
return true;
}
public static bool ShouldMakeSeparateApks()
{
// @todo android fat binary: Currently, there isn't much utility in merging multiple .so's into a single .apk except for debugging,
// but we can't properly handle multiple GPU architectures in a single .apk, so we are disabling the feature for now
// The user will need to manually select the apk to run in their Visual Studio debugger settings (see Override APK in TADP, for instance)
// If we change this, pay attention to in AndroidProjectGenerator
return true;
// check to see if the project wants separate apks
// ConfigCacheIni Ini = nGetConfigCacheIni("Engine");
// bool bSeparateApks = false;
// Ini.GetBool("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "bSplitIntoSeparateApks", out bSeparateApks);
//
// return bSeparateApks;
}
public bool PrepForUATPackageOrDeploy(FileReference ProjectFile, string ProjectName, string ProjectDirectory, string ExecutablePath, string EngineDirectory, bool bForDistribution, string CookFlavor, bool bIsDataDeploy)
{
//Log.TraceInformation("$$$$$$$$$$$$$$ PrepForUATPackageOrDeploy $$$$$$$$$$$$$$$$$");
// note that we cannot allow the data packaged into the APK if we are doing something like Launch On that will not make an obb
// file and instead pushes files directly via deploy
AndroidToolChain ToolChain = new AndroidToolChain(ProjectFile, false, null, null);
MakeApk(ToolChain, ProjectName, ProjectDirectory, ExecutablePath, EngineDirectory, bForDistribution: bForDistribution, CookFlavor: CookFlavor,
bMakeSeparateApks: ShouldMakeSeparateApks(), bIncrementalPackage: false, bDisallowPackagingDataInApk: bIsDataDeploy, bDisallowExternalFilesDir: !bForDistribution || bIsDataDeploy );
return true;
}
public static void OutputReceivedDataEventHandler(Object Sender, DataReceivedEventArgs Line)
{
if ((Line != null) && (Line.Data != null))
{
Log.TraceInformation(Line.Data);
}
}
private void UpdateBuildXML(string UE4Arch, string NDKArch, string EngineDir, string UE4BuildPath)
{
string SourceFilename = Path.Combine(UE4BuildPath, "build.xml");
string DestFilename = SourceFilename;
Dictionary Replacements = new Dictionary{
{ "", UPL.ProcessPluginNode(NDKArch, "buildXmlPropertyAdditions", "")}
};
string[] TemplateSrc = File.ReadAllLines(SourceFilename);
string[] TemplateDest = File.Exists(DestFilename) ? File.ReadAllLines(DestFilename) : null;
for (int LineIndex = 0; LineIndex < TemplateSrc.Length; ++LineIndex)
{
string SrcLine = TemplateSrc[LineIndex];
bool Changed = false;
foreach (var KVP in Replacements)
{
if (SrcLine.Contains(KVP.Key))
{
// insert replacement before the
SrcLine = SrcLine.Replace(KVP.Key, KVP.Value);
// then add the
SrcLine += KVP.Key;
Changed = true;
}
}
if (Changed)
{
TemplateSrc[LineIndex] = SrcLine;
}
}
if (TemplateDest == null || TemplateSrc.Length != TemplateDest.Length || !TemplateSrc.SequenceEqual(TemplateDest))
{
Log.TraceInformation("\n==== Writing new build.xml file to {0} ====", DestFilename);
File.WriteAllLines(DestFilename, TemplateSrc);
}
}
private void UpdateGameActivity(string UE4Arch, string NDKArch, string EngineDir, string UE4BuildPath)
{
string SourceFilename = Path.Combine(EngineDir, "Build", "Android", "Java", "src", "com", "epicgames", "ue4", "GameActivity.java");
string DestFilename = Path.Combine(UE4BuildPath, "src", "com", "epicgames", "ue4", "GameActivity.java");
string LoadLibraryDefaults = "";
string AndroidGraphicsDebugger;
ConfigHierarchy Ini = GetConfigCacheIni(ConfigHierarchyType.Engine);
Ini.GetString("/Script/AndroidRuntimeSettings.AndroidRuntimeSettings", "AndroidGraphicsDebugger", out AndroidGraphicsDebugger);
switch (AndroidGraphicsDebugger.ToLower())
{
case "mali":
LoadLibraryDefaults += "\t\ttry\n" +
"\t\t{\n" +
"\t\t\tSystem.loadLibrary(\"MGD\");\n" +
"\t\t}\n" +
"\t\tcatch (java.lang.UnsatisfiedLinkError e)\n" +
"\t\t{\n" +
"\t\t\tLog.debug(\"libMGD.so not loaded.\");\n" +
"\t\t}\n";
break;
}
Dictionary Replacements = new Dictionary{
{ "//$${gameActivityImportAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityImportAdditions", "")},
{ "//$${gameActivityPostImportAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityPostImportAdditions", "")},
{ "//$${gameActivityClassAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityClassAdditions", "")},
{ "//$${gameActivityReadMetadataAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityReadMetadataAdditions", "")},
{ "//$${gameActivityOnCreateAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityOnCreateAdditions", "")},
{ "//$${gameActivityOnDestroyAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityOnDestroyAdditions", "")},
{ "//$${gameActivityOnStartAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityOnStartAdditions", "")},
{ "//$${gameActivityOnStopAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityOnStopAdditions", "")},
{ "//$${gameActivityOnPauseAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityOnPauseAdditions", "")},
{ "//$${gameActivityOnResumeAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityOnResumeAdditions", "")},
{ "//$${gameActivityOnNewIntentAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityOnNewIntentAdditions", "")},
{ "//$${gameActivityOnActivityResultAdditions}$$", UPL.ProcessPluginNode(NDKArch, "gameActivityOnActivityResultAdditions", "")},
{ "//$${soLoadLibrary}$$", UPL.ProcessPluginNode(NDKArch, "soLoadLibrary", LoadLibraryDefaults)}
};
string[] TemplateSrc = File.ReadAllLines(SourceFilename);
string[] TemplateDest = File.Exists(DestFilename) ? File.ReadAllLines(DestFilename) : null;
for (int LineIndex = 0; LineIndex < TemplateSrc.Length; ++LineIndex)
{
string SrcLine = TemplateSrc[LineIndex];
bool Changed = false;
foreach (var KVP in Replacements)
{
if(SrcLine.Contains(KVP.Key))
{
SrcLine = SrcLine.Replace(KVP.Key, KVP.Value);
Changed = true;
}
}
if (Changed)
{
TemplateSrc[LineIndex] = SrcLine;
}
}
if (TemplateDest == null || TemplateSrc.Length != TemplateDest.Length || !TemplateSrc.SequenceEqual(TemplateDest))
{
Log.TraceInformation("\n==== Writing new GameActivity.java file to {0} ====", DestFilename);
File.WriteAllLines(DestFilename, TemplateSrc);
}
}
private void ExtractAARAndJARFiles(string EngineDir, string UE4BuildPath, List NDKArches, string AppPackageName)
{
AndroidAARHandler AARHandler = new AndroidAARHandler();
string ImportList = "";
// Get some common paths
string AndroidHome = Environment.ExpandEnvironmentVariables("%ANDROID_HOME%").TrimEnd('/', '\\');
EngineDir = EngineDir.TrimEnd('/', '\\');
// Add the AARs from the default aar-imports.txt
// format: Package,Name,Version
string ImportsFile = Path.Combine(UE4BuildPath, "aar-imports.txt");
if (File.Exists(ImportsFile))
{
ImportList = File.ReadAllText(ImportsFile);
}
// Run the UPL imports section for each architecture and add any new imports (duplicates will be removed)
foreach (string NDKArch in NDKArches)
{
ImportList = UPL.ProcessPluginNode(NDKArch, "AARImports", ImportList);
}
// Add the final list of imports and get dependencies
foreach (string Line in ImportList.Split('\n'))
{
string Trimmed = Line.Trim(' ', '\r');
if (Trimmed.StartsWith("repository "))
{
string DirectoryPath = Trimmed.Substring(11).Trim(' ').TrimEnd('/', '\\');
DirectoryPath = DirectoryPath.Replace("$(ENGINEDIR)", EngineDir);
DirectoryPath = DirectoryPath.Replace("$(ANDROID_HOME)", AndroidHome);
DirectoryPath = DirectoryPath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar);
AARHandler.AddRepository(DirectoryPath);
}
else if (Trimmed.StartsWith("repositories "))
{
string DirectoryPath = Trimmed.Substring(13).Trim(' ').TrimEnd('/', '\\');
DirectoryPath = DirectoryPath.Replace("$(ENGINEDIR)", EngineDir);
DirectoryPath = DirectoryPath.Replace("$(ANDROID_HOME)", AndroidHome);
DirectoryPath = DirectoryPath.Replace('\\', Path.DirectorySeparatorChar).Replace('/', Path.DirectorySeparatorChar);
AARHandler.AddRepositories(DirectoryPath, "m2repository");
}
else
{
string[] Sections = Trimmed.Split(',');
if (Sections.Length == 3)
{
string PackageName = Sections[0].Trim(' ');
string BaseName = Sections[1].Trim(' ');
string Version = Sections[2].Trim(' ');
Log.TraceInformation("AARImports: {0}, {1}, {2}", PackageName, BaseName, Version);
AARHandler.AddNewAAR(PackageName, BaseName, Version);
}
}
}
// Finally, extract the AARs and copy the JARs
AARHandler.ExtractAARs(UE4BuildPath, AppPackageName);
AARHandler.CopyJARs(UE4BuildPath);
}
}
}