// Copyright 1998-2014 Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using System.Diagnostics; using System.Net.NetworkInformation; using AutomationTool; using UnrealBuildTool; public class AndroidPlatform : Platform { public AndroidPlatform() :base(UnrealTargetPlatform.Android) { } private static string GetArchitecture(ProjectParams Params) { // @todo android: Need the architecture passed through from the ProjectParams! return "-armv7"; } private static string GetFinalSOName(ProjectParams Params, string DecoratedExeName) { return Path.Combine(Path.GetDirectoryName(Params.ProjectGameExeFilename), DecoratedExeName) + GetArchitecture(Params) + ".so"; } private static string GetFinalApkName(ProjectParams Params, string DecoratedExeName, bool bRenameUE4Game) { string ProjectDir = Path.GetDirectoryName(Path.GetFullPath(Params.RawProjectPath)); // Apk's go to project location, not necessarily where the .so is (content only packages need to output to their directory) string ApkName = Path.Combine(ProjectDir, "Binaries/Android", DecoratedExeName) + GetArchitecture(Params) + ".apk"; // if the source binary was UE4Game, handle using it or switching to project name if (Path.GetFileNameWithoutExtension(Params.ProjectGameExeFilename) == "UE4Game") { if (bRenameUE4Game) { // replace UE4Game with project name (only replace in the filename part) ApkName = Path.Combine(Path.GetDirectoryName(ApkName), Path.GetFileName(ApkName).Replace("UE4Game", Params.ShortProjectName)); } else { // if we want to use UE4 directly then use it from the engine directory not project directory ApkName = ApkName.Replace(ProjectDir, Path.Combine(CmdEnv.LocalRoot, "Engine")); } } Console.WriteLine("APKName = " + ApkName + " ::: SO name = " + GetFinalSOName(Params, DecoratedExeName)); return ApkName; } private static string GetFinalObbName(string ApkName) { // calculate the name for the .obb file string PackageName = GetPackageInfo(ApkName, false); if (PackageName == null) { throw new AutomationException("Failed to get package name from " + ApkName); } string PackageVersion = GetPackageInfo(ApkName, true); if (PackageVersion == null || PackageVersion.Length == 0) { throw new AutomationException("Failed to get package version from " + ApkName); } if (PackageVersion.Length > 0) { int IntVersion = int.Parse(PackageVersion); PackageVersion = IntVersion.ToString("00000"); } string ObbName = string.Format("main.{0}.{1}.obb", PackageVersion, PackageName); // plop the .obb right next to the executable ObbName = Path.Combine(Path.GetDirectoryName(ApkName), ObbName); return ObbName; } private static string GetDeviceObbName(string ApkName) { string ObbName = GetFinalObbName(ApkName); string PackageName = GetPackageInfo(ApkName, false); return "/mnt/sdcard/obb/" + PackageName + "/" + Path.GetFileName(ObbName); } private static string GetFinalBatchName(string ApkName, ProjectParams Params) { return Path.Combine(Path.GetDirectoryName(ApkName), "Install_" + Params.ShortProjectName + "_" + Params.ClientConfigsToBuild[0].ToString() + ".bat"); } public override void Package(ProjectParams Params, DeploymentContext SC, int WorkingCL) { string SOName = GetFinalSOName(Params, SC.StageExecutables[0]); string ApkName = GetFinalApkName(Params, SC.StageExecutables[0], true); string BatchName = GetFinalBatchName(ApkName, Params); // packaging just takes a pak file and makes it the .obb UEBuildConfiguration.bOBBinAPK = Params.OBBinAPK; // Make sure this setting is sync'd pre-build var Deploy = UEBuildDeploy.GetBuildDeploy(UnrealTargetPlatform.Android); string CookFlavor = SC.CookPlatform.IndexOf("_") > 0 ? SC.CookPlatform.Substring(SC.CookPlatform.IndexOf("_")) : ""; Deploy.PrepForUATPackageOrDeploy(Params.ShortProjectName, SC.ProjectRoot, SOName, SC.LocalRoot + "/Engine", Params.Distribution, CookFlavor); // first, look for a .pak file in the staged directory string[] PakFiles = Directory.GetFiles(SC.StageDirectory, "*.pak", SearchOption.AllDirectories); // for now, we only support 1 pak/obb file if (PakFiles.Length != 1) { throw new AutomationException("Can't package for Android with 0 or more than 1 pak file (found {0} pak files in {1})", PakFiles.Length, SC.StageDirectory); } string LocalObbName = GetFinalObbName(ApkName); string DeviceObbName = GetDeviceObbName(ApkName); // Always delete the target OBB file if it exists if (File.Exists(LocalObbName)) { File.Delete(LocalObbName); } if (!Params.OBBinAPK) { Log("Creating {0} from {1}", LocalObbName, PakFiles[0]); File.Copy(PakFiles[0], LocalObbName); } Log("Writing bat for install with {0}", Params.OBBinAPK ? "OBB in APK" : "OBB separate"); string PackageName = GetPackageInfo(ApkName, false); // make a batch file that can be used to install the .apk and .obb files string[] BatchLines = new string[] { "setlocal", "set ADB=%ANDROID_HOME%\\platform-tools\\adb.exe", "%ADB% uninstall " + PackageName, "%ADB% install " + Path.GetFileName(ApkName), "%ADB% shell rm -rf /mnt/sdcard/" + Params.ShortProjectName, "%ADB% shell rm -rf /mnt/sdcard/UE4Game/UE4CommandLine.txt", // we need to delete the commandline in UE4Game or it will mess up loading "%ADB% shell rm -rf /mnt/sdcard/obb/" + PackageName, Params.OBBinAPK ? "" : "%ADB% push " + Path.GetFileName(LocalObbName) + " " + DeviceObbName, }; File.WriteAllLines(BatchName, BatchLines); PrintRunTime(); } public override void GetFilesToArchive(ProjectParams Params, DeploymentContext SC) { if (SC.StageTargetConfigurations.Count != 1) { throw new AutomationException("Android is currently only able to package one target configuration at a time, but StageTargetConfigurations contained {0} configurations", SC.StageTargetConfigurations.Count); } var TargetConfiguration = SC.StageTargetConfigurations[0]; string ApkName = GetFinalApkName(Params, SC.StageExecutables[0], true); string ObbName = GetFinalObbName(ApkName); string BatchName = GetFinalBatchName(ApkName, Params); // verify the files exist if (!FileExists(ApkName)) { throw new AutomationException("ARCHIVE FAILED - {0} was not found", ApkName); } if (!Params.OBBinAPK && !FileExists(ObbName)) { throw new AutomationException("ARCHIVE FAILED - {0} was not found", ObbName); } SC.ArchiveFiles(Path.GetDirectoryName(ApkName), Path.GetFileName(ApkName)); if(!Params.OBBinAPK) { SC.ArchiveFiles(Path.GetDirectoryName(ObbName), Path.GetFileName(ObbName)); } SC.ArchiveFiles(Path.GetDirectoryName(BatchName), Path.GetFileName(BatchName)); } private string GetAdbCommand(ProjectParams Params) { string SerialNumber = Params.Device; if (SerialNumber.Contains("@")) { // get the bit after the @ SerialNumber = SerialNumber.Split("@".ToCharArray())[1]; } if (SerialNumber != "") { SerialNumber = " -s " + SerialNumber; } return Environment.ExpandEnvironmentVariables("/c %ANDROID_HOME%/platform-tools/adb.exe" + SerialNumber + " "); } public override void Deploy(ProjectParams Params, DeploymentContext SC) { string SOName = GetFinalSOName(Params, SC.StageExecutables[0]); string AdbCommand = GetAdbCommand(Params); string ApkName = GetFinalApkName(Params, SC.StageExecutables[0], true); // make sure APK is up to date (this is fast if so) var Deploy = UEBuildDeploy.GetBuildDeploy(UnrealTargetPlatform.Android); string CookFlavor = SC.CookPlatform.IndexOf("_") > 0 ? SC.CookPlatform.Substring(SC.CookPlatform.IndexOf("_")) : ""; Deploy.PrepForUATPackageOrDeploy(Params.ShortProjectName, SC.ProjectRoot, SOName, SC.LocalRoot + "/Engine", Params.Distribution, CookFlavor); // now we can use the apk to get more info string DeviceObbName = GetDeviceObbName(ApkName); string PackageName = GetPackageInfo(ApkName, false); // install the apk string UninstallCommandline = AdbCommand + "uninstall " + PackageName; RunAndLog(CmdEnv, CmdEnv.CmdExe, UninstallCommandline); string InstallCommandline = AdbCommand + "install \"" + ApkName + "\""; RunAndLog(CmdEnv, CmdEnv.CmdExe, InstallCommandline); // update the ue4commandline.txt // update and deploy ue4commandline.txt // always delete the existing commandline text file, so it doesn't reuse an old one string IntermediateCmdLineFile = CombinePaths(SC.StageDirectory, "UE4CommandLine.txt"); Project.WriteStageCommandline(IntermediateCmdLineFile, Params, SC); // copy files to device if we were staging if (SC.Stage) { // cache some strings string BaseCommandline = AdbCommand + "push"; string RemoteDir = "/mnt/sdcard/" + Params.ShortProjectName; string UE4GameRemoteDir = "/mnt/sdcard/" + Params.ShortProjectName; // make sure device is at a clean state Run(CmdEnv.CmdExe, AdbCommand + "shell rm -r " + RemoteDir); Run(CmdEnv.CmdExe, AdbCommand + "shell rm -r " + UE4GameRemoteDir); string[] Files = Directory.GetFiles(SC.StageDirectory, "*", SearchOption.AllDirectories); // copy each UFS file foreach (string Filename in Files) { // don't push the apk, we install it if (Path.GetExtension(Filename).Equals(".apk", StringComparison.InvariantCultureIgnoreCase)) { continue; } string FinalRemoteDir = RemoteDir; /* // handle the special case of the UE4Commandline.txt when using content only game (UE4Game) if (!Params.IsCodeBasedProject && Path.GetFileName(Filename).Equals("UE4CommandLine.txt", StringComparison.InvariantCultureIgnoreCase)) { FinalRemoteDir = "/mnt/sdcard/UE4Game"; }*/ string RemoteFilename = Filename.Replace(SC.StageDirectory, FinalRemoteDir).Replace("\\", "/"); string Commandline = string.Format("{0} \"{1}\" \"{2}\"", BaseCommandline, Filename, RemoteFilename); Run(CmdEnv.CmdExe, Commandline); } // delete the .obb file, since it will cause nothing we just deployed to be used Run(CmdEnv.CmdExe, AdbCommand + "shell rm " + DeviceObbName); } else { // cache some strings string BaseCommandline = AdbCommand + "push"; string RemoteDir = "/mnt/sdcard/" + Params.ShortProjectName; string FinalRemoteDir = RemoteDir; /* // handle the special case of the UE4Commandline.txt when using content only game (UE4Game) if (!Params.IsCodeBasedProject) { FinalRemoteDir = "/mnt/sdcard/UE4Game"; }*/ string RemoteFilename = IntermediateCmdLineFile.Replace(SC.StageDirectory, FinalRemoteDir).Replace("\\", "/"); string Commandline = string.Format("{0} \"{1}\" \"{2}\"", BaseCommandline, IntermediateCmdLineFile, RemoteFilename); Run(CmdEnv.CmdExe, Commandline); } } /** Internal usage for GetPackageName */ private static string PackageLine = null; /** Run an external exe (and capture the output), given the exe path and the commandline. */ private static string GetPackageInfo(string ApkName, bool bRetrieveVersionCode) { // we expect there to be one, so use the first one string AaptPath = GetAaptPath(); var ExeInfo = new ProcessStartInfo(AaptPath, "dump badging \"" + ApkName + "\""); ExeInfo.UseShellExecute = false; ExeInfo.RedirectStandardOutput = true; using (var GameProcess = Process.Start(ExeInfo)) { PackageLine = null; GameProcess.BeginOutputReadLine(); GameProcess.OutputDataReceived += ParsePackageName; GameProcess.WaitForExit(); } string ReturnValue = null; if (PackageLine != null) { // the line should look like: package: name='com.epicgames.qagame' versionCode='1' versionName='1.0' string[] Tokens = PackageLine.Split("'".ToCharArray()); int TokenIndex = bRetrieveVersionCode ? 3 : 1; if (Tokens.Length >= TokenIndex + 1) { ReturnValue = Tokens[TokenIndex]; } } return ReturnValue; } /** Simple function to pipe output asynchronously */ private static void ParsePackageName(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)) { if (PackageLine == null) { string Line = Event.Data; if (Line.StartsWith("package:")) { PackageLine = Line; } } } } private static string GetAaptPath() { // there is a numbered directory in here, hunt it down string[] Subdirs = Directory.GetDirectories(Environment.ExpandEnvironmentVariables("%ANDROID_HOME%/build-tools/")); if (Subdirs.Length == 0) { throw new AutomationException("Failed to find %ANDROID_HOME%/build-tools subdirectory"); } // we expect there to be one, so use the first one return Path.Combine(Subdirs[0], "aapt.exe"); } public override ProcessResult RunClient(ERunOptions ClientRunFlags, string ClientApp, string ClientCmdLine, ProjectParams Params) { string ApkName = ClientApp + GetArchitecture(Params) + ".apk"; if (!File.Exists(ApkName)) { ApkName = GetFinalApkName(Params, Path.GetFileNameWithoutExtension(ClientApp), true); } Console.WriteLine("Apk='{0}', CLientApp='{1}', ExeName='{2}'", ApkName, ClientApp, Params.ProjectGameExeFilename); // run aapt to get the name of the intent string PackageName = GetPackageInfo(ApkName, false); if (PackageName == null) { throw new AutomationException("Failed to get package name from " + ClientApp); } string AdbCommand = GetAdbCommand(Params); string CommandLine = "shell am start -n " + PackageName + "/com.epicgames.ue4.GameActivity"; // start the app on device! ProcessResult ClientProcess = Run(CmdEnv.CmdExe, AdbCommand + CommandLine, null, ClientRunFlags); return ClientProcess; } public override void GetFilesToDeployOrStage(ProjectParams Params, DeploymentContext SC) { // if (SC.StageExecutables.Count != 1 && Params.Package) // { // throw new AutomationException("Exactly one executable expected when staging Android. Had " + SC.StageExecutables.Count.ToString()); // } // // // stage all built executables // foreach (var Exe in SC.StageExecutables) // { // string ApkName = Exe + GetArchitecture(Params) + ".apk"; // // SC.StageFiles(StagedFileType.NonUFS, Params.ProjectBinariesFolder, ApkName); // } } /// /// Gets cook platform name for this platform. /// /// Additional parameter used to indicate special sub-target platform. /// Cook platform string. public override string GetCookPlatform( bool bDedicatedServer, bool bIsClientOnly, string CookFlavor ) { if( CookFlavor.Length > 0 ) { return "Android_" + CookFlavor; } else { return "Android"; } } public override bool DeployPakInternalLowerCaseFilenames() { return false; } public override bool DeployLowerCaseFilenames(bool bUFSFile) { return false; } public override string LocalPathToTargetPath(string LocalPath, string LocalRoot) { return LocalPath.Replace("\\", "/").Replace(LocalRoot, "../../.."); } public override bool IsSupported { get { return true; } } public override string Remap(string Dest) { return Dest; } public override bool RequiresPak(ProjectParams Params) { return Params.Package; } #region Hooks public override void PostBuildTarget(UE4Build Build, string ProjectName, string UProjectPath, string Config) { // Run UBT w/ the prep for deployment only option // This is required as UBT will 'fake' success when building via UAT and run // the deployment prep step before all the required parts are present. if (ProjectName.Length > 0) { string ProjectToBuild = ProjectName; if (ProjectToBuild != "UE4Game" && !string.IsNullOrEmpty(UProjectPath)) { ProjectToBuild = UProjectPath; } string UBTCommand = string.Format("\"{0}\" Android {1} -prepfordeploy", ProjectToBuild, Config); CommandUtils.RunUBT(UE4Build.CmdEnv, Build.UBTExecutable, UBTCommand); } } #endregion public override List GetDebugFileExtentions() { return new List { }; } }